Skip to content

Commit

Permalink
Feat/candle chart (#40)
Browse files Browse the repository at this point in the history
* [Added] Candle Stick Chart
  • Loading branch information
hi-manshu committed Sep 14, 2022
1 parent 7d02c13 commit 209c536
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 4 deletions.
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
You can find the detail implementation of the following:

- [BarChart](docs/BarChart.md)
- [CandleStickChart](docs/CandleStickChart.md)
- [CombinedBarChart](docs/CombinedBarChart.md)
- [HorizontalBarChart](docs/HorizontalBarChart.md)
- [GroupedBarChart](docs/GroupedBarChart.md)
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/himanshoe/charty/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class MainActivity : ComponentActivity() {
navigator.navigate("groupbar")
}, onCombinedBarChartClicked = {
navigator.navigate("combinedBar")
}, onCandleChartClicked = {
navigator.navigate("candleChart")
}
)
}
Expand All @@ -59,12 +61,14 @@ fun MainApp(
onPointChartClicked: () -> Unit,
onPieChartClicked: () -> Unit,
onGroupHorizontalClicked: () -> Unit,
onCandleChartClicked: () -> Unit,
onGroupBarClicked: () -> Unit,
) {

val list: List<Pair<String, () -> Unit>> =
listOf(
"Bar Chart" to onBarChartClicked,
"Candle Chart" to onCandleChartClicked,
"Combined Bar Chart" to onCombinedBarChartClicked,
"Circle Chart" to onCircleChartClicked,
"Curve Line Chart" to onCurveChartClicked,
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/com/himanshoe/charty/Navigator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.himanshoe.charty.ui.BarChartDemo
import com.himanshoe.charty.ui.CandleStickChartDemo
import com.himanshoe.charty.ui.CircleChartDemo
import com.himanshoe.charty.ui.CombinedBarChartDemo
import com.himanshoe.charty.ui.CurveLineChartDemo
Expand All @@ -28,6 +29,7 @@ fun RegisterNavigation(
onGroupHorizontalClicked: () -> Unit,
onGroupBarClicked: () -> Unit,
onCombinedBarChartClicked: () -> Unit,
onCandleChartClicked: () -> Unit,
) {
NavHost(navController = navigator, startDestination = "main") {
composable("barchart") { BarChartDemo() }
Expand All @@ -42,7 +44,8 @@ fun RegisterNavigation(
onPointChartClicked = onPointChartClicked,
onPieChartClicked = onPieChartClicked,
onGroupHorizontalClicked = onGroupHorizontalClicked,
onCombinedBarChartClicked = onCombinedBarChartClicked
onCombinedBarChartClicked = onCombinedBarChartClicked,
onCandleChartClicked = onCandleChartClicked
)
}
composable("horizontalBarChartDemo") { HorizontalBarChartDemo() }
Expand All @@ -54,5 +57,6 @@ fun RegisterNavigation(
composable("grouphorizontalbar") { GroupedHorizontalBarChartDemo() }
composable("groupbar") { GroupBarChartDemo() }
composable("combinedBar") { CombinedBarChartDemo() }
composable("candleChart") { CandleStickChartDemo() }
}
}
45 changes: 45 additions & 0 deletions app/src/main/java/com/himanshoe/charty/ui/CandleStickChartDemo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.himanshoe.charty.ui

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.himanshoe.charty.candle.CandleStickChart
import com.himanshoe.charty.candle.model.CandleEntry

val candleDummyData = listOf(
CandleEntry(49151.66f, 48899.99f, 49000f, 49058.67f),
CandleEntry(49146.02f, 48840.84f, 48949.8f, 49000f),
CandleEntry(49222.54f, 48905.83f, 49157.65f, 48947.89f),
CandleEntry(49474.09f, 49109.58f, 49335.38f, 49149.15f),
CandleEntry(49500f, 49277.56f, 49314.68f, 49340.75f),
CandleEntry(49896.7f, 49252.85f, 49844.7f, 49314.67f),
CandleEntry(49894.47f, 49760.66f, 49765.13f, 49844.7f),
CandleEntry(49893.15f, 49604.31f, 49893.15f, 49737.15f),
CandleEntry(49146.02f, 48840.84f, 48949.8f, 49000f),
CandleEntry(49222.54f, 48905.83f, 49157.65f, 48947.89f),
CandleEntry(49474.09f, 49109.58f, 49335.38f, 49149.15f),
CandleEntry(49500f, 49277.56f, 49314.68f, 49340.75f),
CandleEntry(49896.7f, 49252.85f, 49844.7f, 49314.67f),
CandleEntry(49894.47f, 49760.66f, 49765.13f, 49844.7f),
CandleEntry(49893.15f, 49604.31f, 49893.15f, 49737.15f),
CandleEntry(49151.66f, 48899.99f, 49000f, 49058.67f),
CandleEntry(49146.02f, 48840.84f, 48949.8f, 49000f),
CandleEntry(49222.54f, 48905.83f, 49157.65f, 48947.89f),
CandleEntry(49474.09f, 49109.58f, 49335.38f, 49149.15f),
CandleEntry(49500f, 49277.56f, 49314.68f, 49340.75f),
CandleEntry(49896.7f, 49252.85f, 49844.7f, 49314.67f),
CandleEntry(49894.47f, 49760.66f, 49765.13f, 49844.7f),
CandleEntry(49893.15f, 49604.31f, 49893.15f, 49737.15f),
)

@Composable
fun CandleStickChartDemo() {
CandleStickChart(
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
candleEntryData = candleDummyData,
)
}
131 changes: 131 additions & 0 deletions charty/src/main/java/com/himanshoe/charty/candle/CandleStickChart.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.himanshoe.charty.candle

import android.graphics.Typeface
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import com.himanshoe.charty.candle.config.CandleStickConfig
import com.himanshoe.charty.candle.config.CandleStickDefaults
import com.himanshoe.charty.candle.model.CandleEntry
import com.himanshoe.charty.common.axis.AxisConfig
import com.himanshoe.charty.common.axis.AxisConfigDefaults
import com.himanshoe.charty.common.axis.yAxis

@Composable
fun CandleStickChart(
candleEntryData: List<CandleEntry>,
modifier: Modifier = Modifier,
axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(),
candleStickConfig: CandleStickConfig = CandleStickDefaults.candleStickDefaults()
) {
val x = remember { Animatable(0f) }
val maxYValue = candleEntryData.maxOf { it.high }
val targetValue = if (candleStickConfig.totalCandles == 0) candleEntryData.count()
.toFloat() else candleStickConfig.totalCandles.toFloat()

LaunchedEffect(true) {
x.animateTo(
targetValue = targetValue,
animationSpec = tween(
durationMillis = if (candleStickConfig.shouldAnimateCandle) 3000 else 0,
easing = FastOutLinearInEasing
),
)
}
val hasPadding = axisConfig.showAxis
Canvas(
modifier = modifier
.padding(
start = if (hasPadding) 40.dp else 10.dp,
top = if (hasPadding) 20.dp else 0.dp
)
.drawBehind {
if (axisConfig.showAxis) {
yAxis(axisConfig, maxYValue, true)
}
}
.padding(start = if (hasPadding) 20.dp else 0.dp)

) {
val xBounds = Pair(0f, candleEntryData.count().toFloat())
val yBounds = getBounds(candleEntryData)
val scaleX = size.width.div(xBounds.second.minus(xBounds.first))
val scaleY = size.height.div(yBounds.second.minus(yBounds.first))
val yMove = yBounds.first.times(scaleY)
val interval = (0.rangeTo(kotlin.math.min(candleEntryData.size.minus(1), x.value.toInt())))
val last = interval.last()

interval.forEach { value ->
val xPoint = value.times(scaleX)
val yPointH = (size.height.minus(candleEntryData[value].high.times(scaleY))).plus(yMove)
val yPointL = (size.height.minus(candleEntryData[value].low.times(scaleY))).plus(yMove)
val yPointO = (size.height.minus(candleEntryData[value].opening.times(scaleY))).plus(yMove)
val yPointC = (size.height.minus(candleEntryData[value].closing.times(scaleY))).plus(yMove)
val path1 = Path()
val path2 = Path()
val isPositive = candleEntryData[value].opening <= candleEntryData[value].closing
val candleColor = if (isPositive) candleStickConfig.positiveColor else candleStickConfig.negativeColor
val candleLineColor = if (isPositive) candleStickConfig.positiveCandleLineColor else candleStickConfig.negativeCandleLineColor

path1.moveTo(xPoint, yPointH)
path1.lineTo(xPoint, yPointL)
drawPath(
path = path1,
color = candleLineColor,
style = Stroke(candleStickConfig.highLowLineWidth)
)

path2.moveTo(xPoint, yPointO)
path2.lineTo(xPoint, yPointC)
drawPath(
path = path2,
color = candleColor,
style = Stroke(scaleX.minus(scaleX.div(8)))
)
}

if (candleStickConfig.showPriceText) {
drawIntoCanvas {
it.nativeCanvas.drawText(
candleEntryData[last].closing.toString(),
(last * scaleX) + 50f, // x-coordinates of the origin (top left)
size.height - (candleEntryData[last].closing * scaleY) + yMove + 20f, // y-coordinates of the origin (top left)
textPaint(candleStickConfig.textColor, size.width.div(30))
)
}
}
}
}

private fun textPaint(textColor: Color = Color.Blue, textSizeValue: Float) =
Paint().asFrameworkPaint().apply {
isAntiAlias = true
textSize = textSizeValue
color = textColor.toArgb()
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
}

private fun getBounds(candleEntryList: List<CandleEntry>): Pair<Float, Float> {
var min = Float.MAX_VALUE
var max = -Float.MAX_VALUE
candleEntryList.forEach {
min = min.coerceAtMost(it.low)
max = max.coerceAtLeast(it.high)
}
return Pair(min, max)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.himanshoe.charty.candle.config

import androidx.compose.ui.graphics.Color

data class CandleStickConfig(
val positiveColor: Color,
val negativeColor: Color,
val positiveCandleLineColor: Color = positiveColor,
val negativeCandleLineColor: Color = negativeColor,
val textColor: Color,
val shouldAnimateCandle: Boolean = true,
val showPriceText: Boolean = true,
val highLowLineWidth: Float,
val totalCandles: Int = 0
)

internal object CandleStickDefaults {

fun candleStickDefaults() = CandleStickConfig(
positiveColor = Color.Green,
negativeColor = Color.Red,
textColor = Color.Black,
highLowLineWidth = 4f
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.himanshoe.charty.candle.model

data class CandleEntry(
val high: Float,
val low: Float,
val opening: Float,
val closing: Float
)
9 changes: 6 additions & 3 deletions charty/src/main/java/com/himanshoe/charty/common/axis/Axis.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import java.text.DecimalFormat

internal fun DrawScope.yAxis(axisConfig: AxisConfig, maxValue: Float) {
internal fun DrawScope.yAxis(axisConfig: AxisConfig, maxValue: Float, isCandleChart: Boolean = false) {
val graphYAxisEndPoint = size.height.div(4)
val pathEffect = PathEffect.dashPathEffect(floatArrayOf(40f, 20f), 0f)
val labelScaleFactor = maxValue.div(4)
Expand All @@ -19,7 +19,7 @@ internal fun DrawScope.yAxis(axisConfig: AxisConfig, maxValue: Float) {
drawIntoCanvas {
it.nativeCanvas.apply {
drawText(
getLabelText(labelScaleFactor.times(4.minus(index))),
getLabelText(labelScaleFactor.times(4.minus(index)), isCandleChart),
0F.minus(25),
yAxisEndPoint.minus(10),
Paint().apply {
Expand All @@ -43,4 +43,7 @@ internal fun DrawScope.yAxis(axisConfig: AxisConfig, maxValue: Float) {
}
}

private fun getLabelText(value: Float) = DecimalFormat("#.##").format(value).toString()
private fun getLabelText(value: Float, isCandleChart: Boolean): String {
val pattern = if (isCandleChart) "#" else "#.##"
return DecimalFormat(pattern).format(value).toString()
}
28 changes: 28 additions & 0 deletions docs/CandleStickChart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## CandleStickChart

> A candlestick chart is a style of financial chart used to describe price movements of a security, derivative, or currency.
### Using CandleStickChart in your project:

```kotlin
CandleStickChart(
candleEntryData = // list of CandleEntry,
modifier = Modifier,
)
```

### Creating Data Set:

to create a data set you need to pass List of `CandleEntry`, where `CandleEntry` looks like:
```kotlin
data class CandleEntry(
val high: Float,
val low: Float,
val opening: Float,
val closing: Float
)
```

### Additional Configuration (Optional)
- To edit Config of the Axis, to suit your need to use `AxisConfig`
- To edit Individual Bar config of it having corner radius you need to use `BarConfig`

0 comments on commit 209c536

Please sign in to comment.