Updating Column chart dynamically throws UnsupportedOperationException - android

We have a Donut chart and when a slice is clicked on the donut chart we would like to update the values in the ColumnChart.
This is how we set the listener for donut chart
segments.forEach(Consumer { segment: PieSegment ->
segment.addIsSelectedChangeListener {
drawColumnChart(clickedSliceProperties)
}
})
The method which draws the ColumnChart
private fun drawColumnChart(values: Array<Int>) {
val xAxis: IAxis = sciChartBuilder.newNumericAxis().withGrowBy(0.15, 0.15).withLabelProvider(YearsLabelProvider()).build()
val yAxis: IAxis = sciChartBuilder.newNumericAxis().withGrowBy(0.0, 0.0).build()
xAxis.autoRange = AutoRange.Always
yAxis.autoRange = AutoRange.Always
val dataSeries: IXyDataSeries<Int, Int> = sciChartBuilder.newXyDataSeries(Int::class.javaObjectType, Int::class.javaObjectType).build()
for (i in values.indices) {
dataSeries.append(i, values[i])
}
val rSeries = sciChartBuilder.newColumnSeries()
.withStrokeStyle(-0xdcdcdd, 0.4f)
.withDataPointWidth(0.5)
.withDataSeries(dataSeries)
.withPaletteProvider(ColumnsPaletteProvider())
.build()
UpdateSuspender.using(lineChart) {
lineChart.theme = R.style.SciChart_Bright_Spark
Collections.addAll(lineChart.xAxes, xAxis)
Collections.addAll(lineChart.yAxes, yAxis)
Collections.addAll(lineChart.renderableSeries, rSeries)
Collections.addAll(lineChart.chartModifiers, sciChartBuilder.newModifierGroupWithDefaultModifiers().build())
sciChartBuilder.newAnimator(rSeries).withWaveTransformation().withInterpolator(DecelerateInterpolator()).withDuration(3000).withStartDelay(350).start()
}
}
Trying to pass a different set of values to drawColumnChart() method this exception is thrown.
java.lang.UnsupportedOperationException: AxisCollection.getAxisById('DefaultAxisId') returned more than one axis with the ID=DefaultAxisId. Please check you have assigned correct axis Ids when you have multiple axes in SciChart
Thanks for your time :)

This exception is thrown, because each axis in xAxes/yAxes collection should have unique id. Also you should have in mind that in case of multiple axes you also need to assign correct axis id for renderable series. Please take a look on SciChart tutorial that how to add multiple axes into the chart..
So in your case code for creation of axes should look like this:
val xAxis: IAxis = sciChartBuilder.newNumericAxis().withAxisId(uniqueXAxisId).withGrowBy(0.15, 0.15).withLabelProvider(YearsLabelProvider()).build()
val yAxis: IAxis = sciChartBuilder.newNumericAxis().withAxisId(uniqueYAxisId).withGrowBy(0.0, 0.0).build()
and code for creation of renderable series like this:
val rSeries = sciChartBuilder.newColumnSeries()
.withXAxisId(uniqueXAxisId)
.withYAxisId(uniqueYAxisId)
.withStrokeStyle(-0xdcdcdd, 0.4f)
.withDataPointWidth(0.5)
.withDataSeries(dataSeries)
.withPaletteProvider(ColumnsPaletteProvider())
.build()
However if you want to update column values without adding new series, then just create and set xAxis, yAxis and renderable series once outside listener, store data series in private field and in listener update data series with new data instead of recreating it every time:
dataSeries.clear()
for (i in values.indices) {
dataSeries.append(i, values[i])
}

Related

How can I fix this particular "Val cannot be reassigned" error?

