Android - 使用MPAndroidChart填充两条线之间的颜色

8

enter image description here

我正在使用setFillFormatter,但它并没有帮助我,setfillColor()会跨越第二条线(黑色),因为没有办法在第二条线的Y值处停止第一条线(黄色)。

我想要实现这样的效果:

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

有没有办法为第一条线的每个X值找到第二条线的Y值?我看到dataSetdataProvider对于每次调用getFillLinePosition都返回固定值。


2
我认为你不能使用库中提供的方法来实现这个。你可能需要编写一个定制的渲染器来完成它。看一下 LineChartRenderer - David Rawson
CombinedChart 上填充两条线之间的空间是否可能?请参见我的问题:https://stackoverflow.com/questions/49986048/mpandroidchart-how-to-fill-area-between-two-lines-on-a-combinedchart - ban-geoengineering
2个回答

12
感谢 David Rawson 指引我使用 LineChartRenderer。现在我可以给两条线之间的区域填充颜色。
我们需要进行两个主要更改:
1.实现一个定制的 FillFormator 来返回另一条线的数据集。
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;
}}
实现自定义的LineChartRenderer,绘制并填充封闭路径。
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();
}}
  • 活动结束后

    MyFillFormatter设置为其中一个LineDataSet,并传递另一个LineDataSet作为参数。

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

    这里输入图片描述


    你能否详细说明如何重写 drawCubicFill() 方法,因为该方法被调用而不是 drawLinearFill() 方法。(这是因为我调用了 dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER)。) - ban-geoengineering
    我已经解决了。请查看我的答案,其中包含 MyLineLegendRenderer 的扩展版本 - https://dev59.com/OKDia4cB1Zd3GeqPJc7_#50118570 - ban-geoengineering
    2
    如果您在图表上有其他行,则会出现ClassCastException - 无法将DefaultFillFormatter转换为MyFillFormatter,因此我建议您在渲染器中添加一些检查,如果FillFormatter是DefaultFillFormatter的实例,则使用超级方法而不是来自MyFillFormatter的方法。 - Gabriella Angelova
    干得好!你知道如何根据LineDataSet的条件自定义填充颜色吗?例如,如果我的线在getFillLineBoundary()上方,我想要蓝色,如果我的线在该线下方,我想要绿色。 - Manuela
    @Amit,这个能用在这个问题上吗?https://dev59.com/M73pa4cB1Zd3GeqPeXIj - Jama Mohamed
    显示剩余2条评论

    8
    我使用了Amit的 被接受的回答,但是修改了他的 MyLineLegendRenderer ,以便您还可以在两个水平贝塞尔线之间填充 - 例如,如果您正在使用 myDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); 我还稍微整理了一下代码 - 比如添加了注释、删除了冗余的代码等等。
    因此,这是我替换 Amit 的 MyLineLegendRenderer 类的代码:
    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();
    
        }
    
    }
    

    您应该将此类与Amit的答案中的其他代码一起使用。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接