Search code examples
androidindexoutofboundsexceptionmpandroidchart

Update XAxis values in MPAndroidChart


I am developing a budgeting application, and in this case, I have a Statistics class where the values of each group of expenses is collected and populated in bars using MPAndroidChart. The idea is that when we select a different month in the spinner the values displayed in the chart are updated accordingly. This is working correctly when the number of categories of expenses of the current month is the same than categories in the new month selected. On the other hand, if the number of categories of the current month is different than the number of categories in the selected month I am getting an ArrayIndexOutOfBoundsException.

I am already creating a new object of this class each time a different month is selected in the spinner, so my question is, how can I update the mValues array in myXAxisValueFormatter to avoid having this Exception?

Statistics.java

package com.robin.xbudget;

import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;

import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.formatter.IndexAxisValueFormatter;
import com.github.mikephil.charting.utils.ColorTemplate;

import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Statistics extends Fragment {
    private final String TAG = this.getClass().getSimpleName();

    StatisticsListener callback;

    private TextView mNumberIncomes;
    private TextView mNumberExpenses;
    private TextView mNumberDaysPassed;
    private TextView mNumberDaysRemaining;
    private LocalDate mLocalDateFirst;
    private LocalDate mLocalDateLast;
    private Spinner monthSpinner;
    private BarChart mBarChart;
    private BarDataSet mBarDataSet;
    private BarData mData;
    ArrayList<BarEntry> barEntries;


    @RequiresApi(api = Build.VERSION_CODES.O)
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.activity_statistics, container, false);

        mNumberIncomes = (TextView) view.findViewById(R.id.number_incomes);
        mNumberExpenses = (TextView) view.findViewById(R.id.number_expenses);
        mNumberDaysPassed = (TextView) view.findViewById(R.id.days_passed_number);
        mNumberDaysRemaining = (TextView) view.findViewById(R.id.days_remaining_number);
        mLocalDateFirst = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth());
        mLocalDateLast = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());

        mNumberIncomes.setText(String.valueOf(callback.getTotalIncomes()));
        mNumberExpenses.setText(String.valueOf(callback.getTotalExpenses()));

        mNumberDaysPassed.setText(String.valueOf(Period.between(mLocalDateFirst, LocalDate.now()).getDays()));
        mNumberDaysRemaining.setText(String.valueOf(Period.between(LocalDate.now(), mLocalDateLast).getDays()));

        monthSpinner = (Spinner) view.findViewById(R.id.spinner_month_stats);
        monthSpinner.setAdapter(callback.getArrayAdapter());

        monthSpinner.setSelection(callback.getSpinnerPosition());

        monthSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                callback.setPeriodSelected((String) monthSpinner.getAdapter().getItem(position));

                dataFiller();
                mBarChart.invalidate();

                Log.d(TAG, "onItemSelected called");
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
            }
        });

        mBarChart = (BarChart) view.findViewById(R.id.bar_chart);
        dataFiller();

        return view;
    }

    public void dataFiller(){

        mBarChart.setDrawBarShadow(false);
        mBarChart.setDrawValueAboveBar(true);
        mBarChart.setMaxVisibleValueCount(50);
        mBarChart.setPinchZoom(false);
        mBarChart.setDrawGridBackground(false);

        barEntries = new ArrayList<>();

        int x = 0;

            for (String s : callback.getExpensesDataGroup()) {
            float value = 0;
            for (Transaction t : callback.getExpensesDataChild().get(s)) {
                value += t.getQuantity();
            }
            barEntries.add(new BarEntry((float) ++x, value));
        }

        //This is the graphic
        mBarDataSet = new BarDataSet(barEntries, "Expenses");
        mBarDataSet.setColors(ColorTemplate.COLORFUL_COLORS);
        mBarDataSet.notifyDataSetChanged();
        mData = new BarData(mBarDataSet);
        mData.setBarWidth(0.9f);

        mBarChart.setData(mData);

        String[] expenses = new String[callback.getExpensesDataGroup().size() + 1];
        expenses[0] = "Dummy";
        for (int i = 1; i <= callback.getExpensesDataGroup().size(); i++) {
            expenses[i] = callback.getExpensesDataGroup().get(i - 1);
        }

        Log.d(TAG, "Prior to mValues before: " + expenses.length);

        mBarChart.notifyDataSetChanged();
        XAxis xAxis = mBarChart.getXAxis();
        xAxis.setValueFormatter(new myXAxisValueFormatter(expenses));
        xAxis.setPosition(XAxis.XAxisPosition.TOP);
        xAxis.setGranularity(1f);
        //xAxis.setCenterAxisLabels(true);
        //xAxis.setAxisMinimum(1);

    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try{
            callback = (StatisticsListener)context;
        }catch(ClassCastException cce){
            throw new ClassCastException("Class must implement StatisticListener");
        }
    }


    class myXAxisValueFormatter extends IndexAxisValueFormatter {

        private String[] mValues;
        public myXAxisValueFormatter(String[]values) {
            this.mValues = values;
        }

        @Override
        public String getFormattedValue(float value) {
            return mValues[(int)value];
        }
    }

    interface StatisticsListener{

        double getTotalIncomes();
        double getTotalExpenses();

        Map<String, List<Transaction>> getExpensesDataChild();
        List<String> getExpensesDataGroup();
        ArrayAdapter getArrayAdapter();

        void setPeriodSelected(String periodSelected);
        int getSpinnerPosition();
    }
}