I have this code that keeps giving me a "Val cannot be reassigned" error but I can't seem to change the variable to a var instead of val. I simply want to be able to set a string value to my cell reference so I can access the values later like this myStringsArrayList.add(deviceData.cellOne).
Here is my code:
val cells = listOf(
deviceData.cellOne,
deviceData.cellTwo,
deviceData.cellThree,
deviceData.cellFour,
deviceData.cellFive,
deviceData.cellSix,
deviceData.cellSeven,
deviceData.cellEight,
deviceData.cellNine,
deviceData.cellTen,
deviceData.cellEleven,
deviceData.cellTwelve,
deviceData.cellThirteen,
deviceData.cellFourteen
)
for ((i, cell) in cells.withIndex()) {
val value = data[2 + i].toDouble() / 100 + 3.52
val cellNumberString = (i + 1).toString()
val formattedString = "Cell $cellNumberString: %.2fV".format(value)
cell = formattedString // THIS IS WHERE THE PROBLEM IS (cell is a val)
}
Does anyone know how I can get around this and achieve the functionality that I want?
I tried using a listIterator() but it hasn't seemed to work the way that I want it to.
Here is my attempt with the listIterator():
val cells = mutableListOf(
deviceData.cellOne,
deviceData.cellTwo,
deviceData.cellThree,
deviceData.cellFour,
deviceData.cellFive,
deviceData.cellSix,
deviceData.cellSeven,
deviceData.cellEight,
deviceData.cellNine,
deviceData.cellTen,
deviceData.cellEleven,
deviceData.cellTwelve,
deviceData.cellThirteen,
deviceData.cellFourteen)
val iterate = cells.listIterator()
while (iterate.hasNext()) {
var cell = iterate.next()
val value = data[2 + iterate.nextIndex()].toDouble() / 100 + 3.52
val cellNumberString = (iterate.nextIndex() + 1).toString()
val formattedString = "Cell $cellNumberString: %.2fV".format(value)
cell = formattedString
}
You can't make that a var, and there's no reason to anyway!
for ((i, cell) in cells.withIndex()) {
...
cell = formattedString // THIS IS WHERE THE PROBLEM IS (cell is a val)
}
You're creating a for loop there on a collection, with an index, and giving it a block of code to run for each loop. So when the loop runs, you're provided with two parameters - the current item from the collection, and its index in that collection.
These are just internal variables for use in the loop - you can't reassign them, because they're the values being passed in. Even if you could, you'd just be changing the values of those local variables.
What you're probably trying to do is update the cells list, taking the current item in the loop, finding it in cells, and replacing it. You'd have to actually update cells to do that! Change the list itself. You could do that with cells[i] = formattedString - but because you're currently iterating over that cells collection, you shouldn't modify it!
You could copy the source list, but the typical Kotlin way is to create a new list, using map (which transforms values):
cells.mapIndexed { i, cell ->
val value = data[2 + i].toDouble() / 100 + 3.52
val cellNumberString = (i + 1).toString()
// last expression is the return value, i.e. the formatted string
"Cell $cellNumberString: %.2fV".format(value)
}
That will spit out a new (immutable) list where each cell has been mapped to that formatted string version.
You could make cells a var and just reassign it:
cells = cells.mapIndexed { ... }
or just chain the map call when you initialise the val, so that end result is what gets assigned:
val cells = listOf(
...
).mapIndexed { ... }
But you're not actually using cell in that loop anyway, just using the index to generate values. You can create a list like this:
val data = List(14) { i ->
val value = data[2 + i].toDouble() / 100 + 3.52
// you can calculate the index inside the string by using braces
"Cell ${i + 1}: %.2fV".format(value)
}
It all depends whether you need to keep that list of devicedata values around for anything (if so use it to create another list)
You may wanna use Interator this way:
val list = mutableListOf("One", "Two", "Three", "Four")
println(list.joinToString(" "))
val iterator = list.listIterator()
while(iterator.hasNext()) {
val value = iterator.next()
if (value == "Two") {
iterator.set("xxxxxx")
}
}
println(list.joinToString(" "))
Use the 'set' method.

How can I prevent repeating numbers in a random range operator Kotlin?

