Android - Fill the color between two lines using MPAndroidChart
Asked Answered
S

2

8

enter image description here

I am using setFillFormatter, but it's not helping me and, setfillColor() crosses the second line(black) as there is no way to stop the first line(Yellow) at Y values of the second line. I want to implement something like this:

dataSet.setFillFormatter(new IFillFormatter() {

            @Override
            public float getFillLinePosition(ILineDataSet dataSet, LineDataProvider dataProvider) {
                return //return Y value of the second line for current X of line being filled;
            }
        });

Is there any way to find the Y value of the second line for each X of first line? I see both dataSet and dataProvider returns fixed values for each call of getFillLinePosition.

Seacoast answered 11/4, 2017 at 9:54 Comment(2)
I don't think you can do that using the provided methods in the library. You will probably have to write a custom renderer to do that. Look at LineChartRendererLauncher
Is it possible to fill the space between two lines on a CombinedChart? Please see my question at #49986548Digestive
S
12

Thanks David Rawson for pointing me towards LineChartRenderer. I am able to color the area between two lines.

We need to make two major changes.

  1. Implement a custom FillFormator to return the dataset of another line.

    public class MyFillFormatter implements IFillFormatter {
    private ILineDataSet boundaryDataSet;
    
    public MyFillFormatter() {
        this(null);
    }
    //Pass the dataset of other line in the Constructor 
    public MyFillFormatter(ILineDataSet boundaryDataSet) {
        this.boundaryDataSet = boundaryDataSet;
    }
    
    @Override
    public float getFillLinePosition(ILineDataSet dataSet, LineDataProvider dataProvider) {
        return 0;
    }
    
    //Define a new method which is used in the LineChartRenderer
    public List<Entry> getFillLineBoundary() {
        if(boundaryDataSet != null) {
            return ((LineDataSet) boundaryDataSet).getValues();
        }
        return null;
    }}
    
  2. Implement a custom LineChartRenderer to draw and fill the enclosed path.

    public class MyLineLegendRenderer extends LineChartRenderer {
    
    public MyLineLegendRenderer(LineDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
        super(chart, animator, viewPortHandler);
    }
    
    //This method is same as it's parent implemntation
    @Override
    protected void drawLinearFill(Canvas c, ILineDataSet dataSet, Transformer trans, XBounds bounds) {
        final Path filled = mGenerateFilledPathBuffer;
    
        final int startingIndex = bounds.min;
        final int endingIndex = bounds.range + bounds.min;
        final int indexInterval = 128;
    
        int currentStartIndex = 0;
        int currentEndIndex = indexInterval;
        int iterations = 0;
    
        // Doing this iteratively in order to avoid OutOfMemory errors that can happen on large bounds sets.
        do {
            currentStartIndex = startingIndex + (iterations * indexInterval);
            currentEndIndex = currentStartIndex + indexInterval;
            currentEndIndex = currentEndIndex > endingIndex ? endingIndex : currentEndIndex;
    
            if (currentStartIndex <= currentEndIndex) {
                generateFilledPath(dataSet, currentStartIndex, currentEndIndex, filled);
    
                trans.pathValueToPixel(filled);
    
                final Drawable drawable = dataSet.getFillDrawable();
                if (drawable != null) {
    
                    drawFilledPath(c, filled, drawable);
                } else {
    
                    drawFilledPath(c, filled, dataSet.getFillColor(), dataSet.getFillAlpha());
                }
            }
    
            iterations++;
    
        } while (currentStartIndex <= currentEndIndex);
    }
    
    //This is where we define the area to be filled.
    private void generateFilledPath(final ILineDataSet dataSet, final int startIndex, final int endIndex, final Path outputPath) {
    
        //Call the custom method to retrieve the dataset for other line
        final List<Entry> boundaryEntry = ((MyFillFormatter)dataSet.getFillFormatter()).getFillLineBoundary();
    
        final float phaseY = mAnimator.getPhaseY();    
        final Path filled = outputPath;
        filled.reset();
    
        final Entry entry = dataSet.getEntryForIndex(startIndex);
    
        filled.moveTo(entry.getX(), boundaryEntry.get(0).getY());
        filled.lineTo(entry.getX(), entry.getY() * phaseY);
    
        // create a new path
        Entry currentEntry = null;
        Entry previousEntry = null;
        for (int x = startIndex + 1; x <= endIndex; x++) {
    
            currentEntry = dataSet.getEntryForIndex(x);
            filled.lineTo(currentEntry.getX(), currentEntry.getY() * phaseY);
    
        }
    
        // close up
        if (currentEntry != null && previousEntry!= null) {
            filled.lineTo(currentEntry.getX(), previousEntry.getY());
        }
    
        //Draw the path towards the other line 
        for (int x = endIndex ; x > startIndex; x--) {
            previousEntry = boundaryEntry.get(x);
            filled.lineTo(previousEntry.getX(), previousEntry.getY() * phaseY);
        }
    
        filled.close();
    }}
    
  3. At the end of the activity

    Set the MyFillFormatter to one of the LineDataSet passing another LineDataSet as argument.

    lineDataSet2.setFillFormatter(new MyFillFormatter(LineDataSet1));
    
    mChart.setRenderer(new MyLineLegendRenderer(mChart, mChart.getAnimator(), mChart.getViewPortHandler()));
    

