blob: 99e7b6e683a2a2a16c571891f36f5561232bc177 [file] [log] [blame]
package com.github.mikephil.charting;
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.Path;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Log;
import java.text.DecimalFormat;
import java.util.ArrayList;
/**
* Baseclass of all LineChart and BarChart.
*
* @author Philipp Jahoda
*/
public abstract class BarLineChartBase extends Chart {
/**
* string that is drawn next to the values in the chart, indicating their
* unit
*/
protected String mUnit = "";
/** the maximum number of entried to which values will be drawn */
protected int mMaxVisibleCount = 100;
/**
* width of the x-legend in pixels - this is calculated by the
* calcTextWidth() method
*/
protected int mXLegendWidth = 1;
/**
* the modulus that indicates if a value at a specified index in an
* array(list) is drawn or not. If index % modulus == 0 DRAW, else dont
* draw.
*/
protected int mLegendGridModulus = 1;
/**
* indicates how many digits should be used for the y-legend, -1 means
* automatically calculate
*/
protected int mYLegendDigitsToUse = -1;
/** the number of y-legend entries the chart has */
protected int mYLegendCount = 9;
/** the width of the grid lines */
protected float mGridWidth = 1f;
/** array that contains all values of the y-axis legend */
protected Float[] mYLegend = new Float[mYLegendCount];
/** if true, units are drawn next to the values in the chart */
protected boolean mDrawUnitInChart = false;
/**
* if true, units are drawn next to the values in the legend
*/
protected boolean mDrawUnitInLegend = true;
/** if true, x-legend text is centered */
protected boolean mCenterXLegendText = false;
/**
* if set to true, the x-legend entries will adjust themselves when scaling
* the graph
*/
protected boolean mAdjustXLegend = true;
/** if true, the grid will be drawn, otherwise not, default true */
protected boolean mDrawGrid = true;
/** if true, dragging / scaling is enabled for the chart */
protected boolean mDragEnabled = true;
/**
* if true, the y-legend values will be rounded - the legend entry count
* will only be approximated
*/
protected boolean mRoundedYLegend = false;
/** if true, the y-legend will always start at zero */
protected boolean mStartAtZero = true;
/** 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 mOutLinePaint;
/** paint for the x-legend values */
protected Paint mXLegendPaint;
/** paint for the y-legend values */
protected Paint mYLegendPaint;
/** paint used for highlighting values */
protected Paint mHighlightPaint;
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, mMatrixTouch);
mXLegendPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mXLegendPaint.setColor(Color.BLACK);
mXLegendPaint.setTextAlign(Align.CENTER);
mXLegendPaint.setTextSize(Utils.convertDpToPixel(10f));
mYLegendPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mYLegendPaint.setColor(Color.BLACK);
mYLegendPaint.setTextAlign(Align.RIGHT);
mYLegendPaint.setTextSize(Utils.convertDpToPixel(10f));
mGridPaint = new Paint();
mGridPaint.setColor(Color.GRAY);
mGridPaint.setStrokeWidth(mGridWidth);
mGridPaint.setStyle(Style.STROKE);
mGridPaint.setAlpha(90);
mOutLinePaint = new Paint();
mOutLinePaint.setColor(Color.BLACK);
mOutLinePaint.setStrokeWidth(mGridWidth * 2f);
mOutLinePaint.setStyle(Style.STROKE);
mGridBackgroundPaint = new Paint();
mGridBackgroundPaint.setStyle(Style.FILL);
// mGridBackgroundPaint.setColor(Color.WHITE);
mGridBackgroundPaint.setColor(Color.rgb(240, 240, 240)); // light
// grey
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 onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDataNotSet)
return;
long starttime = System.currentTimeMillis();
if (mAdjustXLegend)
calcModulus();
// execute all drawing commands
drawOutline();
drawGridBackground();
// make sure the graph values and grid cannot be drawn outside the
// content-rect
int clipRestoreCount = mDrawCanvas.save();
mDrawCanvas.clipRect(mContentRect);
drawHorizontalGrid();
drawVerticalGrid();
drawData();
drawHighlights();
drawAdditional();
drawValues();
// Removes clipping rectangle
mDrawCanvas.restoreToCount(clipRestoreCount);
drawXLegend();
drawYLegend();
drawMarkerView();
drawDescription();
canvas.drawBitmap(mDrawBitmap, 0, 0, mDrawPaint);
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
protected void prepare() {
if (mDataNotSet)
return;
calcMinMax();
prepareXLegend();
prepareYLegend();
// calculate how many digits are needed
calcFormats();
prepareMatrix();
Log.i(LOG_TAG, "xVals: " + mXVals.size() + ", yVals: " + mYVals.size());
}
/**
* calculates the modulus for legend and grid
*/
protected void calcModulus() {
float[] values = new float[9];
mMatrixTouch.getValues(values);
mLegendGridModulus = (int) Math.ceil((mXVals.size() * mXLegendWidth)
/ (mContentRect.width() * values[Matrix.MSCALE_X]));
}
/** the decimalformat responsible for formatting the y-legend */
protected DecimalFormat mFormatYLegend = null;
/** the decimalformat responsible for formatting the values in the chart */
protected DecimalFormat mFormatValue = null;
/** the number of digits the y-legend is formatted with */
protected int mYLegendFormatDigits = -1;
/**
* calculates the required number of digits for the y-legend and for the
* values that might be drawn in the chart (if enabled)
*/
protected void calcFormats() {
int bonus = 0;
if (!mRoundedYLegend)
bonus++;
// calcualte the step between the legend entries and get the needed
// digits, -1 means calculate automatically, else the set value is used
if (mYLegendDigitsToUse == -1)
mYLegendFormatDigits = Utils.getLegendFormatDigits(mYLegend[2] - mYLegend[1], bonus);
else
mYLegendFormatDigits = mYLegendDigitsToUse;
if (mValueDigitsToUse == -1)
mValueFormatDigits = Utils.getFormatDigits(mDeltaY);
else
mValueFormatDigits = mValueDigitsToUse;
StringBuffer a = new StringBuffer();
for (int i = 0; i < mYLegendFormatDigits; i++) {
if (i == 0)
a.append(".");
a.append("0");
}
mFormatYLegend = new DecimalFormat("###,###,###,##0" + a.toString());
StringBuffer b = new StringBuffer();
for (int i = 0; i < mValueFormatDigits; i++) {
if (i == 0)
b.append(".");
b.append("0");
}
mFormatValue = new DecimalFormat("###,###,###,##0" + b.toString());
}
@Override
protected void calcMinMax() {
super.calcMinMax(); // calc min and max in the super class
// additional handling for space (default 10% space)
float spaceTop = (mYMax - mYChartMin) / 100f * 10f;
if (mStartAtZero) {
mYChartMin = 0;
} else {
float spaceBottom = (mYMax - mYChartMin) / 100f * 10f;
mYChartMin = mYChartMin - spaceBottom;
}
// calc delta
mDeltaY = (mYMax + spaceTop) - mYChartMin;
mYChartMax = mYChartMin + mDeltaY;
Log.i(LOG_TAG, "DeltaX: " + mDeltaX + ", DeltaY: " + mDeltaY);
}
/**
* setup the X legend
*/
protected void prepareXLegend() {
StringBuffer a = new StringBuffer();
int length = (int) (((float) (mXVals.get(0).length() + mXVals.get(mXVals.size() - 1)
.length())));
if (mXVals.get(0).length() <= 3)
length *= 2;
// else if(mXVals.get(0).length() <= 5) length *= 1;
// else if(mXVals.get(0).length() <= 7) length = (int) (length / 1.5f);
// else if(mXVals.get(0).length() <= 9) length = (int) (length / 2.5f);
// else if(mXVals.get(0).length() > 9) length = (int) (length / 4f);
for (int i = 0; i < length; i++) {
a.append("h");
}
mXLegendWidth = calcTextWidth(mXLegendPaint, a.toString());
}
/**
* Math.pow(...) is very expensive, so avoid calling it and create it
* yourself
*/
protected static final double POW_10[] = {
0.00001, 0.0001, 0.001, 0.01, 0.1,
1, 10, 100, 1000, 10000, 100000,
1000000, 10000000, 100000000, 1000000000
// maximum = 1 billion
};
/**
* setup the Y legend
*/
protected void prepareYLegend() {
ArrayList<Float> yLegend = new ArrayList<Float>();
// if the legend is supposed to be rounded
if (mRoundedYLegend) {
// float interval = (mYMax+mYMin) / (Y_LEGEND_COUNT - 1);
float interval = (mDeltaY) / (mYLegendCount - 1);
double log10 = Math.log10(interval);
int exp = (int) Math.floor(log10);
double tenPowExp = POW_10[exp + 5]; // +5 because the POW array has
// its "1" at index 5
double multi = Math.round(interval / tenPowExp);
if (multi >= 1) {
if (multi > 2 && multi < 5) {
multi = 5;
} else if (multi > 5 && multi < 10) {
multi = 10;
}
}
float step = (float) (multi * tenPowExp);
float val = 0;
if (step >= 1f)
val = (int) (mYChartMin / step) * step;
else {
val = mYChartMin;
}
// float val = mYMin % step * (int) (mYMin / step);
// create the y-legend values in the calculated step size
while (val <= mDeltaY + step + mYChartMin) {
yLegend.add(val);
val = val + step;
}
if (step >= 1f)
mYChartMin = (int) (mYChartMin / step) * step;
// set the new delta adequate to the last y-legend value
mDeltaY = val - step - mYChartMin;
mYChartMax = yLegend.get(yLegend.size() - 1);
} else {
float interval = (mDeltaY) / (mYLegendCount - 1);
yLegend.add(mYChartMin);
for (int i = 1; i < mYLegendCount - 1; i++) {
yLegend.add(mYChartMin + ((float) i * interval));
}
yLegend.add(mDeltaY + mYChartMin);
}
// convert the list to an array
mYLegend = yLegend.toArray(new Float[0]);
}
/**
* draws the x legend to the screen
*/
protected void drawXLegend() {
// pre allocate to save performance (dont allocate in loop)
float[] position = new float[] {
0f, 0f
};
for (int i = 0; i < mXVals.size(); i++) {
if (i % mLegendGridModulus == 0) {
position[0] = i;
// center the text
if (mCenterXLegendText)
position[0] += 0.5f;
transformPointArray(position);
if (position[0] >= mOffsetLeft && position[0] <= getWidth() - mOffsetRight + 10) {
// Rect rect = new Rect();
// mXLegendPaint.getTextBounds(mXVals.get(i), 0,
// mXVals.get(i).length(), rect);
//
// float toRight = rect.width() / 2;
//
// // make sure
// if(i == 0) toRight = rect.width();
mDrawCanvas.drawText(mXVals.get(i), position[0], mOffsetTop - 5,
mXLegendPaint);
}
}
}
}
/**
* draws the y legend to the screen
*/
protected void drawYLegend() {
float[] positions = new float[mYLegend.length * 2];
for (int i = 0; i < positions.length; i += 2) {
positions[i] = 0;
positions[i + 1] = mYLegend[i / 2];
}
// y-axis cannot be scaled, therefore transform without the touch matrix
transformPointArrayNoTouch(positions);
for (int i = 0; i < positions.length; i += 2) {
if (mYLegend[i / 2] != null) {
float yPos = positions[i + 1] + 8;
// lower the position of the top y-legend entry so that it does
// not interfear with the x-legend
if (i >= positions.length - 2)
yPos = positions[i + 1] + 14;
if (!mDrawTopYLegendEntry && i >= positions.length - 2)
return;
if(mDrawUnitInLegend) {
mDrawCanvas.drawText(mFormatYLegend.format(mYLegend[i / 2]) + mUnit,
positions[i] - 10,
yPos, mYLegendPaint);
} else {
mDrawCanvas.drawText(mFormatYLegend.format(mYLegend[i / 2]),
positions[i] - 10,
yPos, mYLegendPaint);
}
}
}
}
/** indicates if the top y-legend entry is drawn or not */
private boolean mDrawTopYLegendEntry = true;
/**
* set this to true to enable drawing the top y-legend entry. Disabling this
* can be helpful when the y-legend and x-legend interfear with each other.
* default: true
*
* @param enabled
*/
public void setDrawTopYLegendEntry(boolean enabled) {
mDrawTopYLegendEntry = enabled;
}
/**
* draws a line that surrounds the chart
*/
protected void drawOutline() {
mDrawCanvas.drawRect(mOffsetLeft, mOffsetTop, getWidth() - mOffsetRight, getHeight()
- mOffsetBottom, mOutLinePaint);
}
/**
* draws the grid background
*/
protected void drawGridBackground() {
Rect gridBackground = new Rect(mOffsetLeft, mOffsetTop, getWidth() - mOffsetRight,
getHeight() - mOffsetBottom);
// draw the grid background
mDrawCanvas.drawRect(gridBackground, mGridBackgroundPaint);
}
/**
* draws the horizontal grid
*/
protected void drawHorizontalGrid() {
if (!mDrawGrid)
return;
// create a new path object only once and use reset() instead of
// unnecessary allocations
Path p = new Path();
// draw the horizontal grid
for (int i = 0; i < mYLegend.length; i++) {
p.reset();
p.moveTo(0, mYLegend[i]);
p.lineTo(mDeltaX, mYLegend[i]);
transformPath(p);
mDrawCanvas.drawPath(p, mGridPaint);
}
}
/**
* draws the vertical grid
*/
protected void drawVerticalGrid() {
if (!mDrawGrid)
return;
float[] position = new float[] {
0f, 0f
};
for (int i = 0; i < mXVals.size(); i++) {
if (i % mLegendGridModulus == 0) {
position[0] = i;
transformPointArray(position);
if (position[0] >= mOffsetLeft && position[0] <= getWidth()) {
mDrawCanvas.drawLine(position[0], mOffsetTop, position[0], getHeight()
- mOffsetBottom, mGridPaint);
}
}
}
}
/**
* determines how much space (in percent of the total range) is left between
* the loweset value of the chart and its bottom (bottomSpace) and the
* highest value of the chart and its top (topSpace) recommended: 3-25 %
* NOTE: the bottomSpace will only apply if "startAtZero" is set to false
*
* @param bottomSpace
* @param topSpace
*/
// public void setSpacePercent(int bottomSpace, int topSpace) {
// mYSpacePercentBottom = bottomSpace;
// mYSpacePercentTop = topSpace;
// }
/**
* sets the effective range of y-values the chart can display
*
* @param minY
* @param maxY
*/
public void setYRange(float minY, float maxY) {
mYChartMin = minY;
mYChartMax = maxY;
mDeltaY = mYChartMax - mYChartMin;
prepareMatrix();
prepareYLegend();
}
/**
* sets the number of legend entries for the y-legend max = 15, min = 3
*
* @param yCount
*/
public void setYLegendCount(int yCount) {
if (yCount > 15)
yCount = 15;
if (yCount < 3)
yCount = 3;
mYLegendCount = yCount;
mYLegend = new Float[mYLegendCount];
}
/**
* if set to true, the x-legend entries will adjust themselves when scaling
* the graph default: true
*
* @param enabled
*/
public void setAdjustXLegend(boolean enabled) {
mAdjustXLegend = enabled;
}
/**
* returns true if the x-legend adjusts itself when scaling the graph, false
* if not
*
* @return
*/
public boolean isAdjustXLegendEnabled() {
return mAdjustXLegend;
}
/**
* 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;
}
/**
* sets the size of the y-legend text in pixels min = 7f, max = 14f
*
* @param size
*/
public void setYLegendTextSize(float size) {
if (size > 14f)
size = 14f;
if (size < 7f)
size = 7f;
mYLegendPaint.setTextSize(Utils.convertDpToPixel(size));
}
/**
* sets the size of the x-legend text in pixels min = 7f, max = 14f
*
* @param size
*/
public void setXLegendTextSize(float size) {
if (size > 14f)
size = 14f;
if (size < 7f)
size = 7f;
mXLegendPaint.setTextSize(Utils.convertDpToPixel(size));
}
/**
* returns true if drawing the grid is enabled, false if not
*
* @return
*/
public boolean isDrawGridEnabled() {
return mDrawGrid;
}
/**
* set to true if the grid should be drawn, false if not
*
* @param enabled
*/
public void setDrawGrid(boolean enabled) {
this.mDrawGrid = enabled;
}
/**
* enable this to force the y-legend to always start at zero
*
* @param enabled
*/
public void setStartAtZero(boolean enabled) {
this.mStartAtZero = enabled;
prepare();
}
/**
* set to true to round all y-legend values if true, the y-legend values
* will be rounded - the y-legend entry count will only be approximated this
* way
*
* @param enabled
*/
public void setRoundedYLegend(boolean enabled) {
this.mRoundedYLegend = enabled;
prepare();
}
/**
* sets the unit that is drawn next to the values in the chart, e.g. %
*
* @param unit
*/
public void setUnit(String unit) {
mUnit = unit;
}
/**
* returns true if the chart is set to start at zero, false otherwise
*
* @return
*/
public boolean isStartAtZeroEnabled() {
return mStartAtZero;
}
/**
* if set to true, units are drawn next to values in the chart, default:
* false
*
* @param enabled
*/
public void setDrawUnitsInChart(boolean enabled) {
mDrawUnitInChart = enabled;
}
/**
* if set to true, units are drawn next to y-legend values, default: true
*
* @param enabled
*/
public void setDrawUnitsInLegend(boolean enabled) {
mDrawUnitInLegend = enabled;
}
/**
* returns true if the y-legend is set to be rounded, false if not
*
* @return
*/
public boolean isYLegendRounded() {
return mRoundedYLegend;
}
/**
* set this to true to center the x-legend text, default: false
*
* @param enabled
*/
public void setCenterXLegend(boolean enabled) {
mCenterXLegendText = enabled;
}
/**
* returns true if the x-legend text is centered
*
* @return
*/
public boolean isXLegendCentered() {
return mCenterXLegendText;
}
/**
* 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 / scaling for the chart
*
* @param enabled
*/
public void setDragEnabled(boolean enabled) {
this.mDragEnabled = enabled;
}
/**
* returns true if dragging / scaling is enabled for the chart, false if not
*
* @return
*/
public boolean isDragEnabled() {
return mDragEnabled;
}
/**
* returns the index of the value (on the x-axis) for the given touch point
*
* @param x
* @param y
* @return
*/
public int getIndexByTouchPoint(float x, float y) {
// create an array of the touch-point
float[] pts = new float[2];
pts[0] = x;
pts[1] = y;
Matrix tmp = new Matrix();
// invert all matrixes to convert back to the original value
mMatrixOffset.invert(tmp);
tmp.mapPoints(pts);
mMatrixTouch.invert(tmp);
tmp.mapPoints(pts);
mMatrixValueToPx.invert(tmp);
tmp.mapPoints(pts);
double touchPointIndex = pts[0];
double base = Math.floor(touchPointIndex);
// touch out of chart
if(touchPointIndex < 0 || touchPointIndex > getValueCount()-1) return -1;
int index = (int) base;
if (this instanceof LineChart) {
// check if we are more than half of a x-value or not
if (touchPointIndex - base > 0.5) {
index = (int) base + 1;
}
}
return index;
}
/**
* returns the value displayed at the touched position of the chart
*
* @param x
* @param y
* @return
*/
public Series getSeriesByTouchPoint(float x, float y) {
return mYVals.get(getIndexByTouchPoint(x, y));
}
/**
* sets a typeface for the paint object of the x-legend
*
* @param t
*/
public void setXLegendTypeface(Typeface t) {
mXLegendPaint.setTypeface(t);
}
/**
* sets a typeface for the paint object of the y-legend
*
* @param t
*/
public void setYLegendTypeface(Typeface t) {
mYLegendPaint.setTypeface(t);
}
/**
* sets a typeface for both x and y-legend paints
*
* @param t
*/
public void setLegendTypeface(Typeface t) {
setXLegendTypeface(t);
setYLegendTypeface(t);
}
/**
* sets the number of y-legend digits to use, if set to -1, digits will be
* automatically calculated
*
* @param digits
*/
public void setLegendDigits(int digits) {
mYLegendDigitsToUse = digits;
}
/**
* returns the number of y-legend digits that is set to use, -1 means auto
* calculate
*
* @return
*/
public int getLegendDigits() {
return mYLegendDigitsToUse;
}
@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_OUTLINE:
mOutLinePaint = p;
break;
case PAINT_XLEGEND:
mXLegendPaint = p;
break;
case PAINT_YLEGEND:
mYLegendPaint = p;
break;
}
}
}