package com.github.mikephil.charting.charts; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.PointF; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import com.github.mikephil.charting.data.BarDataSet; import com.github.mikephil.charting.data.BarLineScatterCandleData; import com.github.mikephil.charting.data.BarLineScatterCandleRadarDataSet; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.filter.Approximator; import com.github.mikephil.charting.interfaces.OnDrawListener; import com.github.mikephil.charting.listener.BarLineChartTouchListener; import com.github.mikephil.charting.utils.Highlight; import com.github.mikephil.charting.utils.Legend.LegendPosition; import com.github.mikephil.charting.utils.LimitLine; import com.github.mikephil.charting.utils.LimitLine.LimitLabelPosition; import com.github.mikephil.charting.utils.PointD; import com.github.mikephil.charting.utils.SelInfo; import com.github.mikephil.charting.utils.Utils; import com.github.mikephil.charting.utils.XLabels; import com.github.mikephil.charting.utils.XLabels.XLabelPosition; import com.github.mikephil.charting.utils.YLabels; import com.github.mikephil.charting.utils.YLabels.YLabelPosition; import java.util.ArrayList; /** * Base-class of LineChart, BarChart, ScatterChart and CandleStickChart. * * @author Philipp Jahoda */ @SuppressLint("RtlHardcoded") public abstract class BarLineChartBase<T extends BarLineScatterCandleData<? extends BarLineScatterCandleRadarDataSet<? extends Entry>>> extends Chart<T> { /** the maximum number of entried to which values will be drawn */ protected int mMaxVisibleCount = 100; /** the width of the grid lines */ protected float mGridWidth = 1f; /** * flag that indicates if pinch-zoom is enabled. if true, both x and y axis * can be scaled with 2 fingers, if false, x and y axis can be scaled * separately */ protected boolean mPinchZoomEnabled = false; /** flat that indicates if double tap zoom is enabled or not */ protected boolean mDoubleTapToZoomEnabled = true; /** if true, dragging is enabled for the chart */ private boolean mDragEnabled = true; /** if true, scaling is enabled for the chart */ private boolean mScaleEnabled = true; /** if true, the y range is predefined */ protected boolean mFixedYValues = false; /** if true, the y-label entries will always start at zero */ protected boolean mStartAtZero = true; /** if true, data filtering is enabled */ protected boolean mFilterData = false; /** paint object for the grid lines */ protected Paint mGridPaint; /** paint object for the (by default) lightgrey background of the grid */ protected Paint mGridBackgroundPaint; /** paint for the line surrounding the chart */ protected Paint mBorderPaint; /** * if set to true, the highlight indicator (lines for linechart, dark bar * for barchart) will be drawn upon selecting values. */ protected boolean mHighLightIndicatorEnabled = true; /** flag indicating if the vertical grid should be drawn or not */ protected boolean mDrawVerticalGrid = true; /** flag indicating if the horizontal grid should be drawn or not */ protected boolean mDrawHorizontalGrid = true; /** flag indicating if the y-labels should be drawn or not */ protected boolean mDrawYLabels = true; /** flag indicating if the x-labels should be drawn or not */ protected boolean mDrawXLabels = true; /** flag indicating if the chart border rectangle should be drawn or not */ protected boolean mDrawBorder = true; /** flag indicating if the grid background should be drawn or not */ protected boolean mDrawGridBackground = true; /** the listener for user drawing on the chart */ protected OnDrawListener mDrawListener; /** * the object representing the labels on the y-axis, this object is prepared * in the pepareYLabels() method */ protected YLabels mYLabels = new YLabels(); /** the object representing the labels on the x-axis */ protected XLabels mXLabels = new XLabels(); // /** the approximator object used for data filtering */ // private Approximator mApproximator; public BarLineChartBase(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public BarLineChartBase(Context context, AttributeSet attrs) { super(context, attrs); } public BarLineChartBase(Context context) { super(context); } @Override protected void init() { super.init(); mListener = new BarLineChartTouchListener(this, mTrans.getTouchMatrix()); mGridPaint = new Paint(); mGridPaint.setColor(Color.GRAY); mGridPaint.setStrokeWidth(mGridWidth); mGridPaint.setStyle(Style.STROKE); mGridPaint.setAlpha(90); mBorderPaint = new Paint(); mBorderPaint.setColor(Color.BLACK); mBorderPaint.setStrokeWidth(mGridWidth * 2f); mBorderPaint.setStyle(Style.STROKE); mGridBackgroundPaint = new Paint(); mGridBackgroundPaint.setStyle(Style.FILL); // mGridBackgroundPaint.setColor(Color.WHITE); mGridBackgroundPaint.setColor(Color.rgb(240, 240, 240)); // light // grey } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mDataNotSet) return; long starttime = System.currentTimeMillis(); // if data filtering is enabled if (mFilterData) { mData = getFilteredData(); Log.i(LOG_TAG, "FilterTime: " + (System.currentTimeMillis() - starttime) + " ms"); starttime = System.currentTimeMillis(); } else { mData = getData(); // Log.i(LOG_TAG, "Filtering disabled."); } if (mXLabels.isAdjustXLabelsEnabled()) calcModulus(); // execute all drawing commands drawGridBackground(); prepareYLabels(); // make sure the graph values and grid cannot be drawn outside the // content-rect int clipRestoreCount = mDrawCanvas.save(); mDrawCanvas.clipRect(mContentRect); drawHorizontalGrid(); drawVerticalGrid(); drawData(); drawLimitLines(); // if highlighting is enabled if (mHighlightEnabled && mHighLightIndicatorEnabled && valuesToHighlight()) drawHighlights(); // Removes clipping rectangle mDrawCanvas.restoreToCount(clipRestoreCount); drawAdditional(); drawXLabels(); drawYLabels(); drawValues(); drawLegend(); drawBorder(); drawMarkers(); drawDescription(); canvas.drawBitmap(mDrawBitmap, 0, 0, mDrawPaint); if (mLogEnabled) Log.i(LOG_TAG, "DrawTime: " + (System.currentTimeMillis() - starttime) + " ms"); } /** * does all necessary preparations, needed when data is changed or flags * that effect the data are changed */ @Override public void prepare() { if (mDataNotSet) return; calcMinMax(mFixedYValues); prepareYLabels(); prepareXLabels(); prepareLegend(); calculateOffsets(); } /** * Sets up all the matrices that will be used for scaling the coordinates to * the display. Offset and Value-px. */ private void prepareMatrix() { mTrans.prepareMatrixValuePx(this); mTrans.prepareMatrixOffset(this); if (mLogEnabled) Log.i(LOG_TAG, "Matrices prepared."); } @Override public void notifyDataSetChanged() { if (!mFixedYValues) { prepare(); // prepareContentRect(); mTrans.prepareMatrixValuePx(this); } else { calcMinMax(mFixedYValues); } } @Override protected void calculateOffsets() { float legendRight = 0f, legendBottom = 0f; // setup offsets for legend if (mDrawLegend && mLegend != null && mLegend.getPosition() != LegendPosition.NONE) { if (mLegend.getPosition() == LegendPosition.RIGHT_OF_CHART || mLegend.getPosition() == LegendPosition.RIGHT_OF_CHART_CENTER) { // this is the space between the legend and the chart float spacing = Utils.convertDpToPixel(12f); legendRight = mLegend.getMaximumEntryLength(mLegendLabelPaint) + mLegend.getFormSize() + mLegend.getFormToTextSpace() + spacing; mLegendLabelPaint.setTextAlign(Align.LEFT); } else if (mLegend.getPosition() == LegendPosition.BELOW_CHART_LEFT || mLegend.getPosition() == LegendPosition.BELOW_CHART_RIGHT || mLegend.getPosition() == LegendPosition.BELOW_CHART_CENTER) { if (mXLabels.getPosition() == XLabelPosition.TOP) legendBottom = mLegendLabelPaint.getTextSize() * 3.5f; else { legendBottom = mLegendLabelPaint.getTextSize() * 2.5f; } } mLegend.setOffsetBottom(legendBottom); mLegend.setOffsetRight(legendRight); } float yleft = 0f, yright = 0f; // String label = mYLabels.getFormattedLabel(mYLabels.mEntryCount - 1); String label = mYLabels.getLongestLabel(); // calculate the maximum y-label width (including eventual offsets) float ylabelwidth = Utils.calcTextWidth(mYLabelPaint, label + mUnit + (mYChartMin < 0 ? "----" : "+++")); // offsets if (mDrawYLabels) { // offsets for y-labels if (mYLabels.getPosition() == YLabelPosition.LEFT) { yleft = ylabelwidth; mYLabelPaint.setTextAlign(Align.RIGHT); } else if (mYLabels.getPosition() == YLabelPosition.RIGHT) { yright = ylabelwidth; mYLabelPaint.setTextAlign(Align.LEFT); } else if (mYLabels.getPosition() == YLabelPosition.BOTH_SIDED) { yright = ylabelwidth; yleft = ylabelwidth; } } float xtop = 0f, xbottom = 0f; float xlabelheight = Utils.calcTextHeight(mXLabelPaint, "Q") * 2f; if (mDrawXLabels) { // offsets for x-labels if (mXLabels.getPosition() == XLabelPosition.BOTTOM) { xbottom = xlabelheight; } else if (mXLabels.getPosition() == XLabelPosition.TOP) { xtop = xlabelheight; } else if (mXLabels.getPosition() == XLabelPosition.BOTH_SIDED) { xbottom = xlabelheight; xtop = xlabelheight; } } // all required offsets are calculated, now find largest and apply float min = Utils.convertDpToPixel(11f); mOffsetBottom = Math.max(min, xbottom + legendBottom); mOffsetTop = Math.max(min, xtop); mOffsetLeft = Math.max(min, yleft); mOffsetRight = Math.max(min, yright + legendRight); if (mLegend != null) { // those offsets are equal for legend and other chart, just apply // them mLegend.setOffsetTop(mOffsetTop + min / 3f); mLegend.setOffsetLeft(mOffsetLeft); } prepareContentRect(); prepareMatrix(); } /** * Calculates the offsets that belong to the legend, this method is only * relevant when drawing into the chart. It can be used to refresh the * legend. */ public void calculateLegendOffsets() { // setup offsets for legend if (mLegend.getPosition() == LegendPosition.RIGHT_OF_CHART) { mLegend.setOffsetRight(mLegend.getMaximumEntryLength(mLegendLabelPaint)); mLegendLabelPaint.setTextAlign(Align.LEFT); } else if (mLegend.getPosition() == LegendPosition.BELOW_CHART_LEFT || mLegend.getPosition() == LegendPosition.BELOW_CHART_RIGHT) { if (mXLabels.getPosition() == XLabelPosition.TOP) mLegend.setOffsetBottom(mLegendLabelPaint.getTextSize() * 3.5f); else { mLegend.setOffsetBottom(mLegendLabelPaint.getTextSize() * 2.5f); } } } /** * calculates the modulus for x-labels and grid */ protected void calcModulus() { float[] values = new float[9]; mTrans.getTouchMatrix().getValues(values); mXLabels.mXAxisLabelModulus = (int) Math .ceil((mData.getXValCount() * mXLabels.mLabelWidth) / (mContentRect.width() * values[Matrix.MSCALE_X])); } @Override protected void calcMinMax(boolean fixedValues) { super.calcMinMax(fixedValues); // calc min and max in the super class if (!fixedValues) { // additional handling for space (default 15% space) // float space = Math.abs(mDeltaY / 100f * 15f); float space = Math .abs(Math.abs(Math.max(Math.abs(mYChartMax), Math.abs(mYChartMin))) / 100f * 20f); if (Math.abs(mYChartMax - mYChartMin) < 0.00001f) { if (Math.abs(mYChartMax) < 10f) space = 1f; else space = Math.abs(mYChartMax / 100f * 20f); } if (mStartAtZero) { if (mYChartMax < 0) { mYChartMax = 0; // calc delta mYChartMin = mYChartMin - space; } else { mYChartMin = 0; // calc delta mYChartMax = mYChartMax + space; } } else { mYChartMin = mYChartMin - space / 2f; mYChartMax = mYChartMax + space / 2f; } } mDeltaY = Math.abs(mYChartMax - mYChartMin); } /** * setup the x-axis labels */ protected void prepareXLabels() { StringBuffer a = new StringBuffer(); int max = (int) Math.round(mData.getXValAverageLength() + mXLabels.getSpaceBetweenLabels()); for (int i = 0; i < max; i++) { a.append("h"); } mXLabels.mLabelWidth = Utils.calcTextWidth(mXLabelPaint, a.toString()); mXLabels.mLabelHeight = Utils.calcTextHeight(mXLabelPaint, "Q"); } /** * Sets up the y-axis labels. Computes the desired number of labels between * the two given extremes. Unlike the papareXLabels() method, this method * needs to be called upon every refresh of the view. * * @return */ protected void prepareYLabels() { float yMin = 0f; float yMax = 0f; // calculate the starting and entry point of the y-labels (depending on // zoom / contentrect bounds) if (mContentRect.width() > 10 && !mTrans.isFullyZoomedOutY()) { PointD p1 = getValuesByTouchPoint(mContentRect.left, mContentRect.top); PointD p2 = getValuesByTouchPoint(mContentRect.left, mContentRect.bottom); if (!mTrans.isInvertYAxisEnabled()) { yMin = (float) p2.y; yMax = (float) p1.y; } else { if (!mStartAtZero) yMin = (float) Math.min(p1.y, p2.y); else yMin = 0; yMax = (float) Math.max(p1.y, p2.y); } } else { if (!mTrans.isInvertYAxisEnabled()) { yMin = mYChartMin; yMax = mYChartMax; } else { if (!mStartAtZero) yMin = (float) Math.min(mYChartMax, mYChartMin); else yMin = 0; yMax = (float) Math.max(mYChartMax, mYChartMin); } } int labelCount = mYLabels.getLabelCount(); double range = Math.abs(yMax - yMin); if (labelCount == 0 || range <= 0) { mYLabels.mEntries = new float[] {}; mYLabels.mEntryCount = 0; return; } double rawInterval = range / labelCount; double interval = Utils.roundToNextSignificant(rawInterval); double intervalMagnitude = Math.pow(10, (int) Math.log10(interval)); int intervalSigDigit = (int) (interval / intervalMagnitude); if (intervalSigDigit > 5) { // Use one order of magnitude higher, to avoid intervals like 0.9 or // 90 interval = Math.floor(10 * intervalMagnitude); } // if the labels should only show min and max if (mYLabels.isShowOnlyMinMaxEnabled()) { mYLabels.mEntryCount = 2; mYLabels.mEntries = new float[2]; mYLabels.mEntries[0] = mYChartMin; mYLabels.mEntries[1] = mYChartMax; } else { double first = Math.ceil(yMin / interval) * interval; double last = Utils.nextUp(Math.floor(yMax / interval) * interval); double f; int i; int n = 0; for (f = first; f <= last; f += interval) { ++n; } mYLabels.mEntryCount = n; if (mYLabels.mEntries.length < n) { // Ensure stops contains at least numStops elements. mYLabels.mEntries = new float[n]; } for (f = first, i = 0; i < n; f += interval, ++i) { mYLabels.mEntries[i] = (float) f; } } if (interval < 1) { mYLabels.mDecimals = (int) Math.ceil(-Math.log10(interval)); } else { mYLabels.mDecimals = 0; } } /** * draws the x-axis labels to the screen depending on their position */ private void drawXLabels() { if (!mDrawXLabels) return; float yoffset = Utils.convertDpToPixel(4f); mXLabelPaint.setTypeface(mXLabels.getTypeface()); mXLabelPaint.setTextSize(mXLabels.getTextSize()); mXLabelPaint.setColor(mXLabels.getTextColor()); if (mXLabels.getPosition() == XLabelPosition.TOP) { drawXLabels(getOffsetTop() - yoffset); } else if (mXLabels.getPosition() == XLabelPosition.BOTTOM) { drawXLabels(getHeight() - mOffsetBottom + mXLabels.mLabelHeight + yoffset * 1.5f); } else if (mXLabels.getPosition() == XLabelPosition.BOTTOM_INSIDE) { drawXLabels(getHeight() - getOffsetBottom() - yoffset); } else if (mXLabels.getPosition() == XLabelPosition.TOP_INSIDE) { drawXLabels(getOffsetTop() + yoffset + mXLabels.mLabelHeight); } else { // BOTH SIDED drawXLabels(getOffsetTop() - 7); drawXLabels(getHeight() - mOffsetBottom + mXLabels.mLabelHeight + yoffset * 1.6f); } } /** * draws the x-labels on the specified y-position * * @param yPos */ protected void drawXLabels(float yPos) { // pre allocate to save performance (dont allocate in loop) float[] position = new float[] { 0f, 0f }; for (int i = 0; i < mData.getXValCount(); i += mXLabels.mXAxisLabelModulus) { position[0] = i; // center the text if (mXLabels.isCenterXLabelsEnabled()) position[0] += 0.5f; mTrans.pointValuesToPixel(position); if (position[0] >= mOffsetLeft && position[0] <= getWidth() - mOffsetRight) { String label = mData.getXVals().get(i); if (mXLabels.isAvoidFirstLastClippingEnabled()) { // avoid clipping of the last if (i == mData.getXValCount() - 1) { float width = Utils.calcTextWidth(mXLabelPaint, label); if (width > getOffsetRight() * 2 && position[0] + width > getWidth()) position[0] -= width / 2; // avoid clipping of the first } else if (i == 0) { float width = Utils.calcTextWidth(mXLabelPaint, label); position[0] += width / 2; } } mDrawCanvas.drawText(label, position[0], yPos, mXLabelPaint); } } } /** * draws the y-axis labels to the screen */ private void drawYLabels() { if (!mDrawYLabels) return; float[] positions = new float[mYLabels.mEntryCount * 2]; for (int i = 0; i < positions.length; i += 2) { // only fill y values, x values are not needed since the y-labels // are // static on the x-axis positions[i + 1] = mYLabels.mEntries[i / 2]; } mTrans.pointValuesToPixel(positions); mYLabelPaint.setTypeface(mYLabels.getTypeface()); mYLabelPaint.setTextSize(mYLabels.getTextSize()); mYLabelPaint.setColor(mYLabels.getTextColor()); float xoffset = Utils.convertDpToPixel(5f); float yoffset = Utils.calcTextHeight(mYLabelPaint, "A") / 2.5f; // determine position and draw adequately if (mYLabels.getPosition() == YLabelPosition.LEFT) { mYLabelPaint.setTextAlign(Align.RIGHT); drawYLabels(mOffsetLeft - xoffset, positions, yoffset); } else if (mYLabels.getPosition() == YLabelPosition.RIGHT) { mYLabelPaint.setTextAlign(Align.LEFT); drawYLabels(getWidth() - mOffsetRight + xoffset, positions, yoffset); } else if (mYLabels.getPosition() == YLabelPosition.RIGHT_INSIDE) { mYLabelPaint.setTextAlign(Align.RIGHT); drawYLabels(getWidth() - mOffsetRight - xoffset, positions, yoffset); } else if (mYLabels.getPosition() == YLabelPosition.LEFT_INSIDE) { mYLabelPaint.setTextAlign(Align.LEFT); drawYLabels(mOffsetLeft + xoffset, positions, yoffset); } else { // BOTH SIDED Y-AXIS LABELS // draw left legend mYLabelPaint.setTextAlign(Align.RIGHT); drawYLabels(mOffsetLeft - xoffset, positions, yoffset); // draw right legend mYLabelPaint.setTextAlign(Align.LEFT); drawYLabels(getWidth() - mOffsetRight + xoffset, positions, yoffset); } } /** * draws the y-labels on the specified x-position * * @param xPos * @param positions */ private void drawYLabels(float xPos, float[] positions, float yOffset) { // draw for (int i = 0; i < mYLabels.mEntryCount; i++) { String text = mYLabels.getFormattedLabel(i); if (!mYLabels.isDrawTopYLabelEntryEnabled() && i >= mYLabels.mEntryCount - 1) return; if (mYLabels.isDrawUnitsInYLabelEnabled()) { mDrawCanvas.drawText(text + mUnit, xPos, positions[i * 2 + 1] + yOffset, mYLabelPaint); } else { mDrawCanvas.drawText(text, xPos, positions[i * 2 + 1] + yOffset, mYLabelPaint); } } } /** enums for all different border styles */ public enum BorderPosition { LEFT, RIGHT, TOP, BOTTOM } /** * array that holds positions where to draw the chart border lines */ private BorderPosition[] mBorderPositions = new BorderPosition[] { BorderPosition.BOTTOM }; /** * draws a line that surrounds the chart */ protected void drawBorder() { if (!mDrawBorder || mBorderPositions == null) return; for (int i = 0; i < mBorderPositions.length; i++) { if (mBorderPositions[i] == null) continue; switch (mBorderPositions[i]) { case LEFT: mDrawCanvas.drawLine(mOffsetLeft, mOffsetTop, mOffsetLeft, getHeight() - mOffsetBottom, mBorderPaint); break; case RIGHT: mDrawCanvas.drawLine(getWidth() - mOffsetRight, mOffsetTop, getWidth() - mOffsetRight, getHeight() - mOffsetBottom, mBorderPaint); break; case TOP: mDrawCanvas.drawLine(mOffsetLeft, mOffsetTop, getWidth() - mOffsetRight, mOffsetTop, mBorderPaint); break; case BOTTOM: mDrawCanvas.drawLine(mOffsetLeft, getHeight() - mOffsetBottom, getWidth() - mOffsetRight, getHeight() - mOffsetBottom, mBorderPaint); break; } } } /** * draws the grid background */ protected void drawGridBackground() { if (!mDrawGridBackground) return; Rect gridBackground = new Rect((int) mOffsetLeft + 1, (int) mOffsetTop + 1, getWidth() - (int) mOffsetRight, getHeight() - (int) mOffsetBottom); // draw the grid background mDrawCanvas.drawRect(gridBackground, mGridBackgroundPaint); } /** * draws the horizontal grid */ protected void drawHorizontalGrid() { if (!mDrawHorizontalGrid) return; // pre alloc float[] position = new float[2]; // draw the horizontal grid for (int i = 0; i < mYLabels.mEntryCount; i++) { position[1] = mYLabels.mEntries[i]; mTrans.pointValuesToPixel(position); mDrawCanvas.drawLine(mOffsetLeft, position[1], getWidth() - mOffsetRight, position[1], mGridPaint); } } /** * draws the vertical grid */ protected void drawVerticalGrid() { if (!mDrawVerticalGrid || mData == null) return; float[] position = new float[] { 0f, 0f }; for (int i = 0; i < mData.getXValCount(); i += mXLabels.mXAxisLabelModulus) { position[0] = i; mTrans.pointValuesToPixel(position); if (position[0] >= mOffsetLeft && position[0] <= getWidth()) { mDrawCanvas.drawLine(position[0], mOffsetTop, position[0], getHeight() - mOffsetBottom, mGridPaint); } } } /** * Draws the limit lines if there are one. */ private void drawLimitLines() { ArrayList<LimitLine> limitLines = mData.getLimitLines(); if (limitLines == null) return; float[] pts = new float[4]; for (int i = 0; i < limitLines.size(); i++) { LimitLine l = limitLines.get(i); pts[1] = l.getLimit(); pts[3] = l.getLimit(); mTrans.pointValuesToPixel(pts); pts[0] = 0; pts[2] = getWidth(); mLimitLinePaint.setColor(l.getLineColor()); mLimitLinePaint.setPathEffect(l.getDashPathEffect()); mLimitLinePaint.setStrokeWidth(l.getLineWidth()); mDrawCanvas.drawLines(pts, mLimitLinePaint); // if drawing the limit-value is enabled if (l.isDrawValueEnabled()) { PointF pos = getPosition(new Entry(l.getLimit(), 0)); // save text align Align align = mValuePaint.getTextAlign(); float xOffset = Utils.convertDpToPixel(4f); float yOffset = l.getLineWidth() + xOffset; String label = mValueFormatter.getFormattedValue(l.getLimit()); if (mDrawUnitInChart) label += mUnit; if (l.getLabelPosition() == LimitLabelPosition.RIGHT) { mValuePaint.setTextAlign(Align.RIGHT); mDrawCanvas.drawText(label, getWidth() - mOffsetRight - xOffset, pos.y - yOffset, mValuePaint); } else { mValuePaint.setTextAlign(Align.LEFT); mDrawCanvas.drawText(label, mOffsetLeft + xOffset, pos.y - yOffset, mValuePaint); } mValuePaint.setTextAlign(align); } } } /** * returns true if the specified point (x-axis) exceeds the limits of what * is visible to the right side * * @param v * @return */ protected boolean isOffContentRight(float p) { if (p > mContentRect.right) return true; else return false; } /** * returns true if the specified point (x-axis) exceeds the limits of what * is visible to the left side * * @param v * @return */ protected boolean isOffContentLeft(float p) { if (p < mContentRect.left) return true; else return false; } /** * returns true if the specified point (y-axis) exceeds the limits of what * is visible on the top * * @param v * @return */ protected boolean isOffContentTop(float p) { if (p < mContentRect.top) return true; else return false; } /** * returns true if the specified point (y-axis) exceeds the limits of what * is visible on the bottom * * @param v * @return */ protected boolean isOffContentBottom(float p) { if (p > mContentRect.bottom) return true; else return false; } /** touchlistener that handles touches and gestures on the chart */ protected OnTouchListener mListener; @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); if (mListener == null || mDataNotSet) return false; // check if touch gestures are enabled if (!mTouchEnabled) return false; else return mListener.onTouch(this, event); } /** * ################ ################ ################ ################ */ /** CODE BELOW THIS RELATED TO SCALING AND GESTURES */ /** * Zooms in by 1.4f, into the charts center. center. */ public void zoomIn() { Matrix save = mTrans.zoomIn(getWidth() / 2f, -(getHeight() / 2f)); mTrans.refresh(save, this); } /** * Zooms out by 0.7f, from the charts center. center. */ public void zoomOut() { Matrix save = mTrans.zoomOut(getWidth() / 2f, -(getHeight() / 2f)); mTrans.refresh(save, this); } /** * Zooms in or out by the given scale factor. x and y are the coordinates * (in pixels) of the zoom center. * * @param scaleX if < 1f --> zoom out, if > 1f --> zoom in * @param scaleY if < 1f --> zoom out, if > 1f --> zoom in * @param x * @param y */ public void zoom(float scaleX, float scaleY, float x, float y) { Matrix save = mTrans.zoom(scaleX, scaleY, x, -y); mTrans.refresh(save, this); } /** * Resets all zooming and dragging and makes the chart fit exactly it's * bounds. */ public void fitScreen() { Matrix save = mTrans.fitScreen(); mTrans.refresh(save, this); } /** * If this is set to true, the y-axis is inverted which means that low * values are on top of the chart, high values on bottom. * * @param enabled */ public void setInvertYAxisEnabled(boolean enabled) { mTrans.setInvertYAxisEnabled(enabled); } /** * If this returns true, the y-axis is inverted. * * @return */ public boolean isInvertYAxisEnabled() { return mTrans.isInvertYAxisEnabled(); } /** * Centers the viewport around the specified x-index and the specified * y-value in the chart. Centering the viewport outside the bounds of the * chart is not possible. Makes most sense in combination with the * setScaleMinima(...) method. First set the scale minima, then center the * viewport. SHOULD BE CALLED AFTER setting data for the chart. * * @param xIndex the index on the x-axis to center to * @param yVal the value ont he y-axis to center to */ public synchronized void centerViewPort(final int xIndex, final float yVal) { float indicesInView = mDeltaX / mTrans.getScaleX(); float valsInView = mDeltaY / mTrans.getScaleY(); Log.i(LOG_TAG, "indices: " + indicesInView + ", vals: " + valsInView); float[] pts = new float[] { xIndex - indicesInView / 2f, yVal + valsInView / 2f }; mTrans.centerViewPort(pts, this); } /** * ################ ################ ################ ################ */ /** CODE BELOW IS GETTERS AND SETTERS */ /** * set a new (e.g. custom) charttouchlistener NOTE: make sure to * setTouchEnabled(true); if you need touch gestures on the chart * * @param l */ public void setOnTouchListener(OnTouchListener l) { this.mListener = l; } /** * Sets the OnDrawListener * * @param drawListener */ public void setOnDrawListener(OnDrawListener drawListener) { this.mDrawListener = drawListener; } /** * Gets the OnDrawListener. May be null. * * @return */ public OnDrawListener getDrawListener() { return mDrawListener; } /** * Sets the minimum scale values for both axes. This limits the extent to * which the user can zoom-out. Scale 2f means the user cannot zoom out * further than 2x zoom, ... Min = 1f * * @param scaleXmin * @param scaleYmin */ public void setScaleMinima(float scaleXmin, float scaleYmin) { mTrans.setScaleMinima(scaleXmin, scaleYmin, this); } /** * Sets the effective range of y-values the chart can display. If this is * set, the y-range is fixed and cannot be changed. This means, no * recalculation of the bounds of the chart concerning the y-axis will be * done when adding new data. To disable this, provide Float.NaN as a * parameter or call resetYRange(); * * @param minY * @param maxY * @param invalidate if set to true, the chart will redraw itself after * calling this method */ public void setYRange(float minY, float maxY, boolean invalidate) { if (Float.isNaN(minY) || Float.isNaN(maxY)) { resetYRange(invalidate); return; } mFixedYValues = true; mYChartMin = minY; mYChartMax = maxY; if (minY < 0) { mStartAtZero = false; } mDeltaY = mYChartMax - mYChartMin; calcFormats(); prepareMatrix(); if (invalidate) invalidate(); } /** * Resets the previously set y range. If new data is added, the y-range will * be recalculated. * * @param invalidate if set to true, the chart will redraw itself after * calling this method */ public void resetYRange(boolean invalidate) { mFixedYValues = false; calcMinMax(mFixedYValues); prepareMatrix(); if (invalidate) invalidate(); } /** * if this returns true, the chart has a fixed range on the y-axis that is * not dependant on the actual data in the chart * * @return */ public boolean hasFixedYValues() { return mFixedYValues; } /** * Returns the position (in pixels) the provided Entry has inside the chart * view or null, if the provided Entry is null. * * @param e * @return */ public PointF getPosition(Entry e) { if (e == null) return null; float[] vals = new float[] { e.getXIndex(), e.getVal() }; if (this instanceof BarChart) { BarDataSet set = (BarDataSet) mData.getDataSetForEntry(e); if (set != null) vals[0] += set.getBarSpace() / 2f; } mTrans.pointValuesToPixel(vals); return new PointF(vals[0], vals[1]); } /** * sets the color for the grid lines * * @param color */ public void setGridColor(int color) { mGridPaint.setColor(color); } /** * sets the number of maximum visible drawn values on the chart only active * when setDrawValues() is enabled * * @param count */ public void setMaxVisibleValueCount(int count) { this.mMaxVisibleCount = count; } /** * If set to true, the highlight indicators (cross of two lines for * LineChart and ScatterChart, dark bar overlay for BarChart) that give * visual indication that an Entry has been selected will be drawn upon * selecting values. This does not depend on the MarkerView. Default: true * * @param enabled */ public void setHighlightIndicatorEnabled(boolean enabled) { mHighLightIndicatorEnabled = enabled; } /** * enable this to force the y-axis labels to always start at zero * * @param enabled */ public void setStartAtZero(boolean enabled) { this.mStartAtZero = enabled; prepare(); prepareMatrix(); } /** * returns true if the chart is set to start at zero, false otherwise * * @return */ public boolean isStartAtZeroEnabled() { return mStartAtZero; } /** * sets the width of the grid lines (min 0.1f, max = 3f) * * @param width */ public void setGridWidth(float width) { if (width < 0.1f) width = 0.1f; if (width > 3.0f) width = 3.0f; mGridWidth = width; } /** * Set this to true to enable dragging (moving the chart with the finger) * for the chart (this does not effect scaling). * * @param enabled */ public void setDragEnabled(boolean enabled) { this.mDragEnabled = enabled; } /** * Returns true if dragging is enabled for the chart, false if not. * * @return */ public boolean isDragEnabled() { return mDragEnabled; } /** * Set this to true to enable scaling (zooming in and out by gesture) for * the chart (this does not effect dragging). * * @param enabled */ public void setScaleEnabled(boolean enabled) { this.mScaleEnabled = enabled; } /** * Returns true if scaling (zooming in and out by gesture) is enabled for * the chart, false if not. * * @return */ public boolean isScaleEnabled() { return mScaleEnabled; } /** * Set this to true to enable zooming in by double-tap on the chart. * Default: enabled * * @param enabled */ public void setDoubleTapToZoomEnabled(boolean enabled) { mDoubleTapToZoomEnabled = enabled; } /** * Returns true if zooming via double-tap is enabled false if not. * * @return */ public boolean isDoubleTapToZoomEnabled() { return mDoubleTapToZoomEnabled; } /** * if set to true, the vertical grid will be drawn, default: true * * @param enabled */ public void setDrawVerticalGrid(boolean enabled) { mDrawVerticalGrid = enabled; } /** * if set to true, the horizontal grid will be drawn, default: true * * @param enabled */ public void setDrawHorizontalGrid(boolean enabled) { mDrawHorizontalGrid = enabled; } /** * returns true if drawing the vertical grid is enabled, false if not * * @return */ public boolean isDrawVerticalGridEnabled() { return mDrawVerticalGrid; } /** * returns true if drawing the horizontal grid is enabled, false if not * * @return */ public boolean isDrawHorizontalGridEnabled() { return mDrawHorizontalGrid; } /** * set this to true to draw the border surrounding the chart, default: true * * @param enabled */ public void setDrawBorder(boolean enabled) { mDrawBorder = enabled; } /** * set this to true to draw the grid background, false if not * * @param enabled */ public void setDrawGridBackground(boolean enabled) { mDrawGridBackground = enabled; } /** * set this to true to enable drawing the x-labels, false if not * * @param enabled */ public void setDrawXLabels(boolean enabled) { mDrawXLabels = enabled; } /** * set this to true to enable drawing the y-labels, false if not * * @param enabled */ public void setDrawYLabels(boolean enabled) { mDrawYLabels = enabled; } /** * Returns true if drawing y-labels is enabled, false if not. * * @return */ public boolean isDrawYLabelsEnabled() { return mDrawYLabels; } /** * Returns true if drawing x-labels is enabled, false if not. * * @return */ public boolean isDrawXLabelsEnabled() { return mDrawXLabels; } /** * Sets an array of positions where to draw the chart border lines (e.g. new * BorderStyle[] { BorderStyle.BOTTOM }) * * @param styles */ public void setBorderPositions(BorderPosition[] styles) { mBorderPositions = styles; } /** * Returns the array of positions where the chart-border is drawn. * * @return */ public BorderPosition[] getBorderPositions() { return mBorderPositions; } /** * Sets the width of the border surrounding the chart in dp. * * @param width */ public void setBorderWidth(int width) { mBorderPaint.setStrokeWidth(Utils.convertDpToPixel(width)); } /** * Sets the color of the border surrounding the chart. * * @param color */ public void setBorderColor(int color) { mBorderPaint.setColor(color); } /** * Returns the Highlight object (contains x-index and DataSet index) of the * selected value at the given touch point inside the Line-, Scatter-, or * CandleStick-Chart. * * @param x * @param y * @return */ public Highlight getHighlightByTouchPoint(float x, float y) { if (mDataNotSet || mData == null) { Log.e(LOG_TAG, "Can't select by touch. No data set."); return null; } // create an array of the touch-point float[] pts = new float[2]; pts[0] = x; pts[1] = y; mTrans.pixelsToValue(pts); double xTouchVal = pts[0]; double yTouchVal = pts[1]; double base = Math.floor(xTouchVal); double touchOffset = mDeltaX * 0.025; // Log.i(LOG_TAG, "touchindex x: " + xTouchVal + ", touchindex y: " + // yTouchVal + ", offset: " // + touchOffset); // Toast.makeText(getContext(), "touchindex x: " + xTouchVal + // ", touchindex y: " + yTouchVal + ", offset: " + touchOffset, // Toast.LENGTH_SHORT).show(); // touch out of chart if (xTouchVal < -touchOffset || xTouchVal > mDeltaX + touchOffset) return null; if (this instanceof CandleStickChart) base -= 0.5; if (base < 0) base = 0; if (base >= mDeltaX) base = mDeltaX - 1; int xIndex = (int) base; int dataSetIndex = 0; // index of the DataSet inside the ChartData // object // check if we are more than half of a x-value or not if (xTouchVal - base > 0.5) { xIndex = (int) base + 1; } ArrayList<SelInfo> valsAtIndex = getYValsAtIndex(xIndex); dataSetIndex = Utils.getClosestDataSetIndex(valsAtIndex, (float) yTouchVal); if (dataSetIndex == -1) return null; // Toast.makeText(getContext(), "xindex: " + xIndex + ", dataSetIndex: " // + dataSetIndex, // Toast.LENGTH_SHORT).show(); return new Highlight(xIndex, dataSetIndex); } /** * Returns the x and y values in the chart at the given touch point * (encapsulated in a PointD). This method transforms pixel coordinates to * coordinates / values in the chart. This is the opposite method to * getPixelsForValues(...). * * @param x * @param y * @return */ public PointD getValuesByTouchPoint(float x, float y) { // create an array of the touch-point float[] pts = new float[2]; pts[0] = x; pts[1] = y; mTrans.pixelsToValue(pts); double xTouchVal = pts[0]; double yTouchVal = pts[1]; return new PointD(xTouchVal, yTouchVal); } /** * Transforms the given chart values into pixels. This is the opposite * method to getValuesByTouchPoint(...). * * @param x * @param y * @return */ public PointD getPixelsForValues(float x, float y) { float[] pts = new float[] { x, y }; mTrans.pointValuesToPixel(pts); return new PointD(pts[0], pts[1]); } /** * returns the y-value at the given touch position (must not necessarily be * a value contained in one of the datasets) * * @param x * @param y * @return */ public float getYValueByTouchPoint(float x, float y) { return (float) getValuesByTouchPoint(x, y).y; } /** * returns the Entry object displayed at the touched position of the chart * * @param x * @param y * @return */ public Entry getEntryByTouchPoint(float x, float y) { Highlight h = getHighlightByTouchPoint(x, y); if (h != null) { return mData.getEntryForHighlight(h); } return null; } /** * returns the current x-scale factor */ public float getScaleX() { return mTrans.getScaleX(); } /** * returns the current y-scale factor */ public float getScaleY() { return mTrans.getScaleY(); } /** * if the chart is fully zoomed out, return true * * @return */ public boolean isFullyZoomedOut() { return mTrans.isFullyZoomedOut(); } /** * returns the object representing all y-labels, this method can be used to * acquire the YLabels object and modify it (e.g. change the position of the * labels) * * @return */ public YLabels getYLabels() { return mYLabels; } /** * returns the object representing all x-labels, this method can be used to * acquire the XLabels object and modify it (e.g. change the position of the * labels) * * @return */ public XLabels getXLabels() { return mXLabels; } /** * Enables data filtering for the chart data, filtering will use the user * customized Approximator handed over to this method. * * @param a */ public void enableFiltering(Approximator a) { mFilterData = true; // mApproximator = a; } /** * Disables data filtering for the chart. */ public void disableFiltering() { mFilterData = false; } /** * returns true if data filtering is enabled, false if not * * @return */ public boolean isFilteringEnabled() { return mFilterData; } /** * if set to true, both x and y axis can be scaled with 2 fingers, if false, * x and y axis can be scaled separately. default: false * * @param enabled */ public void setPinchZoom(boolean enabled) { mPinchZoomEnabled = enabled; } /** * returns true if pinch-zoom is enabled, false if not * * @return */ public boolean isPinchZoomEnabled() { return mPinchZoomEnabled; } /** * Set an offset in dp that allows the user to drag the chart over it's * bounds on the x-axis. * * @param offset */ public void setDragOffsetX(float offset) { mTrans.setDragOffsetX(offset); } /** * Set an offset in dp that allows the user to drag the chart over it's * bounds on the y-axis. * * @param offset */ public void setDragOffsetY(float offset) { mTrans.setDragOffsetY(offset); } /** * Returns true if both drag offsets (x and y) are zero or smaller. * * @return */ public boolean hasNoDragOffset() { return mTrans.hasNoDragOffset(); } /** * returns the filtered ChartData object depending on approximator settings, * current scale level and x- and y-axis ratio * * @return */ private T getFilteredData() { // // float deltaRatio = mDeltaY / mDeltaX; // float scaleRatio = mScaleY / mScaleX; // // // set the determined ratios // mApproximator.setRatios(deltaRatio, scaleRatio); // // // Log.i("Approximator", "DeltaRatio: " + deltaRatio + // ", ScaleRatio: " // // + scaleRatio); // // ArrayList<DataSet> dataSets = new ArrayList<DataSet>(); // // for (int j = 0; j < mOriginalData.getDataSetCount(); j++) { // // DataSet old = mOriginalData.getDataSetByIndex(j); // // // do the filtering // ArrayList<Entry> approximated = mApproximator.filter(old.getYVals()); // // DataSet set = new DataSet(approximated, old.getLabel()); // dataSets.add(set); // } // // ChartData d = new ChartData(mOriginalData.getXVals(), dataSets); // return d; return null; } @Override public void setPaint(Paint p, int which) { super.setPaint(p, which); switch (which) { case PAINT_GRID: mGridPaint = p; break; case PAINT_GRID_BACKGROUND: mGridBackgroundPaint = p; break; case PAINT_BORDER: mBorderPaint = p; break; } } @Override public Paint getPaint(int which) { Paint p = super.getPaint(which); if (p != null) return p; switch (which) { case PAINT_GRID: return mGridPaint; case PAINT_GRID_BACKGROUND: return mGridBackgroundPaint; case PAINT_BORDER: return mBorderPaint; } return null; } }