MPAndroidChart BarChart not respecting bar width on redraw - android

I'm using a BarChart in an app but sometimes when I re-draw it the bars width is not respected and I get inconsistent results (even though the amount of values in the X axis is always 100).
This is how I want it to look always:
but sometimes it looks like this:
Does anyone know why this happens and how can I force it to always look the same?
I think it has something to do with the range of values in the X axis, as the chart looks good when the range goes from 0 to 50 or similar, but it looks bad when the range is smaller.
I'm already setting a bar width value like this but it doesn't help:
barChartView?.data?.barWidth = 0.3f
This is the full chart configuration I'm using (this method runs only once when the Fragment is created):
private fun setUpChartView() {
barChartView?.setTouchEnabled(false)
val xAxis = barChartView?.xAxis
val yAxis = barChartView?.axisLeft
// Hide all parts of the chart that we don't want to show
barChartView?.legend?.isEnabled = false
barChartView?.description?.isEnabled = false
barChartView?.setDrawGridBackground(false)
xAxis?.setDrawGridLines(false)
xAxis?.setDrawAxisLine(false)
yAxis?.setDrawAxisLine(false)
barChartView?.axisRight?.isEnabled = false
// Show the Y axis grid lines as dashed
yAxis?.enableGridDashedLine(5f, 10f, 0f)
val barChartTextColor = getColor(R.color.chart_text)
val barChartTextSize = 12f
val barChartLabelsTypeface = Typeface.SANS_SERIF
// Show the X axis labels at the bottom and set the proper style
xAxis?.position = XAxis.XAxisPosition.BOTTOM
xAxis?.textColor = barChartTextColor
xAxis?.textSize = barChartTextSize
xAxis?.typeface = barChartLabelsTypeface
// Set the proper style for the Y axis labels
yAxis?.textSize = barChartTextSize
yAxis?.textColor = barChartTextColor
yAxis?.typeface = barChartLabelsTypeface
barChartView?.setNoDataText("")
val viewPortHandler = barChartView?.viewPortHandler
val axisTransformer = barChartView?.getTransformer(LEFT)
// Set a custom axises so that we can control the drawing of the labels and grid lines
barChartView?.setXAxisRenderer(CustomEntriesXAxisRenderer(viewPortHandler = viewPortHandler,
xAxis = xAxis,
transformer = axisTransformer))
barChartView?.rendererLeftYAxis = CustomEntriesYAxisRenderer(viewPortHandler = viewPortHandler,
yAxis = yAxis,
transformer = axisTransformer)
}
This is the code I use to draw the chart when I get new data:
private fun drawBarChart() {
// The data list has always 100 items
val entries = data.map { BarEntry(it.x, it.y) }
// Prepare the chart data set by adding the entries and configuring its style (colors, etc.)
val dataSet = BarDataSet(entries, null)
dataSet.setDrawValues(false)
dataSet.colors = colorsList
// Load data to chart
if (barChartView?.data == null) {
// If there is no data set yet, create a new data object and add it to the chart (this happens the first
// time we draw the chart after the Fragment was created, or after an empty data list was returned)
barChartView?.data = BarData(dataSet)
} else {
// If there is already data in the chart, remove the existing data set and add the new one
barChartView.data?.removeDataSet(0)
barChartView.data?.addDataSet(dataSet)
barChartView.notifyDataSetChanged()
}
// Set a custom width for the bars or the chart will draw them too wide
barChartView?.data?.barWidth = 0.3f
val xAxis = barChartView?.xAxis
val yAxis = barChartView?.axisLeft
// Set the min and max values for the Y axis or the chart lib will calculate them and add more values than
// we need
yAxis?.axisMinimum = 0f
yAxis?.axisMaximum = entries.last().y
// Set the entries that need to be drawn in the X axis, so the chart doesn't calculate them automatically
(barChartView?.rendererXAxis as CustomEntriesXAxisRenderer).entries = xAxisEntries
// Set the entries that need to be drawn in the Y axis, so the chart doesn't calculate them automatically
(barChartView?.rendererLeftYAxis as CustomEntriesYAxisRenderer).entries = yAxisEntries
// Add the label count to avoid the chart from automatically setting a range of labels instead of the ones we need,
// which prevents the axis value formatter below to set the correct labels
xAxis?.setLabelCount(xAxisEntries.size, true)
yAxis?.setLabelCount(yAxisEntries.size, true)
// Use a custom value formatter that sets only the needed labels on the axises
xAxis?.setValueFormatter { value, _ ->
// TODO
}
yAxis?.setValueFormatter { value, _ ->
// TODO
}
// Draw chart
barChartView?.invalidate()
}
And this is the implementation of the custom axis renderers I created:
class CustomEntriesXAxisRenderer(viewPortHandler: ViewPortHandler?, xAxis: XAxis?, transformer: Transformer?)
: XAxisRenderer(viewPortHandler, xAxis, transformer) {
/**
* Entries used to draw the grid lines and labels on the X axis.
*/
var entries: List<Float>? = null
override fun computeSize() {
entries?.forEachIndexed { i, value ->
mAxis.mEntries[i] = value
}
super.computeSize()
}
}
class CustomEntriesYAxisRenderer(viewPortHandler: ViewPortHandler?, yAxis: YAxis?, transformer: Transformer?)
: YAxisRenderer(viewPortHandler, yAxis, transformer) {
/**
* Entries used to draw the grid lines and labels on the Y axis.
*/
var entries: List<Float>? = null
override fun computeAxisValues(min: Float, max: Float) {
super.computeAxisValues(min, max)
entries?.forEachIndexed { i, value ->
mAxis.mEntries[i] = value
}
}
}

