package com.jjoe64.graphview;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.RectF;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import com.jjoe64.graphview.compatible.ScaleGestureDetector;
/**
* GraphView is a Android View for creating zoomable and scrollable graphs.
* This is the abstract base class for all graphs. Extend this class and implement {@link #drawSeries(Canvas, GraphViewData[], float, float, float, double, double, double, double, float)} to display a custom graph.
* Use {@link LineGraphView} for creating a line chart.
*
* @author jjoe64 - jonas gehring - http://www.jjoe64.com
*
* Copyright (C) 2011 Jonas Gehring
* Licensed under the GNU Lesser General Public License (LGPL)
* http://www.gnu.org/licenses/lgpl.html
*/
abstract public class GraphView extends LinearLayout {
static final private class GraphViewConfig {
static final float BORDER = 20;
static final float VERTICAL_LABEL_WIDTH = 100;
static final float HORIZONTAL_LABEL_HEIGHT = 80;
}
private class GraphViewContentView extends View {
private float lastTouchEventX;
private float graphwidth;
/**
* @param context
*/
public GraphViewContentView(Context context) {
super(context);
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
}
/**
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
paint.setAntiAlias(true);
// normal
paint.setStrokeWidth(0);
float border = GraphViewConfig.BORDER;
float horstart = 0;
float height = getHeight();
float width = getWidth() - 1;
double maxY = getMaxY();
double minY = getMinY();
double diffY = maxY - minY;
double maxX = getMaxX(false);
double minX = getMinX(false);
double diffX = maxX - minX;
float graphheight = height - (2 * border);
graphwidth = width;
if (horlabels == null) {
horlabels = generateHorlabels(graphwidth);
}
if (verlabels == null) {
verlabels = generateVerlabels(graphheight);
}
// vertical lines
paint.setTextAlign(Align.LEFT);
int vers = verlabels.length - 1;
for (int i = 1; i < verlabels.length-1; i++) {
paint.setColor(Color.LTGRAY);
float y = ((graphheight / vers) * i) + border;
canvas.drawLine(horstart, y, width, y, paint);
}
// horizontal labels + lines
int hors = horlabels.length - 1;
for (int i = 0; i < horlabels.length; i++) {
paint.setColor(Color.LTGRAY);
float x = ((graphwidth / hors) * i) + horstart;
if (i > 0 && i < horlabels.length-1) {
canvas.drawLine(x, height - border, x, border, paint);
}
paint.setTextAlign(Align.CENTER);
if (i==horlabels.length-1)
paint.setTextAlign(Align.RIGHT);
if (i==0)
paint.setTextAlign(Align.LEFT);
paint.setColor(Color.DKGRAY);
canvas.drawText(horlabels[i], x, height - 4, paint);
}
paint.setTextAlign(Align.CENTER);
canvas.drawText(title, (graphwidth / 2) + horstart, border - 4, paint);
if (maxY != minY) {
paint.setStrokeCap(Paint.Cap.ROUND);
for (int i=0; i<graphSeries.size(); i++) {
paint.setStrokeWidth(graphSeries.get(i).style.thickness);
paint.setColor(graphSeries.get(i).style.color);
drawSeries(canvas, _values(i), graphwidth, graphheight, border, minX, minY, diffX, diffY, horstart);
}
if (showLegend) drawLegend(canvas, height, width);
}
}
private void onMoveGesture(float f) {
// view port update
if (viewportSize != 0) {
viewportStart -= f*viewportSize/graphwidth;
// minimal and maximal view limit
double minX = getMinX(true);
double maxX = getMaxX(true);
if (viewportStart < minX) {
viewportStart = minX;
} else if (viewportStart+viewportSize > maxX) {
viewportStart = maxX - viewportSize;
}
// labels have to be regenerated
horlabels = null;
verlabels = null;
viewVerLabels.invalidate();
}
invalidate();
}
/**
* @param event
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isScrollable()) {
return super.onTouchEvent(event);
}
boolean handled = false;
// first scale
if (scalable && scaleDetector != null) {
scaleDetector.onTouchEvent(event);
handled = scaleDetector.isInProgress();
}
if (!handled) {
// if not scaled, scroll
if ((event.getAction() & MotionEvent.ACTION_DOWN) == MotionEvent.ACTION_DOWN) {
handled = true;
}
if ((event.getAction() & MotionEvent.ACTION_UP) == MotionEvent.ACTION_UP) {
lastTouchEventX = 0;
handled = true;
}
if ((event.getAction() & MotionEvent.ACTION_MOVE) == MotionEvent.ACTION_MOVE) {
if (lastTouchEventX != 0) {
onMoveGesture(event.getX() - lastTouchEventX);
}
lastTouchEventX = event.getX();
handled = true;
}
if (handled)
invalidate();
}
if (handled) {
getParent().requestDisallowInterceptTouchEvent(true);
}
return handled;
}
}
/**
* one data set for a graph series
*/
static public class GraphViewData {
public final double valueX;
public final double valueY;
public GraphViewData(double valueX, double valueY) {
super();
this.valueX = valueX;
this.valueY = valueY;
}
public double[] getXY() {
double[] ret = new double[2];
ret[0] = valueX;
ret[1] = valueY;
return ret;
}
}
public enum LegendAlign {
TOP, MIDDLE, BOTTOM
}
private class VerLabelsView extends View {
/**
* @param context
*/
public VerLabelsView(Context context) {
super(context);
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT, 10));
}
/**
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
// normal
paint.setStrokeWidth(0);
float border = GraphViewConfig.BORDER;
float height = getHeight();
float graphheight = height - (2 * border);
if (verlabels == null) {
verlabels = generateVerlabels(graphheight);
}
// vertical labels
paint.setTextAlign(Align.LEFT);
int vers = verlabels.length - 1;
for (int i = 0; i < verlabels.length; i++) {
float y = ((graphheight / vers) * i) + border;
paint.setColor(Color.DKGRAY);
canvas.drawText(verlabels[i], 0, y, paint);
}
}
}
protected final Paint paint;
private String[] horlabels;
private String[] verlabels;
private String title;
private boolean scrollable;
private double viewportStart;
private double viewportSize;
private final View viewVerLabels;
private ScaleGestureDetector scaleDetector;
private boolean scalable;
private NumberFormat numberformatter;
private final List<GraphViewSeries> graphSeries;
private boolean showLegend = false;
private float legendWidth = 120;
private LegendAlign legendAlign = LegendAlign.MIDDLE;
private boolean manualYAxis;
private double manualMaxYValue;
private double manualMinYValue;
/**
*
* @param context
* @param title [optional]
*/
public GraphView(Context context, String title) {
super(context);
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
if (title == null)
title = "";
else
this.title = title;
paint = new Paint();
graphSeries = new ArrayList<GraphViewSeries>();
viewVerLabels = new VerLabelsView(context);
addView(viewVerLabels);
addView(new GraphViewContentView(context), new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT, 1));
}
private GraphViewData[] _values(int idxSeries) {
GraphViewData[] values = graphSeries.get(idxSeries).values;
if (viewportStart == 0 && viewportSize == 0) {
// all data
return values;
} else {
// viewport
List<GraphViewData> listData = new ArrayList<GraphViewData>();
for (int i=0; i<values.length; i++) {
if (values[i].valueX >= viewportStart) {
if (values[i].valueX > viewportStart+viewportSize) {
listData.add(values[i]); // one more for nice scrolling
break;
} else {
listData.add(values[i]);
}
} else {
if (listData.isEmpty()) {
listData.add(values[i]);
}
listData.set(0, values[i]); // one before, for nice scrolling
}
}
return listData.toArray(new GraphViewData[listData.size()]);
}
}
public void addSeries(GraphViewSeries series) {
series.addGraphView(this);
graphSeries.add(series);
}
protected void drawLegend(Canvas canvas, float height, float width) {
int shapeSize = 15;
int[] paints = new int[graphSeries.size()];
int[] legendgraphs = new int[graphSeries.size()];
for (int i = 0; i < paints.length; i++) {
paints[i] = -1;
legendgraphs[i] = -1;
}
int numpaints = 0;
boolean breakout;
for (int j = 0; j < graphSeries.size(); j++) {
breakout = false;
for (int i = 0; i < paints.length; i++) {
if (paints[i] == graphSeries.get(j).style.color) {
breakout = true;
break;
}
}
if (!breakout) {
paints[numpaints] = graphSeries.get(j).style.color;
legendgraphs[numpaints] = j;
numpaints++;
}
}
// rect
paint.setARGB(0, 0, 0, 0);
float legendHeight = (shapeSize+5)*numpaints +5;
float lLeft = width-legendWidth - 10;
float lTop;
switch (legendAlign) {
case TOP:
lTop = 10;
break;
case MIDDLE:
lTop = height/2 - legendHeight/2;
break;
default:
lTop = height - GraphViewConfig.BORDER - legendHeight -10;
}
float lRight = lLeft+legendWidth;
float lBottom = lTop+legendHeight;
canvas.drawRoundRect(new RectF(lLeft, lTop, lRight, lBottom), 8, 8, paint);
for (int i=0; i<numpaints; i++) {
paint.setColor(graphSeries.get(legendgraphs[i]).style.color);
canvas.drawRect(new RectF(lLeft+5, lTop+5+(i*(shapeSize+5)), lLeft+5+shapeSize, lTop+((i+1)*(shapeSize+5))), paint);
if (graphSeries.get(i).description != null) {
paint.setColor(Color.BLACK);
paint.setTextAlign(Align.LEFT);
canvas.drawText(graphSeries.get(legendgraphs[i]).description, lLeft+5+shapeSize+5, lTop+shapeSize+(i*(shapeSize+5)), paint);
}
}
}
abstract public void drawSeries(Canvas canvas, GraphViewData[] values, float graphwidth, float graphheight, float border, double minX, double minY, double diffX, double diffY, float horstart);
/**
* formats the label
* can be overwritten
* @param value x and y values
* @param isValueX if false, value y wants to be formatted
* @return value to display
*/
protected String formatLabel(double value, boolean isValueX) {
if (numberformatter == null) {
numberformatter = NumberFormat.getNumberInstance();
double highestvalue = getMaxY();
double lowestvalue = getMinY();
if (highestvalue - lowestvalue < 0.1) {
numberformatter.setMaximumFractionDigits(6);
} else if (highestvalue - lowestvalue < 1) {
numberformatter.setMaximumFractionDigits(4);
} else if (highestvalue - lowestvalue < 20) {
numberformatter.setMaximumFractionDigits(3);
} else if (highestvalue - lowestvalue < 100) {
numberformatter.setMaximumFractionDigits(1);
} else {
numberformatter.setMaximumFractionDigits(0);
}
}
return numberformatter.format(value);
}
private String[] generateHorlabels(float graphwidth) {
int numLabels = (int) (graphwidth/GraphViewConfig.VERTICAL_LABEL_WIDTH);
String[] labels = new String[numLabels+1];
double min = getMinX(false);
double max = getMaxX(false);
for (int i=0; i<=numLabels; i++) {
labels[i] = formatLabel(min + ((max-min)*i/numLabels), true);
}
return labels;
}
synchronized private String[] generateVerlabels(float graphheight) {
int numLabels = (int) (graphheight/GraphViewConfig.HORIZONTAL_LABEL_HEIGHT);
String[] labels = new String[numLabels+1];
double min = getMinY();
double max = getMaxY();
for (int i=0; i<=numLabels; i++) {
labels[numLabels-i] = formatLabel(min + ((max-min)*i/numLabels), false);
}
return labels;
}
public LegendAlign getLegendAlign() {
return legendAlign;
}
public float getLegendWidth() {
return legendWidth;
}
/**
* returns the maximal X value of the current viewport (if viewport is set)
* otherwise maximal X value of all data.
* @param ignoreViewport
*
* warning: only override this, if you really know want you're doing!
*/
protected double getMaxX(boolean ignoreViewport) {
// if viewport is set, use this
if (!ignoreViewport && viewportSize != 0) {
return viewportStart+viewportSize;
} else {
// otherwise use the max x value
// values must be sorted by x, so the last value has the largest X value
double highest = 0;
if (graphSeries.size() > 0)
{
GraphViewData[] values = graphSeries.get(0).values;
highest = values[values.length-1].valueX;
for (int i=1; i<graphSeries.size(); i++) {
values = graphSeries.get(i).values;
highest = Math.max(highest, values[values.length-1].valueX);
}
}
return highest;
}
}
/**
* returns the maximal Y value of all data.
*
* warning: only override this, if you really know want you're doing!
*/
protected double getMaxY() {
double largest;
if (manualYAxis) {
largest = manualMaxYValue;
} else {
largest = Integer.MIN_VALUE;
for (int i=0; i<graphSeries.size(); i++) {
GraphViewData[] values = _values(i);
for (int ii=0; ii<values.length; ii++)
if (values[ii].valueY > largest)
largest = values[ii].valueY;
}
}
return largest;
}
/**
* returns the minimal X value of the current viewport (if viewport is set)
* otherwise minimal X value of all data.
* @param ignoreViewport
*
* warning: only override this, if you really know want you're doing!
*/
protected double getMinX(boolean ignoreViewport) {
// if viewport is set, use this
if (!ignoreViewport && viewportSize != 0) {
return viewportStart;
} else {
// otherwise use the min x value
// values must be sorted by x, so the first value has the smallest X value
double lowest = 0;
if (graphSeries.size() > 0)
{
GraphViewData[] values = graphSeries.get(0).values;
lowest = values[0].valueX;
for (int i=1; i<graphSeries.size(); i++) {
values = graphSeries.get(i).values;
lowest = Math.min(lowest, values[0].valueX);
}
}
return lowest;
}
}
/**
* returns the minimal Y value of all data.
*
* warning: only override this, if you really know want you're doing!
*/
protected double getMinY() {
double smallest;
if (manualYAxis) {
smallest = manualMinYValue;
} else {
smallest = Integer.MAX_VALUE;
for (int i=0; i<graphSeries.size(); i++) {
GraphViewData[] values = _values(i);
for (int ii=0; ii<values.length; ii++)
if (values[ii].valueY < smallest)
smallest = values[ii].valueY;
}
}
return smallest;
}
public boolean isScrollable() {
return scrollable;
}
public boolean isShowLegend() {
return showLegend;
}
public void redrawAll() {
verlabels = null;
horlabels = null;
numberformatter = null;
invalidate();
viewVerLabels.invalidate();
}
public void removeSeries(GraphViewSeries series)
{
graphSeries.remove(series);
}
public void removeSeries(int index)
{
if (index < 0 || index >= graphSeries.size())
{
throw new IndexOutOfBoundsException("No series at index " + index);
}
graphSeries.remove(index);
}
public void scrollToEnd() {
if (!scrollable) throw new IllegalStateException("This GraphView is not scrollable.");
double max = getMaxX(true);
viewportStart = max-viewportSize;
redrawAll();
}
/**
* set's static horizontal labels (from left to right)
* @param horlabels if null, labels were generated automatically
*/
public void setHorizontalLabels(String[] horlabels) {
this.horlabels = horlabels;
}
public void setLegendAlign(LegendAlign legendAlign) {
this.legendAlign = legendAlign;
}
public void setLegendWidth(float legendWidth) {
this.legendWidth = legendWidth;
}
/**
* you have to set the bounds {@link #setManualYAxisBounds(double, double)}. That automatically enables manualYAxis-flag.
* if you want to disable the menual y axis, call this method with false.
* @param manualYAxis
*/
public void setManualYAxis(boolean manualYAxis) {
this.manualYAxis = manualYAxis;
}
/**
* set manual Y axis limit
* @param max
* @param min
*/
public void setManualYAxisBounds(double max, double min) {
manualMaxYValue = max;
manualMinYValue = min;
manualYAxis = true;
}
/**
* this forces scrollable = true
* @param scalable
*/
synchronized public void setScalable(boolean scalable) {
this.scalable = scalable;
if (scalable == true && scaleDetector == null) {
scrollable = true; // automatically forces this
scaleDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
double center = viewportStart + viewportSize / 2;
viewportSize /= detector.getScaleFactor();
viewportStart = center - viewportSize / 2;
// viewportStart must not be < minX
double minX = getMinX(true);
if (viewportStart < minX) {
viewportStart = minX;
}
// viewportStart + viewportSize must not be > maxX
double maxX = getMaxX(true);
double overlap = viewportStart + viewportSize - maxX;
if (overlap > 0) {
// scroll left
if (viewportStart-overlap > minX) {
viewportStart -= overlap;
} else {
// maximal scale
viewportStart = minX;
viewportSize = maxX - viewportStart;
}
}
redrawAll();
return true;
}
});
}
}
/**
* the user can scroll (horizontal) the graph. This is only useful if you use a viewport {@link #setViewPort(double, double)} which doesn't displays all data.
* @param scrollable
*/
public void setScrollable(boolean scrollable) {
this.scrollable = scrollable;
}
public void setShowLegend(boolean showLegend) {
this.showLegend = showLegend;
}
/**
* set's static vertical labels (from top to bottom)
* @param verlabels if null, labels were generated automatically
*/
public void setVerticalLabels(String[] verlabels) {
this.verlabels = verlabels;
}
/**
* set's the viewport for the graph.
* @param start x-value
* @param size
*/
public void setViewPort(double start, double size) {
viewportStart = start;
viewportSize = size;
}
}