Android - Fill the color between two lines using MPAndroidChart

元气小坏坏 提交于 2019-11-26 22:08:36

问题


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.


回答1:


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()));
    




回答2:


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.



来源:https://stackoverflow.com/questions/43342597/android-fill-the-color-between-two-lines-using-mpandroidchart

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!