Skip to content

Commit ba2e51f

Browse files
committed
clipRect to chart area, allow min/max values to be outside of rect, adjust inset calculation to avoid crash on screen orientation change
1 parent 968e96a commit ba2e51f

1 file changed

Lines changed: 46 additions & 30 deletions

File tree

  • compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts

compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.PathMeasure
3535
import androidx.compose.ui.graphics.SolidColor
3636
import androidx.compose.ui.graphics.drawscope.DrawScope
3737
import androidx.compose.ui.graphics.drawscope.Stroke
38+
import androidx.compose.ui.graphics.drawscope.clipRect
3839
import androidx.compose.ui.graphics.drawscope.inset
3940
import androidx.compose.ui.graphics.isSpecified
4041
import androidx.compose.ui.graphics.takeOrElse
@@ -72,12 +73,12 @@ import ir.ehsannarmani.compose_charts.models.Line
7273
import ir.ehsannarmani.compose_charts.models.PopupProperties
7374
import ir.ehsannarmani.compose_charts.models.ZeroLineProperties
7475
import ir.ehsannarmani.compose_charts.utils.calculateOffset
75-
import ir.ehsannarmani.compose_charts.utils.rememberComputedChartMaxValue
7676
import kotlinx.coroutines.CoroutineScope
7777
import kotlinx.coroutines.Job
7878
import kotlinx.coroutines.delay
7979
import kotlinx.coroutines.launch
8080
import kotlin.math.max
81+
import kotlin.math.min
8182