Logcat output

2020-09-09 08:26:11.208 30608-30608/com.robin.xbudget D/Statistics: Prior to mValues before: 3
2020-09-09 08:26:11.373 30608-30608/com.robin.xbudget D/Statistics: Prior to mValues before: 3
2020-09-09 08:26:11.377 30608-30608/com.robin.xbudget D/Statistics: onItemSelected called
2020-09-09 08:26:13.002 30608-30608/com.robin.xbudget D/Statistics: Prior to mValues before: 2
2020-09-09 08:26:13.013 30608-30608/com.robin.xbudget E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.robin.xbudget, PID: 30608
    java.lang.ArrayIndexOutOfBoundsException: length=2; index=2
        at com.robin.xbudget.Statistics$myXAxisValueFormatter.getFormattedValue(Statistics.java:175)
        at com.github.mikephil.charting.formatter.ValueFormatter.getAxisLabel(ValueFormatter.java:62)
        at com.github.mikephil.charting.components.AxisBase.getFormattedLabel(AxisBase.java:488)
        at com.github.mikephil.charting.components.AxisBase.getLongestLabel(AxisBase.java:474)
        at com.github.mikephil.charting.renderer.XAxisRenderer.computeSize(XAxisRenderer.java:78)
        at com.github.mikephil.charting.renderer.XAxisRenderer.computeAxisValues(XAxisRenderer.java:73)
        at com.github.mikephil.charting.renderer.XAxisRenderer.computeAxis(XAxisRenderer.java:66)
        at com.github.mikephil.charting.charts.BarLineChartBase.notifyDataSetChanged(BarLineChartBase.java:346)
        at com.robin.xbudget.Statistics$1.onItemSelected(Statistics.java:86)
        at android.widget.AdapterView.fireOnSelected(AdapterView.java:1366)
        at android.widget.AdapterView.dispatchOnItemSelected(AdapterView.java:1355)
        at android.widget.AdapterView.access$300(AdapterView.java:59)
        at android.widget.AdapterView$SelectionNotifier.run(AdapterView.java:1314)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7050)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:965)

Solution

  • you need to check the value in the getFormattedValue, because it can be < 0 or >= mValues.length

    class myXAxisValueFormatter extends IndexAxisValueFormatter {
    
        private String[] mValues;
        public myXAxisValueFormatter(String[]values) {
            this.mValues = values;
        }
    
        @Override
        public String getFormattedValue(float value) {
            int intValue = (int)value;
            if (intValue < 0 || intValue >= mValues.length){
                return "";
            } else {
                return mValues[intValue];
            }
        }
    }