Search code examples
androidmpandroidchart

MPAndroidChart - Line chart is no longer filling entire view after changing the number of data points


I'm having an issue in my app where adding a large number of data points, clearing them, adding a smaller number of datapoints causes the LineChart to not completely reach the end of the view.

I recreated this issue with a simple LineChart.

enter image description here

In the linechart, I first add 365 data points by clicking the Add 365 button and it successfully loads all the points into the LineChart view with a width of 300dp.

Next, I click the Add 25 button to load the 25 data points into the LineChart, but as you can see the line no longer reaches the end of the view for some reason.

I thought this might be because the chart is zoomed all the way out, so I added lineChart.fitScreen() but it doesn't do anything.

So I'm no longer sure what the problem is. Does anyone know what I'm doing incorrectly?

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.linechart)
    LineChart lineChart;

    private final List<Entry> entries = new ArrayList<>();
    private LineDataSet dataSet;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        dataSet = new LineDataSet(entries, null);
        Drawable drawable = ContextCompat.getDrawable(this, R.drawable.gradient_purple);
        dataSet.setFillDrawable(drawable);
        dataSet.setLineWidth(1.5f);
        dataSet.setDrawHorizontalHighlightIndicator(false);
        dataSet.setHighLightColor(Color.WHITE);
        dataSet.setHighlightLineWidth(1.5f);
        dataSet.setColor(ContextCompat.getColor(this, R.color.teal_700));
        dataSet.setDrawFilled(true);
        dataSet.setDrawCircles(false);
        dataSet.setDrawValues(false);

        lineChart.setMinOffset(0f);
        lineChart.getAxisLeft().setEnabled(false);
        lineChart.getAxisRight().setEnabled(false);
        lineChart.getXAxis().setEnabled(false);
        lineChart.setNoDataText(null);
        lineChart.setScaleEnabled(false);
        lineChart.getDescription().setEnabled(false);
        lineChart.getLegend().setEnabled(false);
    }

    @OnClick(R.id.btn_add_25)
    void onAdd25() {
        dataSet.clear();
        for (int i = 0; i < 25; i++) {
            Entry entry = new Entry(i, i);
            dataSet.addEntry(entry);
        }
        LineData lineData = new LineData(dataSet);
        lineChart.setData(lineData);

        //lineChart.fitScreen(); Adding fitScreen doesn't do anything
        lineChart.invalidate();


    }

    @OnClick(R.id.btn_add_365)
    void onAdd365() {
        dataSet.clear();
        for (int i = 0; i < 365; i++) {
            Entry entry = new Entry(i, i);
            dataSet.addEntry(entry);
        }
        LineData lineData = new LineData(dataSet);
        lineChart.setData(lineData);

        //lineChart.fitScreen(); Adding fitScreen doesn't do anything
        lineChart.invalidate();
    }
}

Layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.MainActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/purple_500"
        android:elevation="4dp"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:fontFamily="sans-serif-medium"
            android:text="Test App"
            android:textColor="@color/white"
            android:textSize="20sp" />
    </androidx.appcompat.widget.Toolbar>

    <com.github.mikephil.charting.charts.LineChart
        android:id="@+id/linechart"
        android:layout_width="250dp"
        android:layout_height="200dp"
        android:clipToPadding="false"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

    <Button
        android:id="@+id/btn_add_25"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="300dp"
        android:text="Add 25"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/btn_add_365"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="200dp"
        android:text="Add 365"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Using version: implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'


Solution

  • TL;DR - Call dataSet.calcMinMax() after clearing and updating its data with a smaller dataset.

    For whatever reason, the dataset is not clearing its min/max X & Y values when you call dataSet.clear();, so when you had a dataset with X max of 364, adding a new dataset that only goes to 24 still keeps that old maximum, and the chart is drawn to that bound. The easy fix is to call calcMinMax() on it manually yourself after filling it with smaller data set (or always as a best practice), like this:

    dataSet.clear();
    for (int i = 0; i < 25; i++) {
        Entry entry = new Entry(i, i);
        dataSet.addEntry(entry);
    }
    
    // if you print out dataSet.getXMax() here, it shows 364 if
    //  you plotted the 365 point data set already
    
    dataSet.calcMinMax();
    
    // dataSet.getXMax() here will now be correct
    

    Another approach, which is more robust to the dataset saving state like this, would be to generate a new dataset for each call. You could define a helper function to apply the formatting that takes a list of entries, like this:

    private LineDataSet createDataSet(List<Entry> entries) {
        LineDataSet dataSet = new LineDataSet(entries, null);
        Drawable drawable = ContextCompat.getDrawable(this, R.drawable.gradient_purple);
        dataSet.setFillDrawable(drawable);
        dataSet.setLineWidth(1.5f);
        dataSet.setDrawHorizontalHighlightIndicator(false);
        dataSet.setHighLightColor(Color.WHITE);
        dataSet.setHighlightLineWidth(1.5f);
        dataSet.setColor(ContextCompat.getColor(this, R.color.teal_700));
        dataSet.setDrawFilled(true);
        dataSet.setDrawCircles(false);
        dataSet.setDrawValues(false);
        return dataSet;
    }
    

    then remove the dataSet and entries class members and calls to them in onCreate and instead do something like this on onAdd25 and onAdd365

    void onAdd365() {
        ArrayList<Entry> entries = new ArrayList<>();
        for (int i = 0; i < 365; i++) {
            Entry entry = new Entry(i, i);
            entries.add(entry);
        }
        LineDataSet dataSet = createDataSet(entries);
        LineData lineData = new LineData(dataSet);
        lineChart.setData(lineData);
        lineChart.invalidate();
    }