Skip to content

Dial and line chart changes #35

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ package com.netguru.multiplatform.charts
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.tween

sealed class ChartAnimation {
object Disabled : ChartAnimation()
sealed class ChartDisplayAnimation {
object Disabled : ChartDisplayAnimation()

class Simple(
val animationSpec: () -> AnimationSpec<Float> = {
tween(DEFAULT_DURATION, DEFAULT_DELAY)
},
) : ChartAnimation()
) : ChartDisplayAnimation()

class Sequenced(
val animationSpec: (dataSeriesIndex: Int) -> AnimationSpec<Float> = { index ->
tween(DEFAULT_DURATION, index * DEFAULT_DELAY)
},
) : ChartAnimation()
) : ChartDisplayAnimation()

private companion object {
const val DEFAULT_DURATION = 300
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,55 @@
package com.netguru.multiplatform.charts

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.layout
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sign

internal fun Double.mapValueToDifferentRange(
inMin: Double,
inMax: Double,
outMin: Double,
outMax: Double,
) = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
) =
if (inMin == inMax) (outMax - outMin).div(2.0) else (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin

fun Float.mapValueToDifferentRange(
inMin: Float,
inMax: Float,
outMin: Float,
outMax: Float,
) = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
) =
if (inMin == inMax) (outMax - outMin).div(2F) else (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin

fun Long.mapValueToDifferentRange(
inMin: Long,
inMax: Long,
outMin: Long,
outMax: Long,
) = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
) =
if (inMin == inMax) (outMax - outMin).div(2) else (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin

fun Long.mapValueToDifferentRange(
inMin: Long,
inMax: Long,
outMin: Float,
outMax: Float,
) = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
) =
if (inMin == inMax) (outMax - outMin).div(2F) else (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin

fun Number.round(decimals: Int = 2): String {
return when (this) {
Expand All @@ -46,20 +60,79 @@ fun Number.round(decimals: Int = 2): String {
} catch (e: IllegalArgumentException) {
"-"
}

else -> {
this.toString()
}
}
}

@Composable
internal fun StartAnimation(animation: ChartAnimation, data: Any): Boolean {
var animationPlayed by remember(data) {
mutableStateOf(animation is ChartAnimation.Disabled)
fun getAnimationAlphas(
animation: ChartDisplayAnimation,
numberOfElementsToAnimate: Int,
uniqueDatasetKey: Any,
): List<Float> {
var animationPlayed by remember(uniqueDatasetKey) {
mutableStateOf(animation is ChartDisplayAnimation.Disabled)
}
LaunchedEffect(Unit) {
LaunchedEffect(uniqueDatasetKey) {
animationPlayed = true
}

return animationPlayed
return when (animation) {
ChartDisplayAnimation.Disabled -> (1..numberOfElementsToAnimate).map { 1f }
is ChartDisplayAnimation.Simple -> (1..numberOfElementsToAnimate).map {
animateFloatAsState(
targetValue = if (animationPlayed) 1f else 0f,
animationSpec = if (animationPlayed) animation.animationSpec() else tween(durationMillis = 0),
).value
}

is ChartDisplayAnimation.Sequenced -> (1..numberOfElementsToAnimate).map {
animateFloatAsState(
targetValue = if (animationPlayed) 1f else 0f,
animationSpec = if (animationPlayed) animation.animationSpec(it) else tween(durationMillis = 0),
).value
}
}
}

fun Modifier.vertical() =
rotate(-90f)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.height, placeable.width) {
placeable.place(
x = -(placeable.width / 2 - placeable.height / 2),
y = -(placeable.height / 2 - placeable.width / 2)
)
}
}

fun Float.toRadians() = this * PI.toFloat() / 180f

fun Float.roundToMultiplicationOf(multiplicand: Float, roundToCeiling: Boolean): Float {
if (this == 0f) {
return 0f
}
if (multiplicand <= 0) {
throw IllegalArgumentException("multiplicand must be positive!")
}

fun Float.ceilOrFloor(ceil: Boolean): Float = if (ceil) {
ceil(this)
} else {
floor(this)
}

val round = when (sign) {
-1f -> !roundToCeiling
1f -> roundToCeiling
else -> throw IllegalStateException("If `this == 0f` this line should never be reached")
}

val closest = (absoluteValue / multiplicand).ceilOrFloor(round) * multiplicand

return sign * closest
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,118 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

private val TOUCH_OFFSET = 20.dp
private val OVERLAY_WIDTH = 200.dp

@Composable
internal fun OverlayInformation(
positionX: Float,
positionX: Float?,
containerSize: Size,
surfaceColor: Color,
touchOffset: Dp = TOUCH_OFFSET,
overlayWidth: Dp = OVERLAY_WIDTH,
backgroundColor: Color,
touchOffsetVertical: Dp,
touchOffsetHorizontal: Dp,
requiredOverlayWidth: Dp?,
overlayAlpha: Float,
pointsToAvoid: List<Offset> = emptyList(),
content: @Composable () -> Unit,
) {
if (positionX < 0) return
if (positionX == null || positionX < 0) {
return
}

val density = LocalDensity.current

var overlayHeight by remember {
mutableStateOf(0)
}
var overlayWidth by remember {
val pxs: Int
with(density) {
pxs = requiredOverlayWidth?.roundToPx() ?: 0
}
mutableStateOf(pxs)
}


val putInfoOnTheLeft = positionX > (containerSize.width / 2)
val (offsetX, offsetY) = remember(pointsToAvoid, overlayHeight, overlayWidth, putInfoOnTheLeft) {
pointsToAvoid
.takeIf { it.isNotEmpty() }
?.let {
val x: Dp
val y: Dp
with(density) {
x = if (putInfoOnTheLeft) {
val minX = it.minOf { it.x }.toDp()
minX - touchOffsetHorizontal - overlayWidth.toDp()
} else {
val maxX = it.maxOf { it.x }.toDp()
maxX + touchOffsetHorizontal
}

val minY = it.minOf { it.y }
val maxY = it.maxOf { it.y }
y = (containerSize.height - ((maxY + minY) / 2) - (overlayHeight / 2))
.coerceIn(
minimumValue = 0f,
maximumValue = containerSize.height - overlayHeight
)
.toDp()
}

x to y
}
?: run {
with(density) {
positionX.toDp() +
// change offset based on cursor position to avoid out of screen drawing on the right
if (putInfoOnTheLeft) {
-overlayWidth.toDp() - touchOffsetHorizontal
} else {
touchOffsetHorizontal
}
} to touchOffsetVertical
}
}

BoxWithConstraints(
modifier = Modifier
.onSizeChanged {
overlayHeight = it.height
if (requiredOverlayWidth == null) {
overlayWidth = it.width
}
}
.offset(
x = with(LocalDensity.current) {
positionX.toDp() +
// change offset based on cursor position to avoid out of screen drawing on the right
if (positionX.toDp() > (containerSize.width / 2).toDp()) {
-OVERLAY_WIDTH - TOUCH_OFFSET
} else {
TOUCH_OFFSET
}
},
y = touchOffset
x = offsetX,
y = offsetY,
)
.width(OVERLAY_WIDTH)
.alpha(0.9f)
.alpha(overlayAlpha)
.clip(RoundedCornerShape(10.dp))
.background(surfaceColor)
.background(backgroundColor)
.padding(8.dp)
.then(
if (requiredOverlayWidth != null) {
Modifier.requiredWidth(requiredOverlayWidth)
} else {
Modifier
}
)
) {
content()
}
Expand Down
Loading