@@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
3737import androidx.compose.ui.graphics.drawscope.Stroke
3838import androidx.compose.ui.graphics.drawscope.clipRect
3939import androidx.compose.ui.graphics.drawscope.inset
40+ import androidx.compose.ui.graphics.drawscope.rotate
4041import androidx.compose.ui.graphics.isSpecified
4142import androidx.compose.ui.graphics.takeOrElse
4243import androidx.compose.ui.input.pointer.PointerInputScope
@@ -73,6 +74,8 @@ import ir.ehsannarmani.compose_charts.models.LabelProperties
7374import ir.ehsannarmani.compose_charts.models.Line
7475import ir.ehsannarmani.compose_charts.models.PopupProperties
7576import ir.ehsannarmani.compose_charts.models.ZeroLineProperties
77+ import ir.ehsannarmani.compose_charts.utils.InsetPad
78+ import ir.ehsannarmani.compose_charts.utils.bounds
7679import ir.ehsannarmani.compose_charts.utils.calculateOffset
7780import kotlinx.coroutines.CoroutineScope
7881import kotlinx.coroutines.Job
@@ -250,7 +253,7 @@ fun LineChart(
250253 if (! properties.enabled) return @forEachIndexed
251254
252255 val bounds = insetPad.toBounds(size)
253- val positionX = position.x.coerceIn(bounds.xMin , bounds.xMax )
256+ val positionX = position.x.coerceIn(bounds.left , bounds.right )
254257 val pathData = linesPathData[dataIndex]
255258
256259 val isSingleValue = line.values.count() == 1
@@ -775,44 +778,6 @@ data class DotInfo(
775778 val value : Float ,
776779)
777780
778- data class InsetBounds (
779- val xMin : Float ,
780- val xMax : Float ,
781- val yMin : Float ,
782- val yMax : Float ,
783- ) {
784- fun xWithin (x : Float ): Boolean {
785- return x in xMin.. xMax
786- }
787-
788- fun yWithin (y : Float ): Boolean {
789- return y in yMin.. yMax
790- }
791-
792- fun pointWithin (x : Float , y : Float ): Boolean {
793- return xWithin(x) && yWithin(y)
794- }
795- }
796-
797- data class InsetPad (
798- var left : Float ,
799- var right : Float ,
800- var top : Float ,
801- var bottom : Float ,
802- ) {
803- fun width (size : IntSize ): Float {
804- return size.width - left - right
805- }
806-
807- fun height (size : IntSize ): Float {
808- return size.height - top - bottom
809- }
810-
811- fun toBounds (size : IntSize ) = InsetBounds (
812- left, size.width - right, top, size.height - bottom
813- )
814- }
815-
816781fun DrawScope.inset (insetPad : InsetPad , block : DrawScope .() -> Unit ) {
817782 val mostLeft = size.width / 2f
818783 val mostTop = size.height / 2f
@@ -826,6 +791,43 @@ fun DrawScope.inset(insetPad: InsetPad, block: DrawScope.() -> Unit) {
826791 }
827792}
828793
794+ data class MinLabelPadding (
795+ val minLeft : Float ,
796+ val minRight : Float ,
797+ val minBottom : Float ,
798+ val shouldRotate : Boolean
799+ )
800+
801+ fun getLabelMinPadding (
802+ labels : List <String >,
803+ labelProperties : LabelProperties ,
804+ measureLabel : (String ) -> TextLayoutResult
805+ ): MinLabelPadding {
806+ if (labels.isEmpty()) return MinLabelPadding (0f , 0f , 0f , false )
807+ val measuredLabels = labels.map { measureLabel(it) }
808+ val labelPad = measureLabel(" M" ).size.width
809+ val maxLabelWidth = measuredLabels.maxOf { it.size.width }
810+ val minLabelWidth = measuredLabels.minOf { it.size.width }
811+ val shouldRotate = labelProperties.rotation.mode == LabelProperties .Rotation .Mode .Force ||
812+ ((maxLabelWidth / minLabelWidth.toDouble()) >= 1.5 && labelProperties.rotation.degree != 0f )
813+ if (shouldRotate) {
814+ val rotatedBounds =
815+ measuredLabels.map { it.bounds.rotate(- labelProperties.rotation.degree) }
816+ return MinLabelPadding (
817+ rotatedBounds[0 ].width / 2f ,
818+ rotatedBounds.last().width / 2f ,
819+ rotatedBounds.maxOf { it.height } + labelPad,
820+ true
821+ )
822+ }
823+ return MinLabelPadding (
824+ measuredLabels[0 ].size.width / 2f ,
825+ measuredLabels.last().size.width / 2f ,
826+ measuredLabels.maxOf { it.size.height }.toFloat() + labelPad,
827+ false
828+ )
829+ }
830+
829831fun getInsetPad (
830832 measureLabel : (String ) -> TextLayoutResult ,
831833 measureIndicator : (String ) -> TextLayoutResult ,
@@ -834,38 +836,28 @@ fun getInsetPad(
834836 labelProperties : LabelProperties
835837): InsetPad {
836838 val labels = labelProperties.labels
837- val insetPad = InsetPad (0f , 0f , 0f , 0f )
839+ val (minLeft, minRight, minBottom, shouldRotate) =
840+ getLabelMinPadding(labels, labelProperties, measureLabel)
841+ val insetPad = InsetPad (0f , 0f , 0f , 0f , shouldRotate)
838842 if (indicators.isNotEmpty() && indicatorProperties.enabled) {
839843 val indicatorPad = measureIndicator(" M" ).size.width
840844 val textIndicators = indicators.map(indicatorProperties.contentBuilder)
841845 val maxIndicatorWidth = textIndicators.maxOf { measureIndicator(it).size.width }
842846 val maxIndicatorHeight = textIndicators.maxOf { measureIndicator(it).size.height }
843847 insetPad.top = maxIndicatorHeight / 2f
844- if (labels.isNotEmpty()) {
845- val labelPad = measureLabel(" M" ).size.width
846- insetPad.bottom = labels.maxOf { measureLabel(it).size.height }.toFloat() + labelPad
847- if (indicatorProperties.position == IndicatorPosition .Horizontal .Start ) {
848- insetPad.left =
849- max(measureLabel(labels[0 ]).size.width / 2f , maxIndicatorWidth.toFloat()) +
850- indicatorPad.toFloat()
851- insetPad.right = measureLabel(labels.last()).size.width / 2f
852- } else {
853- insetPad.left = measureLabel(labels[0 ]).size.width / 2f
854- insetPad.right =
855- max(measureLabel(labels.last()).size.width / 2f , maxIndicatorWidth.toFloat()) +
856- indicatorPad.toFloat()
857- }
848+ insetPad.bottom = max(maxIndicatorHeight / 2f , minBottom)
849+ val indicatorSidePad = max(minLeft, maxIndicatorWidth.toFloat()) + indicatorPad.toFloat()
850+ if (indicatorProperties.position == IndicatorPosition .Horizontal .Start ) {
851+ insetPad.left = indicatorSidePad
852+ insetPad.right = minRight
858853 } else {
859- insetPad.bottom = (maxIndicatorHeight) / 2f
860- if (indicatorProperties.position == IndicatorPosition .Horizontal .Start ) {
861- insetPad.left = (maxIndicatorWidth + indicatorPad).toFloat()
862- } else {
863- insetPad.right = (maxIndicatorWidth + indicatorPad).toFloat()
864- }
854+ insetPad.left = minLeft
855+ insetPad.right = indicatorSidePad
865856 }
866857 } else if (labels.isNotEmpty()) {
867- insetPad.left = measureLabel(labels[0 ]).size.width / 2f
868- insetPad.right = measureLabel(labels.last()).size.width / 2f
858+ insetPad.left = minLeft
859+ insetPad.right = minRight
860+ insetPad.bottom = minBottom
869861 }
870862 return insetPad
871863}
@@ -976,59 +968,109 @@ private fun RowScope.LineChartCanvas(
976968 onPressJobChange = onPressJobChange
977969 )
978970 Canvas (modifier = modifier) {
979- val yTicks = if (indicators.isNotEmpty() && indicatorProperties.enabled) {
980- val sortedIndicators = indicators.sortedBy { - it }.filter { it in minValue.. maxValue }
981- val maxIndicatorWidth = sortedIndicators.maxOf {
982- measureIndicator(indicatorProperties.contentBuilder(it)).size.width
983- }
984- val drawingHeight = size.height - insetPad.top - insetPad.bottom
985- val getOffset = { value: Double ->
986- drawingHeight + insetPad.top - calculateOffset(
987- maxValue,
988- minValue,
989- drawingHeight,
990- value.toFloat()
971+ val yTicks = getTicksAndDrawIndicators(
972+ indicators,
973+ indicatorProperties,
974+ minValue,
975+ maxValue,
976+ measureIndicator,
977+ insetPad,
978+ textMeasurer,
979+ )
980+ val xTicks = getTicksAndDrawLabels(labelProperties, measureLabel, insetPad, textMeasurer)
981+ inset(insetPad) {
982+ insetDrawScope(xTicks + listOf (0f , size.width), yTicks + listOf (0f , size.height))
983+ }
984+ }
985+ }
986+
987+ fun DrawScope.getTicksAndDrawIndicators (
988+ indicators : List <Double >,
989+ indicatorProperties : HorizontalIndicatorProperties ,
990+ minValue : Double ,
991+ maxValue : Double ,
992+ measureIndicator : (String ) -> TextLayoutResult ,
993+ insetPad : InsetPad ,
994+ textMeasurer : TextMeasurer
995+ ): List <Float > {
996+ return if (indicators.isNotEmpty() && indicatorProperties.enabled) {
997+ val sortedIndicators = indicators.sortedBy { - it }.filter { it in minValue.. maxValue }
998+ val maxIndicatorWidth = sortedIndicators.maxOf {
999+ measureIndicator(indicatorProperties.contentBuilder(it)).size.width
1000+ }
1001+ val drawingHeight = size.height - insetPad.top - insetPad.bottom
1002+ val getTick = { value: Double ->
1003+ drawingHeight - calculateOffset(
1004+ maxValue,
1005+ minValue,
1006+ drawingHeight,
1007+ value.toFloat()
1008+ )
1009+ }
1010+ sortedIndicators.map {
1011+ val tick = getTick(it)
1012+ val offset = tick + insetPad.top
1013+ val text = indicatorProperties.contentBuilder(it)
1014+ val textSize = measureIndicator(text)
1015+ if (indicatorProperties.position == IndicatorPosition .Horizontal .Start ) {
1016+ drawText(
1017+ textMeasurer = textMeasurer,
1018+ text = text,
1019+ style = indicatorProperties.textStyle,
1020+ topLeft = Offset (
1021+ (maxIndicatorWidth - textSize.size.width).toFloat(),
1022+ offset.toFloat() - textSize.size.height / 2f
1023+ )
9911024 )
992- }
993- sortedIndicators.map {
994- val offset = getOffset(it)
995- val text = indicatorProperties.contentBuilder(it)
996- val textSize = measureIndicator(text)
997- if (indicatorProperties.position == IndicatorPosition .Horizontal .Start ) {
998- drawText(
999- textMeasurer = textMeasurer,
1000- text = text,
1001- style = indicatorProperties.textStyle,
1002- topLeft = Offset (
1003- (maxIndicatorWidth - textSize.size.width).toFloat(),
1004- offset.toFloat() - textSize.size.height / 2f
1005- )
1025+ } else {
1026+ drawText(
1027+ textMeasurer = textMeasurer,
1028+ text = text,
1029+ style = indicatorProperties.textStyle,
1030+ topLeft = Offset (
1031+ (size.width - maxIndicatorWidth),
1032+ offset.toFloat() - textSize.size.height / 2f
10061033 )
1007- } else {
1034+ )
1035+ }
1036+ tick.toFloat()
1037+ }
1038+ } else emptyList()
1039+ }
1040+
1041+ fun DrawScope.getTicksAndDrawLabels (
1042+ labelProperties : LabelProperties ,
1043+ measureLabel : (String ) -> TextLayoutResult ,
1044+ insetPad : InsetPad ,
1045+ textMeasurer : TextMeasurer
1046+ ): List <Float > {
1047+ return if (labelProperties.labels.isNotEmpty() && labelProperties.enabled) {
1048+ val labels = labelProperties.labels
1049+ val maxLabelHeight = labels.maxOf {
1050+ measureLabel(it).size.height
1051+ }
1052+ val insetBounds = insetPad.toBounds(size)
1053+ val bottomStart = insetBounds.bottom + measureLabel(" M" ).size.width.let {
1054+ if (insetPad.shouldRotate) it* 2f / 3 else it.toFloat()
1055+ }
1056+ labels.mapIndexed { index, label ->
1057+ val tick = insetBounds.width * index / (labels.size - 1 )
1058+ val offset = insetBounds.left + tick
1059+ val textSize = measureLabel(label)
1060+ if (insetPad.shouldRotate) {
1061+ val degrees = labelProperties.rotation.degree
1062+ val bounds = textSize.bounds
1063+ val rotatedBounds = bounds.rotate(- degrees)
1064+ val center = Offset (offset, bottomStart + rotatedBounds.height / 2f )
1065+ rotate(degrees, pivot = center) {
10081066 drawText(
10091067 textMeasurer = textMeasurer,
1010- text = text,
1011- style = indicatorProperties.textStyle,
1012- topLeft = Offset (
1013- (size.width - maxIndicatorWidth),
1014- offset.toFloat() - textSize.size.height / 2f
1015- )
1068+ text = label,
1069+ style = labelProperties.textStyle,
1070+ topLeft = center - Offset (textSize.size.width/ 2f , textSize.size.height/ 2f )
10161071 )
10171072 }
1018- (offset - insetPad.top).toFloat()
1019- }
1020- } else emptyList()
1021- val xTicks = if (labelProperties.labels.isNotEmpty() && labelProperties.enabled) {
1022- val labels = labelProperties.labels
1023- val maxLabelHeight = labels.maxOf {
1024- measureLabel(it).size.height
1025- }
1026- val start = insetPad.left
1027- val end = size.width - insetPad.right
1028- val range = end - start
1029- labels.mapIndexed { index, label ->
1030- val offset = start + range * index / (labels.size - 1 )
1031- val textSize = measureLabel(label)
1073+ } else {
10321074 drawText(
10331075 textMeasurer = textMeasurer,
10341076 text = label,
@@ -1038,13 +1080,8 @@ private fun RowScope.LineChartCanvas(
10381080 size.height - maxLabelHeight
10391081 )
10401082 )
1041- offset - start
10421083 }
1043- } else emptyList()
1044- inset(insetPad) {
1045- insetDrawScope(xTicks + listOf (0f , size.width), yTicks + listOf (0f , size.height))
1084+ tick
10461085 }
1047- }
1086+ } else emptyList()
10481087}
1049-
1050-
0 commit comments