From 37735885191f83947e36395ee8a7a7a16fee4cf0 Mon Sep 17 00:00:00 2001 From: taki Date: Sun, 29 Dec 2024 23:46:49 +0900 Subject: [PATCH 1/2] Add CandleStickChart Sample --- .../koalaplot/sample/CandleStickSample.kt | 213 ++++++++++++++++++ .../io/github/koalaplot/sample/MainView.kt | 1 + 2 files changed, 214 insertions(+) create mode 100644 src/commonMain/kotlin/io/github/koalaplot/sample/CandleStickSample.kt diff --git a/src/commonMain/kotlin/io/github/koalaplot/sample/CandleStickSample.kt b/src/commonMain/kotlin/io/github/koalaplot/sample/CandleStickSample.kt new file mode 100644 index 0000000..e4a9caa --- /dev/null +++ b/src/commonMain/kotlin/io/github/koalaplot/sample/CandleStickSample.kt @@ -0,0 +1,213 @@ +package io.github.koalaplot.sample + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.dp +import io.github.koalaplot.core.ChartLayout +import io.github.koalaplot.core.bar.CandleStickPlot +import io.github.koalaplot.core.bar.CandleStickPlotEntry +import io.github.koalaplot.core.bar.candleStickPlotEntry +import io.github.koalaplot.core.legend.FlowLegend +import io.github.koalaplot.core.legend.LegendLocation +import io.github.koalaplot.core.style.KoalaPlotTheme +import io.github.koalaplot.core.style.LineStyle +import io.github.koalaplot.core.util.ExperimentalKoalaPlotApi +import io.github.koalaplot.core.util.VerticalRotation +import io.github.koalaplot.core.util.rotateVertically +import io.github.koalaplot.core.xygraph.* + + +private val colors = listOf(Color.Green, Color.Red) + +private val rotationOptions = listOf(0, 30, 45, 60, 90) + +private fun candleStickEntries(): List> { + return CandleStickData.dates.mapIndexed { index, date -> + candleStickPlotEntry( + x = date, + open = CandleStickData.open[index], + close = CandleStickData.close[index], + high = CandleStickData.high[index], + low = CandleStickData.low[index] + ) + } +} + +@Composable +private fun Legend(thumbnail: Boolean = false) { + if (!thumbnail) { + Surface(shadowElevation = 2.dp) { + FlowLegend( + itemCount = 2, + symbol = { i -> + Box( + modifier = Modifier + .size(padding) + .background(color = colors[i]) + ) + }, + label = { i -> + Text(if (i == 0) "Increasing" else "Decreasing") + }, + modifier = paddingMod + ) + } + } +} + +val candleStickSampleView = object : SampleView { + override val name: String = "Candle Stick" + + override val thumbnail = @Composable { + ThumbnailTheme { + CandleStickSamplePlot(true, name) + } + } + + override val content: @Composable () -> Unit = @Composable { + var selectedOption by remember { mutableStateOf(rotationOptions[0]) } + KoalaPlotTheme(axis = KoalaPlotTheme.axis.copy(minorGridlineStyle = minorGridLineStyle)) { + Column { + CandleStickSamplePlot( + false, + "Stock Price", + Modifier.weight(1f), + selectedOption + ) + ExpandableCard( + colors = CardDefaults.elevatedCardColors(), + elevation = CardDefaults.elevatedCardElevation(), + titleContent = { + Text("X-Axis Label Angle", modifier = paddingMod) + } + ) { + Column { + rotationOptions.forEach { + Row( + Modifier.fillMaxWidth() + .selectable(selected = (it == selectedOption), onClick = { selectedOption = it }) + ) { + RadioButton(selected = (it == selectedOption), onClick = { selectedOption = it }) + Text(text = it.toString()) + } + } + } + } + } + } + } +} + +@OptIn(ExperimentalKoalaPlotApi::class) +@Composable +private fun CandleStickSamplePlot( + thumbnail: Boolean = false, + title: String, + modifier: Modifier = Modifier, + xAxisLabelRotation: Int = 0 +) { + val candleStickEntries = remember { candleStickEntries() } + var cursorPosition by remember { mutableStateOf?>(null) } + + ChartLayout( + modifier = modifier.then(paddingMod), + title = { ChartTitle(title) }, + legend = { Legend(thumbnail) }, + legendLocation = LegendLocation.BOTTOM + ) { + XYGraph( + xAxisModel = CategoryAxisModel(CandleStickData.dates), + yAxisModel = FloatLinearAxisModel(CandleStickData.low.min()..CandleStickData.high.max()), + xAxisStyle = rememberAxisStyle(labelRotation = xAxisLabelRotation), + xAxisLabels = { + if (!thumbnail) AxisLabel("$it", Modifier.padding(top = 2.dp)) + }, + xAxisTitle = { + if (!thumbnail) AxisTitle("Date", modifier = paddingMod) + }, + yAxisStyle = rememberAxisStyle(minorTickSize = 0.dp), + yAxisLabels = { + if (!thumbnail) { + AxisLabel(it.toString(), Modifier.absolutePadding(right = 2.dp)) + } + }, + yAxisTitle = { + if (!thumbnail) { + AxisTitle( + "Price", + modifier = Modifier.rotateVertically(VerticalRotation.COUNTER_CLOCKWISE) + .padding(bottom = padding) + ) + } + }, + verticalMajorGridLineStyle = null + ) { + CandleStickPlot( + defaultCandle = { entry -> + if (!thumbnail) { + + Box( + modifier = Modifier.hoverableElement { + cursorPosition = Point(entry.x, getCurrentPointer().y ) + } + ) { + + HoverSurface { + Column(Modifier.padding(1.dp).background(Color.White)) { + Text("Close: ${entry.close}") + } + } + } + + } + } + + ) { + candleStickEntries.forEach { entry -> + item(entry) + } + } + cursorPosition?.let { position -> + val chartScope = this + chartScope.getCurrentPointer() +// println("Pointer Position: $pointerPosition") + val (_, chartY) = chartScope.mouseToChartOffset + val graphSize = chartScope.graphSize +// println("adjusted Y: $chartY") + val yMin = CandleStickData.low.min() + val yMax = CandleStickData.high.max() +// println("Y Height: ${graphSize.height}") + + val yValue = yMax - (chartY / graphSize.height.toFloat()) * (yMax - yMin) +// println("Y Value: $yValue") + + HorizontalLineAnnotation(yValue, LineStyle(SolidColor(Color.Red))) + XYAnnotation(Point(position.x, yValue), AnchorPoint.TopLeft) { + Text("Price: $yValue") + } + } + } + } + } +private val minorGridLineStyle = LineStyle( + brush = SolidColor(Color.LightGray), + pathEffect = PathEffect.dashPathEffect(floatArrayOf(2f, 2f)) +) + +object CandleStickData { + val dates = listOf(20230501, 20230502, 20230503, 20230504, 20230505) + val open = listOf(100f, 150f, 120f, 200f, 180f) + val close = listOf(120f, 130f, 180f, 190f, 160f) + val high = listOf(130f, 160f, 200f, 210f, 190f) + val low = listOf(90f, 110f, 100f, 170f, 150f) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/koalaplot/sample/MainView.kt b/src/commonMain/kotlin/io/github/koalaplot/sample/MainView.kt index ca81f83..6e4d702 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/sample/MainView.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/sample/MainView.kt @@ -96,6 +96,7 @@ private val samples = buildList { add(timeLineSampleView) add(liveTimeChartSampleView) add(xyLineChartGestureSampleView) + add(candleStickSampleView) } @OptIn(ExperimentalMaterial3Api::class) From cd29a3a5d560aaef95617ec87372746498e79aa2 Mon Sep 17 00:00:00 2001 From: taki Date: Mon, 3 Feb 2025 03:32:42 +0900 Subject: [PATCH 2/2] CandleStick Chart Sample --- .../koalaplot/sample/CandleStickSample.kt | 131 ++++++++---------- 1 file changed, 61 insertions(+), 70 deletions(-) diff --git a/src/commonMain/kotlin/io/github/koalaplot/sample/CandleStickSample.kt b/src/commonMain/kotlin/io/github/koalaplot/sample/CandleStickSample.kt index e4a9caa..bff59b1 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/sample/CandleStickSample.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/sample/CandleStickSample.kt @@ -26,7 +26,6 @@ import io.github.koalaplot.core.util.VerticalRotation import io.github.koalaplot.core.util.rotateVertically import io.github.koalaplot.core.xygraph.* - private val colors = listOf(Color.Green, Color.Red) private val rotationOptions = listOf(0, 30, 45, 60, 90) @@ -55,10 +54,10 @@ private fun Legend(thumbnail: Boolean = false) { .size(padding) .background(color = colors[i]) ) - }, + }, label = { i -> Text(if (i == 0) "Increasing" else "Decreasing") - }, + }, modifier = paddingMod ) } @@ -119,86 +118,78 @@ private fun CandleStickSamplePlot( val candleStickEntries = remember { candleStickEntries() } var cursorPosition by remember { mutableStateOf?>(null) } - ChartLayout( - modifier = modifier.then(paddingMod), - title = { ChartTitle(title) }, - legend = { Legend(thumbnail) }, - legendLocation = LegendLocation.BOTTOM + ChartLayout( + modifier = modifier.then(paddingMod), + title = { ChartTitle(title) }, + legend = { Legend(thumbnail) }, + legendLocation = LegendLocation.BOTTOM + ) { + XYGraph( + xAxisModel = CategoryAxisModel(CandleStickData.dates), + yAxisModel = FloatLinearAxisModel(CandleStickData.low.min()..CandleStickData.high.max()), + xAxisStyle = rememberAxisStyle(labelRotation = xAxisLabelRotation), + xAxisLabels = { + if (!thumbnail) AxisLabel("$it", Modifier.padding(top = 2.dp)) + }, + xAxisTitle = { + if (!thumbnail) AxisTitle("Date", modifier = paddingMod) + }, + yAxisStyle = rememberAxisStyle(minorTickSize = 0.dp), + yAxisLabels = { + if (!thumbnail) { + AxisLabel(it.toString(), Modifier.absolutePadding(right = 2.dp)) + } + }, + yAxisTitle = { + if (!thumbnail) { + AxisTitle( + "Price", + modifier = Modifier.rotateVertically(VerticalRotation.COUNTER_CLOCKWISE) + .padding(bottom = padding) + ) + } + }, + verticalMajorGridLineStyle = null ) { - XYGraph( - xAxisModel = CategoryAxisModel(CandleStickData.dates), - yAxisModel = FloatLinearAxisModel(CandleStickData.low.min()..CandleStickData.high.max()), - xAxisStyle = rememberAxisStyle(labelRotation = xAxisLabelRotation), - xAxisLabels = { - if (!thumbnail) AxisLabel("$it", Modifier.padding(top = 2.dp)) - }, - xAxisTitle = { - if (!thumbnail) AxisTitle("Date", modifier = paddingMod) - }, - yAxisStyle = rememberAxisStyle(minorTickSize = 0.dp), - yAxisLabels = { - if (!thumbnail) { - AxisLabel(it.toString(), Modifier.absolutePadding(right = 2.dp)) - } - }, - yAxisTitle = { + CandleStickPlot( + defaultCandle = { entry -> if (!thumbnail) { - AxisTitle( - "Price", - modifier = Modifier.rotateVertically(VerticalRotation.COUNTER_CLOCKWISE) - .padding(bottom = padding) - ) - } - }, - verticalMajorGridLineStyle = null - ) { - CandleStickPlot( - defaultCandle = { entry -> - if (!thumbnail) { - Box( - modifier = Modifier.hoverableElement { - cursorPosition = Point(entry.x, getCurrentPointer().y ) - } - ) { - - HoverSurface { - Column(Modifier.padding(1.dp).background(Color.White)) { - Text("Close: ${entry.close}") - } + Box( + modifier = Modifier.hoverableElement { + val pd = pointerData + if (pd != null) { + cursorPosition = Point(pd.x, pd.y) + } else { + cursorPosition = null } } + ) { + HoverSurface { + Column(Modifier.padding(1.dp).background(Color.White)) { + Text("Close: ${entry.close}") + } + } } } - - ) { - candleStickEntries.forEach { entry -> - item(entry) - } + }, + ) { + candleStickEntries.forEach { entry -> + item(entry) } - cursorPosition?.let { position -> - val chartScope = this - chartScope.getCurrentPointer() -// println("Pointer Position: $pointerPosition") - val (_, chartY) = chartScope.mouseToChartOffset - val graphSize = chartScope.graphSize -// println("adjusted Y: $chartY") - val yMin = CandleStickData.low.min() - val yMax = CandleStickData.high.max() -// println("Y Height: ${graphSize.height}") - - val yValue = yMax - (chartY / graphSize.height.toFloat()) * (yMax - yMin) -// println("Y Value: $yValue") + } - HorizontalLineAnnotation(yValue, LineStyle(SolidColor(Color.Red))) - XYAnnotation(Point(position.x, yValue), AnchorPoint.TopLeft) { - Text("Price: $yValue") - } + pointerData?.let { + HorizontalLineAnnotation(it.y, LineStyle(SolidColor(Color.Red))) + XYAnnotation(Point(it.x, it.y), AnchorPoint.TopLeft) { + Text("Price: ${it.y.toString()}") } } } } +} + private val minorGridLineStyle = LineStyle( brush = SolidColor(Color.LightGray), pathEffect = PathEffect.dashPathEffect(floatArrayOf(2f, 2f)) @@ -210,4 +201,4 @@ object CandleStickData { val close = listOf(120f, 130f, 180f, 190f, 160f) val high = listOf(130f, 160f, 200f, 210f, 190f) val low = listOf(90f, 110f, 100f, 170f, 150f) -} \ No newline at end of file +}