| |
| package com.github.mikephil.charting.charts; |
| |
| import android.content.Context; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.util.AttributeSet; |
| |
| import com.github.mikephil.charting.data.Entry; |
| import com.github.mikephil.charting.data.LineData; |
| import com.github.mikephil.charting.data.LineDataSet; |
| import com.github.mikephil.charting.utils.FillFormatter; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Chart that draws lines, surfaces, circles, ... |
| * |
| * @author Philipp Jahoda |
| */ |
| public class LineChart extends BarLineChartBase<LineData> { |
| |
| /** the width of the highlighning line */ |
| protected float mHighlightWidth = 3f; |
| |
| /** paint for the inner circle of the value indicators */ |
| protected Paint mCirclePaintInner; |
| |
| private FillFormatter mFillFormatter; |
| |
| public LineChart(Context context) { |
| super(context); |
| } |
| |
| public LineChart(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| public LineChart(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| } |
| |
| @Override |
| protected void init() { |
| super.init(); |
| |
| mFillFormatter = new DefaultFillFormatter(); |
| |
| mCirclePaintInner = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mCirclePaintInner.setStyle(Paint.Style.FILL); |
| mCirclePaintInner.setColor(Color.WHITE); |
| |
| mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mHighlightPaint.setStyle(Paint.Style.STROKE); |
| mHighlightPaint.setStrokeWidth(2f); |
| mHighlightPaint.setColor(Color.rgb(255, 187, 115)); |
| } |
| |
| @Override |
| protected void calcMinMax(boolean fixedValues) { |
| super.calcMinMax(fixedValues); |
| |
| // // if there is only one value in the chart |
| // if (mOriginalData.getYValCount() == 1 |
| // || mOriginalData.getYValCount() <= mOriginalData.getDataSetCount()) { |
| // mDeltaX = 1; |
| // } |
| |
| if (mDeltaX == 0 && mData.getYValCount() > 0) |
| mDeltaX = 1; |
| } |
| |
| @Override |
| protected void drawHighlights() { |
| |
| for (int i = 0; i < mIndicesToHightlight.length; i++) { |
| |
| LineDataSet set = mData.getDataSetByIndex(mIndicesToHightlight[i] |
| .getDataSetIndex()); |
| |
| if (set == null) |
| continue; |
| |
| mHighlightPaint.setColor(set.getHighLightColor()); |
| |
| int xIndex = mIndicesToHightlight[i].getXIndex(); // get the |
| // x-position |
| |
| if (xIndex > mDeltaX * mPhaseX) |
| continue; |
| |
| float y = set.getYValForXIndex(xIndex) * mPhaseY; // get the |
| // y-position |
| |
| float[] pts = new float[] { |
| xIndex, mYChartMax, xIndex, mYChartMin, 0, y, mDeltaX, y |
| }; |
| |
| mTrans.pointValuesToPixel(pts); |
| // draw the highlight lines |
| mDrawCanvas.drawLines(pts, mHighlightPaint); |
| } |
| } |
| |
| /** |
| * Class needed for saving the points when drawing cubic-lines. |
| * |
| * @author Philipp Jahoda |
| */ |
| protected class CPoint { |
| |
| public float x = 0f; |
| public float y = 0f; |
| |
| /** x-axis distance */ |
| public float dx = 0f; |
| |
| /** y-axis distance */ |
| public float dy = 0f; |
| |
| public CPoint(float x, float y) { |
| this.x = x; |
| this.y = y; |
| } |
| } |
| |
| /** |
| * draws the given y values to the screen |
| */ |
| @Override |
| protected void drawData() { |
| |
| ArrayList<LineDataSet> dataSets = mData.getDataSets(); |
| |
| for (int i = 0; i < mData.getDataSetCount(); i++) { |
| |
| LineDataSet dataSet = dataSets.get(i); |
| ArrayList<Entry> entries = dataSet.getYVals(); |
| |
| if (entries.size() < 1) |
| continue; |
| |
| mRenderPaint.setStrokeWidth(dataSet.getLineWidth()); |
| mRenderPaint.setPathEffect(dataSet.getDashPathEffect()); |
| |
| // if drawing cubic lines is enabled |
| if (dataSet.isDrawCubicEnabled()) { |
| drawCubic(dataSet, entries); |
| |
| // draw normal (straight) lines |
| } else { |
| drawLinear(dataSet, entries); |
| } |
| |
| mRenderPaint.setPathEffect(null); |
| } |
| } |
| |
| protected void drawCubic(LineDataSet dataSet, ArrayList<Entry> entries) |
| { |
| // get the color that is specified for this position from the |
| // DataSet |
| mRenderPaint.setColor(dataSet.getColor()); |
| |
| float intensity = dataSet.getCubicIntensity(); |
| |
| // the path for the cubic-spline |
| Path spline = new Path(); |
| |
| ArrayList<CPoint> points = new ArrayList<CPoint>(); |
| for (Entry e : entries) |
| points.add(new CPoint(e.getXIndex(), e.getVal())); |
| |
| if (points.size() > 1) { |
| for (int j = 0; j < points.size() * mPhaseX; j++) { |
| |
| CPoint point = points.get(j); |
| |
| if (j == 0) { |
| CPoint next = points.get(j + 1); |
| point.dx = ((next.x - point.x) * intensity); |
| point.dy = ((next.y - point.y) * intensity); |
| } |
| else if (j == points.size() - 1) { |
| CPoint prev = points.get(j - 1); |
| point.dx = ((point.x - prev.x) * intensity); |
| point.dy = ((point.y - prev.y) * intensity); |
| } |
| else { |
| CPoint next = points.get(j + 1); |
| CPoint prev = points.get(j - 1); |
| point.dx = ((next.x - prev.x) * intensity); |
| point.dy = ((next.y - prev.y) * intensity); |
| } |
| |
| // create the cubic-spline path |
| if (j == 0) { |
| spline.moveTo(point.x, point.y * mPhaseY); |
| } |
| else { |
| CPoint prev = points.get(j - 1); |
| spline.cubicTo(prev.x + prev.dx, (prev.y + prev.dy) * mPhaseY, point.x |
| - point.dx, |
| (point.y - point.dy) * mPhaseY, point.x, point.y * mPhaseY); |
| } |
| } |
| } |
| |
| // if filled is enabled, close the path |
| if (dataSet.isDrawFilledEnabled()) { |
| drawCubicFill(dataSet, entries, spline); |
| } else { |
| mRenderPaint.setStyle(Paint.Style.STROKE); |
| } |
| |
| mTrans.pathValueToPixel(spline); |
| |
| mDrawCanvas.drawPath(spline, mRenderPaint); |
| |
| } |
| |
| protected void drawCubicFill(LineDataSet dataSet, ArrayList<Entry> entries, Path spline) |
| { |
| float fillMin = mFillFormatter |
| .getFillLinePosition(dataSet, mData, mYChartMax, mYChartMin); |
| |
| spline.lineTo((entries.size() - 1) * mPhaseX, fillMin); |
| spline.lineTo(0, fillMin); |
| spline.close(); |
| |
| mRenderPaint.setStyle(Paint.Style.FILL); |
| } |
| |
| protected void drawLinear(LineDataSet dataSet, ArrayList<Entry> entries) |
| { |
| mRenderPaint.setStyle(Paint.Style.STROKE); |
| |
| // more than 1 color |
| if (dataSet.getColors() == null || dataSet.getColors().size() > 1) { |
| |
| float[] valuePoints = mTrans.generateTransformedValuesLineScatter(entries, mPhaseY); |
| |
| for (int j = 0; j < (valuePoints.length - 2) * mPhaseX; j += 2) { |
| |
| if (isOffContentRight(valuePoints[j])) |
| break; |
| |
| // make sure the lines don't do shitty things outside |
| // bounds |
| if (j != 0 && isOffContentLeft(valuePoints[j - 1]) |
| && isOffContentTop(valuePoints[j + 1]) |
| && isOffContentBottom(valuePoints[j + 1])) |
| continue; |
| |
| // get the color that is set for this line-segment |
| mRenderPaint.setColor(dataSet.getColor(j / 2)); |
| |
| mDrawCanvas.drawLine(valuePoints[j], valuePoints[j + 1], |
| valuePoints[j + 2], valuePoints[j + 3], mRenderPaint); |
| } |
| |
| } else { // only one color per dataset |
| |
| mRenderPaint.setColor(dataSet.getColor()); |
| |
| Path line = generateLinePath(entries); |
| mTrans.pathValueToPixel(line); |
| |
| mDrawCanvas.drawPath(line, mRenderPaint); |
| } |
| |
| mRenderPaint.setPathEffect(null); |
| |
| // if drawing filled is enabled |
| if (dataSet.isDrawFilledEnabled() && entries.size() > 0) { |
| drawLinearFill(dataSet, entries); |
| } |
| } |
| |
| protected void drawLinearFill(LineDataSet dataSet, ArrayList<Entry> entries) |
| { |
| // mDrawCanvas.drawVertices(VertexMode.TRIANGLE_STRIP, |
| // valuePoints.length, valuePoints, 0, |
| // null, 0, null, 0, null, 0, 0, paint); |
| |
| mRenderPaint.setStyle(Paint.Style.FILL); |
| |
| mRenderPaint.setColor(dataSet.getFillColor()); |
| // filled is drawn with less alpha |
| mRenderPaint.setAlpha(dataSet.getFillAlpha()); |
| |
| // mRenderPaint.setShader(dataSet.getShader()); |
| |
| Path filled = generateFilledPath(entries, |
| mFillFormatter.getFillLinePosition(dataSet, mData, mYChartMax, |
| mYChartMin)); |
| |
| mTrans.pathValueToPixel(filled); |
| |
| mDrawCanvas.drawPath(filled, mRenderPaint); |
| |
| // restore alpha |
| mRenderPaint.setAlpha(255); |
| // mRenderPaint.setShader(null); |
| } |
| |
| /** |
| * Generates the path that is used for filled drawing. |
| * |
| * @param entries |
| * @return |
| */ |
| private Path generateFilledPath(ArrayList<Entry> entries, float fillMin) { |
| |
| Path filled = new Path(); |
| filled.moveTo(entries.get(0).getXIndex(), entries.get(0).getVal() * mPhaseY); |
| |
| // create a new path |
| for (int x = 1; x < entries.size() * mPhaseX; x++) { |
| |
| Entry e = entries.get(x); |
| filled.lineTo(e.getXIndex(), e.getVal() * mPhaseY); |
| } |
| |
| // close up |
| filled.lineTo(entries.get((int) ((entries.size() - 1) * mPhaseX)).getXIndex(), fillMin); |
| filled.lineTo(entries.get(0).getXIndex(), fillMin); |
| filled.close(); |
| |
| return filled; |
| } |
| |
| /** |
| * Generates the path that is used for drawing a single line. |
| * |
| * @param entries |
| * @return |
| */ |
| private Path generateLinePath(ArrayList<Entry> entries) { |
| |
| Path line = new Path(); |
| line.moveTo(entries.get(0).getXIndex(), entries.get(0).getVal() * mPhaseY); |
| |
| // create a new path |
| for (int x = 1; x < entries.size() * mPhaseX; x++) { |
| |
| Entry e = entries.get(x); |
| line.lineTo(e.getXIndex(), e.getVal() * mPhaseY); |
| } |
| |
| return line; |
| } |
| |
| @Override |
| protected void drawValues() { |
| |
| // if values are drawn |
| if (mDrawYValues && mData.getYValCount() < mMaxVisibleCount * mTrans.getScaleX()) { |
| |
| ArrayList<LineDataSet> dataSets = mData.getDataSets(); |
| |
| for (int i = 0; i < mData.getDataSetCount(); i++) { |
| |
| LineDataSet dataSet = dataSets.get(i); |
| |
| // make sure the values do not interfear with the circles |
| int valOffset = (int) (dataSet.getCircleSize() * 1.75f); |
| |
| if (!dataSet.isDrawCirclesEnabled()) |
| valOffset = valOffset / 2; |
| |
| ArrayList<Entry> entries = dataSet.getYVals(); |
| |
| float[] positions = mTrans.generateTransformedValuesLineScatter(entries, mPhaseY); |
| |
| for (int j = 0; j < positions.length * mPhaseX; j += 2) { |
| |
| if (isOffContentRight(positions[j])) |
| break; |
| |
| if (isOffContentLeft(positions[j]) || isOffContentTop(positions[j + 1]) |
| || isOffContentBottom(positions[j + 1])) |
| continue; |
| |
| float val = entries.get(j / 2).getVal(); |
| |
| if (mDrawUnitInChart) { |
| |
| mDrawCanvas.drawText(mValueFormatter.getFormattedValue(val) + mUnit, |
| positions[j], |
| positions[j + 1] |
| - valOffset, mValuePaint); |
| } else { |
| |
| mDrawCanvas.drawText(mValueFormatter.getFormattedValue(val), positions[j], |
| positions[j + 1] - valOffset, |
| mValuePaint); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * draws the circle value indicators |
| */ |
| @Override |
| protected void drawAdditional() { |
| |
| mRenderPaint.setStyle(Paint.Style.FILL); |
| |
| ArrayList<LineDataSet> dataSets = mData.getDataSets(); |
| |
| for (int i = 0; i < mData.getDataSetCount(); i++) { |
| |
| LineDataSet dataSet = dataSets.get(i); |
| |
| // if drawing circles is enabled for this dataset |
| if (dataSet.isDrawCirclesEnabled()) { |
| |
| ArrayList<Entry> entries = dataSet.getYVals(); |
| |
| float[] positions = mTrans.generateTransformedValuesLineScatter(entries, mPhaseY); |
| |
| for (int j = 0; j < positions.length * mPhaseX; j += 2) { |
| |
| // Set the color for the currently drawn value. If the index |
| // is |
| // out of bounds, reuse colors. |
| mRenderPaint.setColor(dataSet.getCircleColor(j / 2)); |
| |
| if (isOffContentRight(positions[j])) |
| break; |
| |
| // make sure the circles don't do shitty things outside |
| // bounds |
| if (isOffContentLeft(positions[j]) || |
| isOffContentTop(positions[j + 1]) |
| || isOffContentBottom(positions[j + 1])) |
| continue; |
| |
| mDrawCanvas.drawCircle(positions[j], positions[j + 1], dataSet.getCircleSize(), |
| mRenderPaint); |
| mDrawCanvas.drawCircle(positions[j], positions[j + 1], |
| dataSet.getCircleSize() / 2f, |
| mCirclePaintInner); |
| } |
| } // else do nothing |
| |
| } |
| } |
| |
| /** |
| * set the width of the highlightning lines, default 3f |
| * |
| * @param width |
| */ |
| public void setHighlightLineWidth(float width) { |
| mHighlightWidth = width; |
| } |
| |
| /** |
| * returns the width of the highlightning line, default 3f |
| * |
| * @return |
| */ |
| public float getHighlightLineWidth() { |
| return mHighlightWidth; |
| } |
| |
| @Override |
| public void setPaint(Paint p, int which) { |
| super.setPaint(p, which); |
| |
| switch (which) { |
| case PAINT_CIRCLES_INNER: |
| mCirclePaintInner = p; |
| break; |
| } |
| } |
| |
| @Override |
| public Paint getPaint(int which) { |
| Paint p = super.getPaint(which); |
| if (p != null) |
| return p; |
| |
| switch (which) { |
| case PAINT_CIRCLES_INNER: |
| return mCirclePaintInner; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Sets a custom FillFormatter to the chart that handles the position of the |
| * filled-line for each DataSet. Set this to null to use the default logic. |
| * |
| * @param formatter |
| */ |
| public void setFillFormatter(FillFormatter formatter) { |
| |
| if (formatter == null) |
| formatter = new DefaultFillFormatter(); |
| |
| mFillFormatter = formatter; |
| } |
| |
| /** |
| * Default formatter that calculates the position of the filled line. |
| * |
| * @author Philipp Jahoda |
| */ |
| private class DefaultFillFormatter implements FillFormatter { |
| |
| @Override |
| public float getFillLinePosition(LineDataSet dataSet, LineData data, |
| float chartMaxY, float chartMinY) { |
| |
| float fillMin = 0f; |
| |
| if (dataSet.getYMax() > 0 && dataSet.getYMin() < 0) { |
| fillMin = 0f; |
| } else { |
| |
| if (!mStartAtZero) { |
| |
| float max, min; |
| |
| if (data.getYMax() > 0) |
| max = 0f; |
| else |
| max = chartMaxY; |
| if (data.getYMin() < 0) |
| min = 0f; |
| else |
| min = chartMinY; |
| |
| fillMin = dataSet.getYMin() >= 0 ? min : max; |
| } else { |
| fillMin = 0f; |
| } |
| |
| } |
| |
| return fillMin; |
| } |
| } |
| } |