8283
private data class Popup(
8384
val properties: PopupProperties,
@@ -116,16 +117,6 @@ fun LineChart(
116117
line.values.minOfOrNull { it } ?: 0.0
117118
} ?: 0.0 else 0.0
118119
) {
119-
if (data.isNotEmpty()) {
120-
require(minValue <= (data.minOfOrNull { line -> line.values.minOfOrNull { it } ?: 0.0 }
121-
?: 0.0)) {
122-
"Chart data must be at least $minValue (Specified Min Value)"
123-
}
124-
require(maxValue >= (data.maxOfOrNull { line -> line.values.maxOfOrNull { it } ?: 0.0 }
125-
?: 0.0)) {
126-
"Chart data must be at most $maxValue (Specified Max Value)"
127-
}
128-
}
129120

130121
val density = LocalDensity.current
131122
val scope = rememberCoroutineScope()
@@ -158,13 +149,20 @@ fun LineChart(
158149
mutableStateListOf<PathData>()
159150
}
160151

161-
val computedMaxValue =
162-
rememberComputedChartMaxValue(minValue, maxValue, indicatorProperties.count)
163-
val indicators = remember(indicatorProperties.indicators, minValue, maxValue) {
152+
val computedMaxValue = remember(maxValue, indicatorProperties.indicators) {
153+
val indicatorMax = indicatorProperties.indicators.maxOrNull() ?: return@remember maxValue
154+
max(maxValue, indicatorMax)
155+
}
156+
val computedMinValue = remember(minValue, indicatorProperties.indicators) {
157+
val indicatorMin = indicatorProperties.indicators.minOrNull() ?: return@remember minValue
158+
min(minValue, indicatorMin)
159+
}
160+
161+
val indicators = remember(indicatorProperties.indicators, computedMinValue, maxValue) {
164162
indicatorProperties.indicators.ifEmpty {
165163
split(
166164
count = indicatorProperties.count,
167-
minValue = minValue,
165+
minValue = computedMinValue,
168166
maxValue = computedMaxValue
169167
)
170168
}
@@ -228,7 +226,7 @@ fun LineChart(
228226
}
229227
}
230228

231-
LaunchedEffect(data, minValue, computedMaxValue) {
229+
LaunchedEffect(data, computedMinValue, computedMaxValue) {
232230
linesPathData.clear()
233231
}
234232

@@ -303,7 +301,7 @@ fun LineChart(
303301
fraction = fraction.toDouble(),
304302
rounded = line.curvedEdges ?: curvedEdges,
305303
size = Size(insetPad.width(size), insetPad.height(size)),
306-
minValue = minValue,
304+
minValue = computedMinValue,
307305
maxValue = computedMaxValue
308306
)
309307
}
@@ -359,7 +357,7 @@ fun LineChart(
359357
LineChartCanvas(
360358
data = data,
361359
maxValue = computedMaxValue,
362-
minValue = minValue,
360+
minValue = computedMinValue,
363361
indicators = indicators,
364362
indicatorProperties = indicatorProperties,
365363
labelProperties = labelProperties,
@@ -380,7 +378,7 @@ fun LineChart(
380378
) { xTicks, yTicks ->
381379
val drawZeroLine = {
382380
val zeroY = size.height - calculateOffset(
383-
minValue = minValue,
381+
minValue = computedMinValue,
384382
maxValue = computedMaxValue,
385383
total = size.height,
386384
value = 0f
@@ -398,7 +396,7 @@ fun LineChart(
398396
getLinePath(
399397
dataPoints = it.values.mapIndexed { index, v -> index.toFloat() to v.toFloat() },
400398
maxValue = computedMaxValue.toFloat(),
401-
minValue = minValue.toFloat(),
399+
minValue = computedMinValue.toFloat(),
402400
rounded = it.curvedEdges ?: curvedEdges,
403401
size = size
404402
)
@@ -500,7 +498,7 @@ fun LineChart(
500498
properties = line.dotProperties ?: dotsProperties,
501499
linePath = segmentedPath,
502500
maxValue = computedMaxValue.toFloat(),
503-
minValue = minValue.toFloat(),
501+
minValue = computedMinValue.toFloat(),
504502
pathMeasure = pathMeasure,
505503
scope = scope,
506504
startIndex = pathData.startIndex,
@@ -598,18 +596,26 @@ private fun DrawScope.drawPopup(
598596

599597
}
600598
if (offsetAnimator != null) {
601-
val animatedOffset = if (popup.properties.mode is PopupProperties.Mode.PointMode) {
599+
var animatedOffset = if (popup.properties.mode is PopupProperties.Mode.PointMode) {
602600
rectOffset
603601
} else {
604602
Offset(
605603
x = offsetAnimator.first.value,
606604
y = offsetAnimator.second.value
607605
)
608606
}
609-
val rect = Rect(
607+
var rect = Rect(
610608
offset = animatedOffset,
611609
size = rectSize
612610
)
611+
if (rect.top < 0) rect = rect.copy(top = 0f, bottom = rect.height)
612+
if (rect.bottom > size.height) rect =
613+
rect.copy(top = size.height - rect.height, bottom = size.height)
614+
if (rect.left < 0) rect = rect.copy(left = 0f, right = rect.width)
615+
if (rect.right > size.width) rect =
616+
rect.copy(left = size.width - rect.width, right = size.width)
617+
618+
animatedOffset = Offset(rect.left, rect.top)
613619
drawPath(
614620
path = Path().apply {
615621
addRoundRect(
@@ -804,15 +810,18 @@ data class InsetPad(
804810
)
805811
}
806812

807-
fun DrawScope.inset(insetPad: InsetPad, block: DrawScope.() -> Unit) =
813+
fun DrawScope.inset(insetPad: InsetPad, block: DrawScope.() -> Unit) {
814+
val mostLeft = size.width / 2f
815+
val mostTop = size.height / 2f
808816
inset(
809-
left = insetPad.left,
810-
right = insetPad.right,
811-
top = insetPad.top,
812-
bottom = insetPad.bottom
817+
left = insetPad.left.coerceAtMost(mostLeft),
818+
right = insetPad.right.coerceAtMost(size.width - mostLeft),
819+
top = insetPad.top.coerceAtMost(mostTop),
820+
bottom = insetPad.bottom.coerceAtMost(size.height - mostTop),
813821
) {
814822
block()
815823
}
824+
}
816825

817826
fun getInsetPad(
818827
textMeasurer: TextMeasurer,
@@ -1008,7 +1017,12 @@ private fun RowScope.LineChartCanvas(
10081017
} else emptyList()
10091018
val xTicks = if (labelProperties.labels.isNotEmpty() && labelProperties.enabled) {
10101019
val measureLabel =
1011-
{ text: String -> textMeasurer.measure(text, style = labelProperties.textStyle) }
1020+
{ text: String ->
1021+
textMeasurer.measure(
1022+
text,
1023+
style = labelProperties.textStyle
1024+
)
1025+
}
10121026
val labels = labelProperties.labels
10131027
val maxLabelHeight = labels.maxOf {
10141028
measureLabel(it).size.height
@@ -1032,7 +1046,9 @@ private fun RowScope.LineChartCanvas(
10321046
}
10331047
} else emptyList()
10341048
inset(insetPad) {
1035-
insetDrawScope(xTicks, yTicks)
1049+
clipRect {
1050+
insetDrawScope(xTicks, yTicks)
1051+
}
10361052
}
10371053
}
10381054
}

0 commit comments

Comments
 (0)