Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/candle chart #40

Merged
merged 2 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`