MPAndroidChart: add custom image inside bars

To get the star image inside our bars, we will need to create a custom renderer. Because our bar chart uses BarChartRenderer we will subclass this first and add a parameter for our image:

public class ImageBarChartRenderer extends BarChartRenderer {

    private final Bitmap barImage;

    public ImageBarChartRenderer(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler, Bitmap barImage) {
        super(chart, animator, viewPortHandler);
        this.barImage = barImage;
    }

If we inspect the source for BarChartRenderer we can see that it calls the method called drawData and then iterates through each DataSet and calls drawDataSet. drawDataSet is where the action is happening: it's drawing the shadows and the bars. It's an appropriate place to add logic to draw an extra like images, so let's add a call to a method to draw our images there:

    @Override
    protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) {
        super.drawDataSet(c, dataSet, index);
        drawBarImages(c, dataSet, index);
    }

We now need a method that will iterate through the DataSet and draw the star images. An appropriate method that will serve as a template is drawValues so let's copy that and change it so that is draws an image rather than text. The key to understanding this is seeing how BarBuffer works. BarBuffer holds the on-screen (pixel) co-ordinates for a bar for a given Entry at j, j + 1, j + 2, j + 3.

To clarify, j is the left x co-ordinate, j + 1 is the top y co-ordinate and so on through to the right x co-ordinate at j + 3. We'll extract these to variables to make it easier to understand:

    protected void drawBarImages(Canvas c, IBarDataSet dataSet, int index) {
        BarBuffer buffer = mBarBuffers[index];

        float left; //avoid allocation inside loop
        float right;
        float top;
        float bottom;

        for (int j = 0; j < buffer.buffer.length * mAnimator.getPhaseX(); j += 4) {
            left = buffer.buffer[j];
            right = buffer.buffer[j + 2];
            top = buffer.buffer[j + 1];
            bottom = buffer.buffer[j + 3];

            float x = (left + right) / 2f;

            if (!mViewPortHandler.isInBoundsRight(x))
                break;

            if (!mViewPortHandler.isInBoundsY(top)
                    || !mViewPortHandler.isInBoundsLeft(x))
                continue;

            BarEntry entry = dataSet.getEntryForIndex(j / 4);
            float val = entry.getY();

            if (val > 50) {
                drawStar(c, barImage, x, top);
            }
        }
    }

Here's how to consume the renderer:

    Bitmap starBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.star);
    mChart.setRenderer(new ImageBarChartRenderer(mChart, mChart.getAnimator(), mChart.getViewPortHandler(), starBitmap));

The final step to the renderer is to add logic to scale the bitmap and position it correctly. Here is the final proof-of-concept of the custom renderer:

package com.xxmassdeveloper.mpchartexample;

import android.graphics.Bitmap;
import android.graphics.Canvas;

import com.github.mikephil.charting.animation.ChartAnimator;
import com.github.mikephil.charting.buffer.BarBuffer;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider;
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
import com.github.mikephil.charting.renderer.BarChartRenderer;
import com.github.mikephil.charting.utils.ViewPortHandler;

/**
 * Created by David on 29/12/2016.
 */

public class ImageBarChartRenderer extends BarChartRenderer {

    private final Bitmap barImage;

    public ImageBarChartRenderer(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler, Bitmap barImage) {
        super(chart, animator, viewPortHandler);
        this.barImage = barImage;
    }

    @Override
    public void drawData(Canvas c) {
        super.drawData(c);
    }

    @Override
    protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) {
        super.drawDataSet(c, dataSet, index);
        drawBarImages(c, dataSet, index);
    }

    protected void drawBarImages(Canvas c, IBarDataSet dataSet, int index) {
        BarBuffer buffer = mBarBuffers[index];

        float left; //avoid allocation inside loop
        float right;
        float top;
        float bottom;

        final Bitmap scaledBarImage = scaleBarImage(buffer);

        int starWidth = scaledBarImage.getWidth();
        int starOffset = starWidth / 2;

        for (int j = 0; j < buffer.buffer.length * mAnimator.getPhaseX(); j += 4) {
            left = buffer.buffer[j];
            right = buffer.buffer[j + 2];
            top = buffer.buffer[j + 1];
            bottom = buffer.buffer[j + 3];

            float x = (left + right) / 2f;

            if (!mViewPortHandler.isInBoundsRight(x))
                break;

            if (!mViewPortHandler.isInBoundsY(top)
                    || !mViewPortHandler.isInBoundsLeft(x))
                continue;

            BarEntry entry = dataSet.getEntryForIndex(j / 4);
            float val = entry.getY();

            if (val > 50) {
                drawImage(c, scaledBarImage, x - starOffset, top);
            }
        }
    }

    private Bitmap scaleBarImage(BarBuffer buffer) {
        float firstLeft = buffer.buffer[0];
        float firstRight = buffer.buffer[2];
        int firstWidth = (int) Math.ceil(firstRight - firstLeft);
        return Bitmap.createScaledBitmap(barImage, firstWidth, firstWidth, false);
    }

    protected void drawImage(Canvas c, Bitmap image, float x, float y) {
        if (image != null) {
            c.drawBitmap(image, x, y, null);
        }
    }
}

Here's a screen shot - you can see that values over 50 have the star:

bar chart with custom image inside bars