5

I am starting to use the MPAndroidChart library to build a StackedBarChart showing three y values. Here is the code:

public class Plot
{
    final Context context;
    final BarData data;

    private int count;

    public StackedBarPlot(Context context)
    {
        this.context = context;
        data = setData();
    }

    protected BarData setData()
    {
        final List<BarEntry> entries = new ArrayList<>();
        for (DatabaseEntry entry : entryList)
        {
            final float total = (float) entry.getTotal();
            final float[] y = {100 * entry.getN1() / total,
                    100 * entry.getN2() / total, 100 * entry.getN3() / total};
            entries.add(new BarEntry(/*long*/entry.getDate(), y));
        }
        count = entries.size();


        final BarDataSet dataset = new BarDataSet(entries, null);
        dataset.setColors(new int[]{R.color.green, R.color.blue, R.color.red}, context);
        dataset.setStackLabels(labels);
        dataset.setDrawValues(true);
        dataset.setVisible(true);

        final BarData data = new BarData(dataset);
        data.setBarWidth(0.9f);
        return data;
    }

    public BarChart getChart(int id, View view)
    {
        final BarChart chart = (BarChart) view.findViewById(id);   

        chart.getAxisRight().setEnabled(false);
        chart.getAxisLeft().setEnabled(false);
        final Legend legend = chart.getLegend();
        legend.setDrawInside(true);
        legend.setVerticalAlignment(Legend.LegendVerticalAlignment.TOP);
        legend.setHorizontalAlignment(Legend.LegendHorizontalAlignment.CENTER);

        final XAxis xAxis = chart.getXAxis();
        xAxis.setValueFormatter(dateFormatter);
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setDrawGridLines(false);
        xAxis.setLabelCount(count);

        chart.getDescription().setEnabled(false);
        chart.setData(data);
        chart.setFitBars(true);
        chart.invalidate();
        return chart;
    }

    private final IAxisValueFormatter dateFormatter = new IAxisValueFormatter()
    {
        @Override
        public String getFormattedValue(float value, AxisBase axis)
        {
            return new DateTime((long) value).toString(context.getString("E, MMM d"));
        }
    };
}

Then in my Fragment, I call:

public class MyFragment extends Fragment
{
    private Plot plot;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        plot = new Plot(getActivity());
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)
    {
        final View view = inflater.inflate(R.layout.fragment, parent, false);
        plot.getChart(R.id.chart, view);
        return view;
    }
}

And in MainActivity.java

getFragmentManager().beginTransaction().replace(R.id.content, fragment).commit();

main_activity.xml

 <android.support.v4.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    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=".MainActivity">

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

        </android.support.design.widget.AppBarLayout>

        <FrameLayout
            android:id="@+id/content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    </android.support.design.widget.CoordinatorLayout>

    <android.support.design.widget.NavigationView
        android:id="@+id/navigation"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/drawer_header"
        app:menu="@menu/navigation"/>

</android.support.v4.widget.DrawerLayout>

fragment.xml

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <com.github.mikephil.charting.charts.BarChart
        android:id="@+id/chart"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

The problem is the bars are not rendering correctly. I can see values but the bars are not displaying in the chart. Any advice?

A.A.
  • 866
  • 1
  • 8
  • 22
  • I copied and pasted the code into my IDE to try and debug it but there are some unresolved dependencies that make it difficult to look at and a lack of information about how you are consuming the chart. Please create a [MCVE] that people can just use without having to load your project. However, I suspect this is an issue with how you have set up your XML for the container for the chart. – David Rawson Jan 15 '17 at 09:01
  • @DavidRawson Added layout files and full fragment code – A.A. Jan 15 '17 at 09:34
  • Thanks for updating - I think this could possibly be a bug in the library when you put the chart inside an Activity with a Navigation Drawer. I'll have to look further to confirm – David Rawson Jan 15 '17 at 09:52

4 Answers4

11

This is not an error in the library like some of the previous posts have suggested. Probably you are just misunderstanding how the width of each bar is determined like I did at first.

I was using a long, millisecond time-stamp for my x-axis on my bar chart. I realized that by default MPAndroidChart sets the width of a bar to 0.85f. Think about what this means. My first time-stamp was 1473421800000f and the next was 1473508200000f: a difference of 86400000f! Now how could I expect to see a bar that is 0.85f wide when there is 86400000f between each pair of observations?? To fix this problem you need to do something like the following:

barData.setBarWidth(0.6f * widthBetweenObservations);

So the above sets the width of a bar to equal 60% of the distance between observations.

javajared
  • 181
  • 2
  • 9
1