enter image description here

Seacoast answered 17/4, 2017 at 13:25 Comment(7)
Could you expand your answer to override drawCubicFill() as that method is being called rather than drawLinearFill(). (This is because I am calling dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER).)Digestive
I have worked it out now. Please see my answer for extended version of MyLineLegendRenderer - https://mcmap.net/q/1257079/-android-fill-the-color-between-two-lines-using-mpandroidchartDigestive
as it is written, if you have other lines on the chart, you are getting ClassCastException - DefaultFillFormatter cannot be cast to MyFillFormatter, so I could suggest you to add some checks in the renderer and if the FillFormatter is instanceOf DefaultFillFormatter to use the super methods instead the ones from the MyFillFormatterGoodsized
great work! do you know how to customize fill color based on LineDataSet conditions? eg. I want to have blue color if my line is above the getFillLineBoundary() and green color is my line is under that lineAugusto
@Augusto yes ..you just need to set these two attributes while creating the lineDataSet: -LineDataSet lineDataSet = new LineDataSet(projectionEntries, "label"); lineDataSet.setColor(ContextCompat.getColor(context, color1)); lineDataSet.setFillColor(ContextCompat.getColor(context, color2));Seacoast
Thanks @Amit, but it is not working for me. My issue is the one described here: #56363092, I just created a new question to attach the screenshot and to be clearer :)Augusto
@Amit, can this be used on this #64090307Olly
D
8

I have used Amit's accepted answer, but have modified his MyLineLegendRenderer so that you can also fill between two horizontal bezier lines - e.g., if you are using myDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);

I've also cleaned up the code a little bit - e.g, added comments, removed redundant code, etc.

So here is my replacement for Amit's MyLineLegendRenderer class:

import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import com.github.mikephil.charting.animation.ChartAnimator;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider;
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
import com.github.mikephil.charting.renderer.LineChartRenderer;
import com.github.mikephil.charting.utils.Transformer;
import com.github.mikephil.charting.utils.ViewPortHandler;
import java.util.List;

public class MyLineLegendRenderer extends LineChartRenderer {

    MyLineLegendRenderer(LineDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
        super(chart, animator, viewPortHandler);
    }