One of the MPAndroidChart devs said this is indeed a bug in the library and it's now in their to-do list. If anyone is having this issue you can follow the progress here.
In the meantime, after playing around with this for a while I found a (quite ugly) hack that prevents the chart from looking too bad:
barChartView?.data?.barWidth = when (entries.last().x) {
in 0..30 -> 0.1f
in 30..60 -> 0.2f
else -> 0.3f
}
You can adjust the values to what looks good for your data entries, but basically by setting a different bar width depending on the range of X values in the data set you can at least make this a bit better until the bug is fixed.

Related

How to animate elements of a dynamic list in Jetpack Compose?

How can I transition elements of a list to the new list (possibly different size) with animation?
I have a pie chart and when its slices (fractions) change, I want to animate the previous fractions to the new fractions. The thing is, number of slices can be different each time.
If number of new slices is less than the current ones, the current extra slices should animate from their current fraction to 0.
If number of new slices is greater than the current ones, new extra slices should animate from 0 to their fractions.
#Composable fun PieChartCompose(slices: List<Float>) {
val transitionData = updateTransitionData(slices)
val fractions = transitionData.fractions
// Draw the pie with Canvas using the fractions
}
I have currently implemented this with a list of constant size (10, so slices cannot be more than 10)
(note that the initial animation for chart appearance can be different from subsequent animations):
data class TransitionData(val slices: List<State<Float>>)
enum class ChartState { INITIALIZED, CHANGED }
#Composable fun updateTransitionData(
targetFractions: List<Float>
): TransitionData {
val mutableState = remember { MutableTransitionState(ChartState.INITIALIZED) }
mutableState.targetState = ChartState.CHANGED
val transition = updateTransition(mutableState, label = "main-animation")
val fractions = listOf(
transition.animateFloat(label = "fraction-0-animation") {
if (it == ChartState.INITIALIZED) 0f
else targetSlices.getOrNull(0)?.fraction ?: 0f
},
// ...
transition.animateFloat(label = "fraction-10-animation") {
if (it == ChartState.INITIALIZED) 0f
else targetSlices.getOrNull(10)?.fraction ?: 0f
}
)
return remember(transition) { TransitionData(fractions) }
}
Below is an example chart that initially has two slices and then animates to one slice
(the first slice animates to the single new fraction and the second slice animates to 0—
they are a little inconsistent probably because of interpolations and animation specs):
var slices by mutableStateOf(listOf(0.3f, 0.7f))
PieChartCompose(slices)
slices = listOf(1f)
You can try having a dynamic amount of animateFloat.
Since we want to animate fractions that disappeared, we need to know the old fractions list (in case it's bigger than new one).
That's why I've changed the transition state to operate on fractions list. We can access the "old" state and find the "max" size (comparing old and new fractions list sizes).
The initial state is an empty list, so initially there will be animation from zero for the first fractions.
In animateFloat we try to take the fraction from the target state and if the fraction at that position is no longer there - then make it zero, so it will disappear.
I've also added remember(values) { } around updating values in animatedFractions which is not needed to work, but it's there rather for optimisation. If the count of values would not change then all existing objects would be reused and values list should be the same - then we do not need to update animatedFractions with new State objects.
From updateTransitionData a stable object is returned, with stable list inside. We only modify objects inside of that list. Because it's a SnapshotStateList it will take care of refreshing all Composables that iterate over it.
#Composable
fun updateTransitionData(
targetFractions: List<Float>
): TransitionData {
val mutableState = remember { MutableTransitionState(emptyList<Float>()) }
mutableState.targetState = targetFractions
val transition = updateTransition(mutableState, label = "main-animation")
val maxFractionsSize = max(transition.currentState.size, targetFractions.size)
val values = (0 until maxFractionsSize).map { index ->
transition.animateFloat(label = "fraction-$index-animation") { state ->
state.getOrNull(index) ?: 0f
}
}
val animatedFractions = remember(transition) { SnapshotStateList<State<Float>>() }
remember(values) {
animatedFractions.clear()
animatedFractions.addAll(values)
}
return remember(transition) { TransitionData(animatedFractions) }
}
Here is a quick "linear" demo, with slowed down animations, going through 4 different fractions lists:

Is there a way to pass list of date from realm to graph chart axis?

I have been trying to add the date to my X-axis in my chart but changing the value of time into millisecond and then parsing to float type affects its precision eg: 43578934 it will be rounded to 4357999. Is there a way to solve this problem?
val list = ArrayList<Entry>()
var count = 0f
for (item: Data in listData) {
if (item.createdAt != null && item.pulse != null) {
val milliSecond = item.createdAt!!.time
val x = ((((milliSecond/1000)/60)/60)/24).toString()
Log.d("TAG", "getGraphList: $x")
val y = item.pulse!!.toFloat()
val entry = Entry(x.toFloat(), y)
list.add(entry)
}
}
return list
I figured out a way through which we can pass float value, and we don't have to worry about auto rounding of the value and we can get the precise value of the current date.
In order to do that we need to create our own date class where we can calculate the date using the method shown.

How to set icons in `LineChart` to selected position in MPAndroidChart?

I have a list of Entry points which I have plotted on LineChart. Now I want to set icon to a particular point. How to set or draw icons in LineChart to my selected position in MPAndroidChart?
You can add icon to your selected point using 3 parameters Entry constructor where third parameter is a Drawable:
public Entry(float x, float y, android.graphics.drawable.Drawable icon)
Parameters:
x - the x value
y - the y value (the actual value of the entry)
icon - icon image
Ref: MPAndroidChart v3.0.3 JavaDoc
Example:
ArrayList<Entry> values = new ArrayList<Entry>();
values.add(new Entry (x, y, ContextCompat.getDrawable(getApplicationContext(),R.drawable.star)));
You can mark a single point with icon from entire LineDataSet by adding Drawable in Entry constructor.
I know this is an old question but This is my solution in case anyone still needs it.
First, you need to call setDrawIcons(true) on your data set. Then You need to define your entry as a member inside your class to be able to reset it later.
private var selectedEntry: Entry? = null
And Inside onValueSelected you need to set selectedEntry's icon as null if it exists and set the icon for the new selectedEntry point.
override fun onValueSelected(e: Entry?, h: Highlight?) {
if (selectedEntry != null) {
selectedEntry?.icon = null
}
for (set in chart.data.dataSets) {
selectedEntry = set.getEntryForIndex(set.getEntryIndex(e))
selectedEntry?.icon = ContextCompat.getDrawable(this,R.drawable.star)
}
chart.invalidate()
}

How to set color for values in MPAndroidChart?

I am using MPAndroidChart.
Searched through the documentation but couldn't find anything that implemented correctly. I have a graph at the moment but I want to change the color of the line if it goes over a certain amount. Graph Example In the example I have linked, it shows a line drawn through the values from 10. I would like this line (the one going through the chart) and the color of the line in the chart to change color over 10. Is this possible? Using MPAndroidChart. I have one dataset at the moment.
Thanks in advance.
Yes. You just need a simple logic.
List<Integer> colors = ...;
List<Entry> entries = ...;
for(...) {
entries.add(...);
if(entries.get(i).getVal() > 10)
colors.add(customcolor);
else
colos.add(othercolor);
}
With LineDataSet.setColors() you can add a list of colors. Each color entry is for one data entry.
'
The trick is to calculate intermediate values for crossing the border.
Everytime I add a data entry, i call this method
private fun addDiffValue(newEntry : Entry){
val last = recordedValues[2].last()
val limit = 50f
if(last.y < limit && newEntry.y > limit ){
val gradient = (newEntry.y - last.y) / (newEntry.x - last.x)
val x_border = last.x + ((limit - last.y) / gradient)
recordedValues[2].add(Entry(x_border, limit))
diffColors.add(Color.LTGRAY)
diffColors.add(Color.RED)
}
// Vorher größer, jetzt kleiner
else if(last.y > limit && newEntry.y < limit) {
val gradient = (newEntry.y - last.y) / (newEntry.x - last.x)
val x_border = last.x + ((limit - last.y) / gradient)
recordedValues[2].add(Entry(x_border, limit))
diffColors.add(Color.RED)
diffColors.add(Color.LTGRAY)
}else if(last.y > limit ){
diffColors.add(Color.RED)
} else {
diffColors.add(Color.LTGRAY)
}
recordedValues[2].add(newEntry)
}
It is important to say, that I start with recordedValues[2].add(Entry(0f,0f)), otherwise last() would throw an error.
I create the LineDataSet and add the colors:
val dataSet3 = LineDataSet(recordedValues[2], "My Label")
dataSet3.setColors(diffColors)
As you can see in this screenshot, all values above 50 are red.

MPChart BarChart: draw x axis line on zero value

I am using MPChart for displaying a Barchart. My values are positive and negative. I want to show xAxis as used in Math of traditional way. Using this code my chart display vertical lines (only zero wanted)
mXAxis = mChart.getXAxis();
mXAxis.setDrawGridLines(false);
mXAxis.setEnabled(false);
mYAxis = mChart.getAxisLeft();
mYAxis.setDrawAxisLine(false);
mYAxis.setDrawGridLines(true);
mYAxis.setStartAtZero(false);
mYAxisRight = mChart.getAxisRight();
mYAxisRight.setEnabled(false);
mYAxisRight.setDrawGridLines(false);
Please, provide some sample for removing all horizontal lines but zero
EDIT:
Even when yAxis.setLabelCount(1) (1 because zero value needed to shown) the impl looks like:
public void setLabelCount(int yCount) {
if(yCount > 25) {
yCount = 25;
}
if(yCount < 2) {
yCount = 2;
}
this.mLabelCount = yCount;
}
So, is it recommended to override this implementation?
I have got solution with labels at bottom
Use this in your code
mXAxis = mChart.getXAxis();
mXAxis.setDrawGridLines(false);
mXAxis.setEnabled(true);
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);

Categories

Resources