Android - 使用 MPAndroidChart 填充两条线之间的颜色
Android - Fill the color between two lines using MPAndroidChart
我正在使用 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 值吗?我看到 dataSet 和 dataProvider returns 每次调用 getFillLinePosition 的固定值。
感谢 David Rawson 为我指明 LineChartRenderer
。我可以为两条线之间的区域着色。
我们需要进行两项重大更改。
对另一行的数据集return实施自定义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();
}}
结束activity
将 MyFillFormatter
设置为 LineDataSet
之一,将另一个 LineDataSet
作为参数传递。
lineDataSet2.setFillFormatter(new MyFillFormatter(LineDataSet1));
mChart.setRenderer(new MyLineLegendRenderer(mChart, mChart.getAnimator(), mChart.getViewPortHandler()));
我使用了 Amit 的 ,但修改了他的 MyLineLegendRenderer
以便您也可以在两条 水平贝塞尔曲线 线之间填充 - 例如,如果您正在使用 myDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
我还稍微清理了代码 - 例如,添加了注释、删除了冗余代码等。
所以这是我替代 Amit 的 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();
}
}
您应该将此 class 与 中的其他代码一起使用。
我正在使用 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 值吗?我看到 dataSet 和 dataProvider returns 每次调用 getFillLinePosition 的固定值。
感谢 David Rawson 为我指明 LineChartRenderer
。我可以为两条线之间的区域着色。
我们需要进行两项重大更改。
对另一行的数据集return实施自定义
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(); }}
结束activity
将
MyFillFormatter
设置为LineDataSet
之一,将另一个LineDataSet
作为参数传递。lineDataSet2.setFillFormatter(new MyFillFormatter(LineDataSet1)); mChart.setRenderer(new MyLineLegendRenderer(mChart, mChart.getAnimator(), mChart.getViewPortHandler()));
我使用了 Amit 的 MyLineLegendRenderer
以便您也可以在两条 水平贝塞尔曲线 线之间填充 - 例如,如果您正在使用 myDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
我还稍微清理了代码 - 例如,添加了注释、删除了冗余代码等。
所以这是我替代 Amit 的 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();
}
}
您应该将此 class 与