I have an app in which gives you a certain photo based on an integer being fetched via
class RandomImageLogic(){
fun retrive(): Int{
return (1..9).random()
}
}
However, it is not professional to have a repeated outcome, as I desire a random integer to be fetched each time I call the function in order for the image to be different each time my button is pressed. How can I fetch a new random integer whenever the button calls the function?
The easy way is to pass in the last random number you received and filter it out.
fun retrive(except: Int): Int{
return ((1..9).filter {it != except}).random();
}
In your case, this method might not be called extremely often (only when the user clicks a button).
If this method was to be called more often, filter on an IntRange should be used with care (as proposed in #avalerio's answer).
This iterates the whole range (unnecessarily costing time) and it would create a temporary ArrayList on every call (creating unnecessary garbage and triggering the garbage collector more often than needed).
Here is a sample object NonRepeatingRandom (you can also implement it as a class if you wish). retrieve (which has been expanded with a max parameter and basic sanity checks) recursivly calls itself again if the same number would be generated twice in a row:
object NonRepeatingRandom {
private var previous = -1
fun retrieve(max : Int = 9): Int {
if(max < 0) {
error("Only positive numbers")
}
if(max <= 1) {
// There is nothing random about 0 or 1, do not check against previous, just return
previous = max
return max
}
val rand = (1..9).random()
return if(rand == previous) {
retrieve(max) // recursive call if two subsequent retrieve() calls would return the same number
} else {
previous = rand // remember last random number
rand
}
}
}
fun main(args: Array<String>) {
repeat(1000) {
println(NonRepeatingRandom.retrieve())
}
}
I made a quick-and-dirty performance test, calling my "recursive" method 10 million times and calling the "filter" method 10 million times.
Recursive: 125 ms (10 mio calls)
Filter: 864 ms (10 mio calls)
Pre-fill & random shuffle approach:
class RandomIntIterator(
private val range: IntRange
) : Iterator<Int> {
private lateinit var iterator: Iterator<Int>
init { randomize() }
fun randomize() {
iterator = range.shuffled().iterator()
}
override fun hasNext() = iterator.hasNext()
override fun next() = iterator.next()
}
...
val rnd = RandomIntIterator(1..9)
...
// on button click
if (rnd.hasNext()) {
val num = rnd.next()
// use num
} else {
// renew (if needed)
rnd.randomize()
}
I like using Sequences to generate unending streams of values.
In this case we'll have to write custom code, as checking for repeated values is a stateful operation, and while Sequences has a distinct() stateful filter, it applies to all generated values - we only want it to apply to a limited window.
TL;DR:
class RandomImageLogic(
private val random: Random,
/** The number of sequential values that must be distinct */
noRepeatsLimit: Int = 2
) {
private val sourceValues: List<Int> = (0..9).toList()
private fun nextValue(vararg exclusions: Int): Int =
(sourceValues - exclusions.asList()).random(random)
private val randomInts: Iterator<Int> =
generateSequence({
// the initial value just has one random int
val next = nextValue()
ArrayDeque(listOf(next))
}) { previousValues ->
// generate the next value, excluding previous values
val nextValue = nextValue(*previousValues.toIntArray())
// limit the size of previousValues, if necessary
if (previousValues.size >= noRepeatsLimit)
previousValues.removeLastOrNull()
// add the generated value to the beginning of the deque
previousValues.addFirst(nextValue)
previousValues
}
.map {
// convert the Sequence to a list of ints,
// each element is the first item in the deque
it.first()
}
.iterator()
fun retrieve(): Int {
return randomInts.next()
}
}
Testing
Let's write a test first, to make sure our solution works. Kotest has a specific property based testing subproject, and this will let us cover a wide range of test cases very quickly.
So, I ran through the setup, and started setting up the test case.
Seeding RandomImageLogic
First I modified the RandomImageLogic class so that the random selection could be seeded with a provided Random.
import kotlin.random.Random
class RandomImageLogic(private val random: Random) {
fun retrieve(): Int {
return (1..9).random(random = random)
}
}
This will help us to create a Generator for the RandomImageLogic.
Testing all values
Now we can use Kotest to write a property-based test that will assert "for all sequential values, they are different"
import io.kotest.core.spec.style.FunSpec
import io.kotest.property.arbitrary.arbitrary
import io.kotest.property.forAll
class RandomImageLogicTest: FunSpec({
// This generator will create a new instance of `RandomImageLogic`
// and generate two sequential values.
val sequentialValuesArb = arbitrary { rs ->
val randomImageLogic = RandomImageLogic(rs.random)
val firstValue = randomImageLogic.retrieve()
val secondValue = randomImageLogic.retrieve()
firstValue to secondValue
}
test("expect sequential values are different") {
forAll(sequentialValuesArb) { (firstValue, secondValue) ->
firstValue != secondValue
}
}
})
Of course, the test fails.
Property failed after 4 attempts
Arg 0: (1, 1)
Repeat this test by using seed 1210584330919845105
Caused by org.opentest4j.AssertionFailedError: expected:<true> but was:<false>
So let's fix it!
Generating Sequences
As I said earlier, I really like Sequences. They're perfect for this use-case, where we have an infinite source of values.
To demonstrate how to make a Sequence, let's convert the existing code, and use an Iterator to fetch values.
class RandomImageLogic(private val random: Random) {
private val randomInts =
// generate a sequence using values from this lambda
generateSequence { (1..9).random(random = random) }
// use an iterator to fetch values
.iterator()
fun retrieve(): Int {
return randomInts.next()
}
}
This hasn't solved the problem yet - the Sequence only generates and provides one value at a time and so cannot do any filtering. Fortunately generateSequence() has an variant with nextFunction: (T) -> T?, where we can determine the next value based on the previous value.
If we use this constructor, and do a bit of refactoring to share the source values, and a util method to generate a next value while filtering out previous values...
private val sourceValues: List<Int> = (0..9).toList()
private fun nextValue(vararg exclusions: Int): Int =
(sourceValues - exclusions.asList()).random(random)
private val randomInts: Iterator<Int> =
generateSequence({ nextValue() }) { previousValue ->
nextValue(previousValue)
}
.iterator()
and now if we run the test, it passes!
Test Duration Result
expect sequential values are different 0.077s passed
Improvement: more than two distinct sequential values
What happens if you don't just want two sequential values to be distinct, but 3? Or even more? Let's make the 'no-repeated-values' limit configurable, and I think this will demonstrate why Sequences are a good solution.
class RandomImageLogic(
private val random: Random,
/** The number of sequential values that must be distinct */
noRepeatsLimit: Int = 2
) {
// ...
}
Testing
Once again, let's write a test to make sure things work as expected.
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.collections.shouldNotContainDuplicates
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.checkAll
import kotlin.random.Random
class RandomImageLogicTest : FunSpec({
test("expect arbitrary sequential values are different") {
checkAll(Arb.int(), Arb.int(1..10)) { seed, noRepeatsLimit->
val randomImageLogic = RandomImageLogic(Random(seed), noRepeatsLimit)
val result = List(noRepeatsLimit) { randomImageLogic.retrieve() }
withClue("Result: $result") {
result shouldHaveSize noRepeatsLimit
result.shouldNotContainDuplicates()
}
}
}
})
And of course the test fails.
Property test failed for inputs
0) -459964888
1) 5
Caused by java.lang.AssertionError: Result: [3, 8, 0, 2, 8]
Collection should not contain duplicates
There's lots of options to make a Sequence stateful - again let's just look at one.
Sequence of values
Instead of a sequence of individual values, we can have a sequence where each element is a list of not only the current value, but also previously seen values.
Let's use ArrayDeque to store these values, because it's easy to add and remove values from the start and end.
Again, we use the same generateSequence constructor with a seedFunction and nextFunction - except this time each element is deque which stores all values, and in nextFunction we add new values to the start of the deque, trimming it if it's larger than the window size noRepeatsLimit
private val randomInts: Iterator<Int> =
generateSequence({
// the initial value just has one random int
val next = nextValue()
ArrayDeque(listOf(next))
}) { previousValues ->
// generate the next value, excluding previous values
val nextValue = nextValue(*previousValues.toIntArray())
// limit the size of previousValues, if necessary
if (previousValues.size >= noRepeatsLimit)
previousValues.removeLastOrNull()
// add the generated value to the beginning of the deque
previousValues.addFirst(nextValue)
previousValues
}
.map {
// convert the Sequence to a list of ints,
// each element is the first item in the deque
it.first()
}
.iterator()
And yup, the test passes!
Test Duration Result
expect arbitrary sequential values are different 0.210s passed
Storing state
It's important to think about how state will be stored. 'State' is required by RandomImageLogic to know whether a generated value is distinct.
In the Sequence implementation, it's stored internally, and so is specifically associated with an instance of RandomImageLogic. Maybe your application only has one instance of RandomImageLogic at any one time, in which case the state will always be up to date and will be shared between all invocations.
But what happens if there's more than one instance of RandomImageLogic? Or if there's multi-threading? Or if the RandomImageLogic instance is recreated?
The answers to these depend on the implementation and situation. From your question I suspect that it's not critically important that images never repeat, but I bring this up because it is important to think about.