I had to start from this StackedBarActivity example and going down one bit at a time until I figured out what's causing the problem. It's the use of the long timestamp from entry.getDate() for the X axis values with or without the custom IAxisValueFormatter. It's a bug in the library reported here.

Here is what I ended up doing as a workaround. I got the duration since the timestamp in days:

long diff = new Duration(entry.getDate(), DateTime.now().getMillis()).getStandardDays();
entries.add(new BarEntry(diff, y));

and then in my custom IAxisValueFormatter:

private final IAxisValueFormatter dateFormatter = new IAxisValueFormatter()
{
    @Override
    public String getFormattedValue(float value, AxisBase axis)
    {
        return LocalDate.now().minusDays((int) value).toString("EEE");
    }
};
A.A.
  • 866
  • 1
  • 8
  • 22
  • 1
    Thanks very much for this useful answer. There were some other questions like this one too! – David Rawson Jan 16 '17 at 09:56
  • This won't work if you've got timestamp for dates in the past and in the future together, because the library accepts only entries added to a DataSet sorted by their x-position. You should sort the entries after having calculated the Duration between your date and the DateTime.now(), but then your data will be messed up – Keridano Jan 31 '17 at 14:14
  • @Keridano it will still work for the case you described. The value of `diff` can be positive or negative and then in the formatter, `minusDays()` will give the correct date after subtracting/addingthe difference. – A.A. Jan 31 '17 at 23:36
  • The problem lays before the formatter it is called. Inside the library flow when it is trying to calculate the x.max and x.min after you have called chart.setData() the app will crash because the data is not sorted. – Keridano Feb 01 '17 at 10:16
  • @Keridano the data is still sorted. It just has both negative and positive values. Have you actually tested the solution before deciding that it won't work? – A.A. Feb 01 '17 at 13:01
  • Yes, I tried in several ways (I've also changed the reference Date with Distant Future and Distant Past dates instead of the DateTime.now()) and a NegativeArraySizeException occurred in every case. I searched in the MPAndroidChart issue list and I found this [link](https://github.com/PhilJay/MPAndroidChart/issues/2074). – Keridano Feb 01 '17 at 15:41
  • @Keridano Thank you for sharing this. This solution still works for my case and any similar cases where no future dates are included in the data. Please share what you did to fix the future date case in a separate answer in case someone else needs it. Also, please provide an example where one would need to plot data for future dates – A.A. Feb 01 '17 at 17:01
  • Solution found! I've tested it and it works without problems, see answer below – Keridano Feb 09 '17 at 08:36
1

I've found another workaround, that will work even if you've got timestamps for dates in the past and in the future together, (see the comments in A.A answer for the full story). The trick is similar to the one that you can use when you have to plot multiple datasets with different values in the X-axis (see https://stackoverflow.com/a/28549480/5098038). Instead of putting the timestamps directly into the BarEntries x values, create an ArrayList which contains the timestamps and put the indexes into the BarEntries. Then in the formatter take the values contained into the dataset (the indexes) and use them to get the timestamps contained in the Arraylist.


Create array from original timestamp values:

Long[] xTimestamps = { t1, t2, t3 };
List<Long> xArray  = new ArrayList<>(Arrays.asList(xTimestamps));

Add indexes into BarEntries:

entries.add(new BarEntry(xArray.indexOf(t1), y));

Retrieve data with formatter:

private static final SimpleDateFormat CHART_SDF = new SimpleDateFormat("dd/MM/yyyy", getApplicationContext().getResources().getConfiguration().locale);
private final IAxisValueFormatter dateFormatter = new IAxisValueFormatter() {
            @Override
            public String getFormattedValue(float value, AxisBase axis) {
                Long timestamp = xArray.get((int)value);
                return Chart_SDF.format(new Date(timestamp));
            }
});
Community
  • 1
  • 1
Keridano
  • 146
  • 1
  • 7
1

One more pretty workaround. Just use TimeUnit.MILLISECONDS.toDays in BarEntry and TimeUnit.DAYS.toMillis in formatter

Long dateInMills = someDateObject.getTime();
entries.add(new BarEntry(TimeUnit.MILLISECONDS.toDays(dateInMills), y));

and then in my custom IAxisValueFormatter:

private final IAxisValueFormatter dateFormatter = new IAxisValueFormatter()
{
    @Override
    public String getFormattedValue(float value, AxisBase axis)
    {
            Float fVal = value;
            long mills = TimeUnit.DAYS.toMillis(fVal.longValue());
            ....
            //here convert mills to date and string
            ....
            return convertedString;
    }
};
Dmitry
  • 133
  • 1
  • 7
  • Be carefull. Hours will be rounding and If your time is not in GMT time will be shifted by one day... – Dmitry Mar 23 '17 at 13:47