    // This method is same as its parent implementation. (Required so our version of generateFilledPath() is called.)
    @Override
    protected void drawLinearFill(Canvas c, ILineDataSet dataSet, Transformer trans, XBounds bounds) {

        final Path filled = mGenerateFilledPathBuffer;

        final int startingIndex = bounds.min;
        final int endingIndex = bounds.range + bounds.min;
        final int indexInterval = 128;

        int currentStartIndex;
        int currentEndIndex;
        int iterations = 0;

        // Doing this iteratively in order to avoid OutOfMemory errors that can happen on large bounds sets.
        do {
            currentStartIndex = startingIndex + (iterations * indexInterval);

            currentEndIndex = currentStartIndex + indexInterval;
            currentEndIndex = currentEndIndex > endingIndex ? endingIndex : currentEndIndex;

            if (currentStartIndex <= currentEndIndex) {
                generateFilledPath(dataSet, currentStartIndex, currentEndIndex, filled);

                trans.pathValueToPixel(filled);

                final Drawable drawable = dataSet.getFillDrawable();
                if (drawable != null) {
                    drawFilledPath(c, filled, drawable);
                }
                else {
                    drawFilledPath(c, filled, dataSet.getFillColor(), dataSet.getFillAlpha());
                }
            }

            iterations++;

        } while (currentStartIndex <= currentEndIndex);
    }

    // This method defines the perimeter of the area to be filled for horizontal bezier data sets.
    @Override
    protected void drawCubicFill(Canvas c, ILineDataSet dataSet, Path spline, Transformer trans, XBounds bounds) {

        final float phaseY = mAnimator.getPhaseY();

        //Call the custom method to retrieve the dataset for other line
        final List<Entry> boundaryEntries = ((MyFillFormatter)dataSet.getFillFormatter()).getFillLineBoundary();

        // We are currently at top-last point, so draw down to the last boundary point
        Entry boundaryEntry = boundaryEntries.get(bounds.min + bounds.range);
        spline.lineTo(boundaryEntry.getX(), boundaryEntry.getY() * phaseY);

        // Draw a cubic line going back through all the previous boundary points
        Entry prev = dataSet.getEntryForIndex(bounds.min + bounds.range);
        Entry cur = prev;
        for (int x = bounds.min + bounds.range; x >= bounds.min; x--) {

            prev = cur;
            cur = boundaryEntries.get(x);

            final float cpx = (prev.getX()) + (cur.getX() - prev.getX()) / 2.0f;

            spline.cubicTo(
                    cpx, prev.getY() * phaseY,
                    cpx, cur.getY() * phaseY,
                    cur.getX(), cur.getY() * phaseY);
        }

        // Join up the perimeter
        spline.close();

        trans.pathValueToPixel(spline);

        final Drawable drawable = dataSet.getFillDrawable();
        if (drawable != null) {
            drawFilledPath(c, spline, drawable);
        }
        else {
            drawFilledPath(c, spline, dataSet.getFillColor(), dataSet.getFillAlpha());
        }

    }

    // This method defines the perimeter of the area to be filled for straight-line (default) data sets.
    private void generateFilledPath(final ILineDataSet dataSet, final int startIndex, final int endIndex, final Path outputPath) {

        final float phaseY = mAnimator.getPhaseY();
        final Path filled = outputPath; // Not sure if this is required, but this is done in the original code so preserving the same technique here.
        filled.reset();

        //Call the custom method to retrieve the dataset for other line
        final List<Entry> boundaryEntries = ((MyFillFormatter)dataSet.getFillFormatter()).getFillLineBoundary();

        final Entry entry = dataSet.getEntryForIndex(startIndex);
        final Entry boundaryEntry = boundaryEntries.get(startIndex);

        // Move down to boundary of first entry
        filled.moveTo(entry.getX(), boundaryEntry.getY() * phaseY);

        // Draw line up to value of first entry
        filled.lineTo(entry.getX(), entry.getY() * phaseY);

        // Draw line across to the values of the next entries
        Entry currentEntry;
        for (int x = startIndex + 1; x <= endIndex; x++) {
            currentEntry = dataSet.getEntryForIndex(x);
            filled.lineTo(currentEntry.getX(), currentEntry.getY() * phaseY);
        }

        // Draw down to the boundary value of the last entry, then back to the first boundary value
        Entry boundaryEntry1;
        for (int x = endIndex; x > startIndex; x--) {
            boundaryEntry1 = boundaryEntries.get(x);
            filled.lineTo(boundaryEntry1.getX(), boundaryEntry1.getY() * phaseY);
        }

        // Join up the perimeter
        filled.close();

    }

}

You should use this class along with the other code in Amit's answer.

Digestive answered 1/5, 2018 at 14:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.