How to add decimal values to entries for chart using MpAndroidChart Library?

I'm trying to create a line chart in Android using MPAndroidChart Library and as entries I have values like 1200.10, 1300.70 and so on, but on my chart the values are rounded (1200, 1301), and I want to display the original values. How can I do that? I tried different solutions but couldn't solve the problem yet. I'm using the Kotlin language. Thanks!
for (item in reversedCashList) {
if (i <= daysNmb) {
var cashValue: String = transformDataForChart(item.value!!)
dataValsEntries.add(Entry(i, cashValue.toFloat()))
i++
}
}
Also, I'm using this formatter Class to format my values because the initial format is like 120.200,10 and I changed them to 120200.10 but this values is displayed as 120200. My Formatter Class:
private fun transformDataForChart(totalValue: String): String {
return if (totalValue.contains(".")) {
val test = totalValue.replace(".", "")
test.replace(",", ".")
} else {
totalValue.replace(",", ".")
}
}
You can try with BigDecimal, something like BigDecimal.valueOf(X).setScale(decimalPlace(usually 2), BigDecimal.ROUND_HALF_UP).floatValue()
The idea is that float cannot hold so many values as the Double, I've encountered this issue as some point as well, and I had to change everything to Double just to make it more easier to maintain... Therefor I don't think is a straight-forward method to keep everything you need in the float format.

removeAt removes elements in passed ArrayList too

I've never saw this
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun eraseOldLocations() {
val a: ArrayList<Ub> =myUbs
val minTime: Long = (now- tenDaysago)
val z = misUbs.size
for(i in 0 until z){
if( myUbs[i].time <minTime) {
a.removeAt(i)
}
}
myUbs = a
}
each time the condition is fulfilled, the element in the position i of a is removed ... BUT also from the myUbs !!! myUbs is a public object, so as the loop continues, appears an error because the variable i exceeds the size of myUbs ...
The function eraseOldLocations() must delete elements with date older than ten days ago. Why is this happening?
Do a shallow copy of myUbs, so that references of list will differ and changes won't reflect to each other:
val a: MutableList<Ub> = myUbs.toMutableList()
MutableList is essentially the same, but if you want ArrayList explicitly (althrough not recommended in Kotlin) you can call ArrayList constructor to delegate to the Collection specified. That essentially does the shallow copy as well:
val a: ArrayList<Ub> = ArrayList(myUbs)

How to add a color overlay to an animation in Lottie?

I'm using Lottie for Android to add some animations in an app. In this app the primary and accent color can be chosen via the settings. I'm using an animation with a transparent background. To make the animation fit the chosen colors I'd like to add a color overlay to the animation, this way I can have one animation file but I can set the color programmatically.
Does anyone have an idea how I can manipulate the animation by adding a color overlay?
To apply a color filter you'll need three things as of now:
KeyPath (name of content you wish to edit)
LottieProperty (name of property you wish to edit)
LottieValueCallback (callback called for every animation re-render)
The layer name can be found in the JSON of the animation by the tag 'nm'.
Add a full color overlay:
LottieAnimationView animationView = findViewById(R.id.animation_view);
animationView.addValueCallback(
new KeyPath("**"),
LottieProperty.COLOR_FILTER,
new SimpleLottieValueCallback<ColorFilter>() {
#Override
public ColorFilter getValue(LottieFrameInfo<ColorFilter> frameInfo) {
return new PorterDuffColorFilter(Color.GREEN, PorterDuff.Mode.SRC_ATOP);
}
}
);
Add a single layer color overlay (layer called "checkmark"):
LottieAnimationView animationView = findViewById(R.id.animation_view);
animationView.addValueCallback(
new KeyPath("checkmark", "**"),
LottieProperty.COLOR_FILTER,
new SimpleLottieValueCallback<ColorFilter>() {
#Override
public ColorFilter getValue(LottieFrameInfo<ColorFilter> frameInfo) {
return new PorterDuffColorFilter(Color.CYAN, PorterDuff.Mode.SRC_ATOP);
}
}
);
Remove any color overlays:
LottieAnimationView animationView = findViewById(R.id.animation_view);
animationView.addValueCallback(new KeyPath("**"), LottieProperty.COLOR_FILTER,
new SimpleLottieValueCallback<ColorFilter>() {
#Override
public ColorFilter getValue(LottieFrameInfo<ColorFilter> frameInfo) {
return null;
}
}
);
You can read all about it in the official documentation.
You can also check out this sample repository
Here's a visual on the results of the code snippets:
Found in sources of lottie, based on main answer (thanks #SolveSoul ).
Java
First, get your color, for example:
int yourColor = ContextCompat.getColor(getContext(),R.color.colorPrimary);
Then set color filter like this:
SimpleColorFilter filter = new SimpleColorFilter(yourColor);
KeyPath keyPath = new KeyPath("**");
LottieValueCallback<ColorFilter> callback = new LottieValueCallback<ColorFilter>(filter);
animationView.addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback);
Kotlin
First, get your color, for example:
val yourColor = ContextCompat.getColor(context, R.color.colorPrimary)
Then set color filter like this:
val filter = SimpleColorFilter(yourColor)
val keyPath = KeyPath("**")
val callback: LottieValueCallback<ColorFilter> = LottieValueCallback(filter)
animationView.addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback)
Kotlin extension
fun LottieAnimationView.changeLayersColor(
#ColorRes colorRes: Int
) {
val color = ContextCompat.getColor(context, colorRes)
val filter = SimpleColorFilter(color)
val keyPath = KeyPath("**")
val callback: LottieValueCallback<ColorFilter> = LottieValueCallback(filter)
addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback)
}
Since you're passing a JSONObject containing all of the drawing data to Lottie when setting the animation, you could just replace some of the color values with your desired ones before you set it.
If you look for the color key c you'll probably find something like
...,"c":{"k":[1,0.7,0,1]},"fillEnabled":true,...
where changing those float values in that JSONArray would change the colors in the animation.
Granted, I'm not saying it will be too trivial to find/replace the correct values, but this should at least point you in that direction.
As a side note: once you find it, you could set the value in your asset to some kind of nice placeholder like "k":[ BG_COLOR_REPLACEMENT_1 ] and then when loading the asset, just run .replace("BG_COLOR_REPLACEMENT_1", "1,0.7,1,1"); on your String before creating the JSONObject and passing it to Lottie.
Compose + Lottie
#Composable
fun LottieAnimation(isPlaying: Boolean = false) {
val composition by rememberLottieComposition(LottieCompositionSpec.Asset(LOTTIE_FILE_NAME))
val progress by animateLottieCompositionAsState(
composition,
iterations = LottieConstants.IterateForever,
isPlaying = isPlaying,
)
val dynamicProperties = rememberLottieDynamicProperties(
rememberLottieDynamicProperty(
property = LottieProperty.COLOR_FILTER,
value = SimpleColorFilter(MaterialTheme.colors.primary.toArgb()),
keyPath = arrayOf("**")
),
)
LottieAnimation(
composition = composition,
progress = { progress },
dynamicProperties = dynamicProperties,
)
}
I saw Guardanis answer and elaborated a safe way to find the colour inside the JSON that contains the Lottie animation:
Search for - "c":{"a" - and you will find fragments like this for each layer of you image: {"ty":"fl","c":{"a":0,"k":[0.4,0.4,0.4,0.4]}
In the fragment you will notice "c" means COLOUR, "a" means ALPHA and "k" is the CMYK of the colour of the layer. Just change it for the one that you want.
In Kotlin(v1.4.32) To set up full animation in all layers just do this :
YOURS_LottieAnimationView.addValueCallback(
KeyPath("**"),
LottieProperty.COLOR_FILTER,
{ PorterDuffColorFilter(Color.parseColor("#b70101"), PorterDuff.Mode.SRC_ATOP) }
)
If your JSON has has an sc: field then you should be able to set the Hex color directly
like:
"sc": "#6664e7"

Categories

Resources