GraphView 4.0.1 as a module (no support library)

This commit is contained in:
Milos Kozak 2022-10-26 14:13:08 +02:00
parent 38e12b845b
commit 6e33d354c7
31 changed files with 6131 additions and 3 deletions

View file

@ -170,6 +170,7 @@ dependencies {
wearApp project(':wear') wearApp project(':wear')
implementation project(':iconify') implementation project(':iconify')
implementation project(':graphview')
implementation project(':shared') implementation project(':shared')
implementation project(':core') implementation project(':core')
implementation project(':automation') implementation project(':automation')

View file

@ -17,4 +17,5 @@ dependencies {
implementation project(':core') implementation project(':core')
implementation project(':database') implementation project(':database')
implementation project(':shared') implementation project(':shared')
implementation project(':graphview')
} }

View file

@ -14,6 +14,7 @@ apply from: "${project.rootDir}/core/jacoco_global.gradle"
dependencies { dependencies {
implementation project(':shared') implementation project(':shared')
implementation project(':database') implementation project(':database')
implementation project(':graphview')
} }
android { android {

View file

@ -46,9 +46,6 @@ dependencies {
api 'com.madgag.spongycastle:core:1.58.0.0' api 'com.madgag.spongycastle:core:1.58.0.0'
api "com.google.crypto.tink:tink-android:$tink_version" api "com.google.crypto.tink:tink-android:$tink_version"
// Graphview cannot be upgraded
api "com.jjoe64:graphview:4.0.1"
//db //db
api "com.j256.ormlite:ormlite-core:$ormLite_version" api "com.j256.ormlite:ormlite-core:$ormLite_version"
api "com.j256.ormlite:ormlite-android:$ormLite_version" api "com.j256.ormlite:ormlite-android:$ormLite_version"

1
graphview/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

20
graphview/build.gradle Normal file
View file

@ -0,0 +1,20 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-allopen'
apply plugin: 'com.hiya.jacoco-android'
apply plugin: 'kotlinx-serialization'
apply from: "${project.rootDir}/core/android_dependencies.gradle"
apply from: "${project.rootDir}/core/android_module_dependencies.gradle"
apply from: "${project.rootDir}/core/test_dependencies.gradle"
apply from: "${project.rootDir}/core/jacoco_global.gradle"
android {
namespace 'com.jjoe64.graphview'
}
dependencies {
api "androidx.core:core-ktx:$core_version"
}

View file

21
graphview/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -0,0 +1,105 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview;
import java.text.NumberFormat;
/**
* The label formatter that will be used
* by default.
* It will use the NumberFormat from Android
* and sets the maximal fraction digits
* depending on the range between min and max
* value of the current viewport.
*
* It is recommended to use this label formatter
* as base class to implement a custom formatter.
*
* @author jjoe64
*/
public class DefaultLabelFormatter implements LabelFormatter {
/**
* number formatter for x and y values
*/
protected NumberFormat[] mNumberFormatter = new NumberFormat[2];
/**
* reference to the viewport of the
* graph.
* Will be used to calculate the current
* range of values.
*/
protected Viewport mViewport;
/**
* uses the default number format for the labels
*/
public DefaultLabelFormatter() {
}
/**
* use custom number format
*
* @param xFormat the number format for the x labels
* @param yFormat the number format for the y labels
*/
public DefaultLabelFormatter(NumberFormat xFormat, NumberFormat yFormat) {
mNumberFormatter[0] = yFormat;
mNumberFormatter[1] = xFormat;
}
/**
* @param viewport the viewport of the graph
*/
@Override
public void setViewport(Viewport viewport) {
mViewport = viewport;
}
/**
* Formats the raw value to a nice
* looking label, depending on the
* current range of the viewport.
*
* @param value raw value
* @param isValueX true if it's a x value, otherwise false
* @return the formatted value as string
*/
public String formatLabel(double value, boolean isValueX) {
int i = isValueX ? 1 : 0;
if (mNumberFormatter[i] == null) {
mNumberFormatter[i] = NumberFormat.getNumberInstance();
double highestvalue = isValueX ? mViewport.getMaxX(false) : mViewport.getMaxY(false);
double lowestvalue = isValueX ? mViewport.getMinX(false) : mViewport.getMinY(false);
if (highestvalue - lowestvalue < 0.1) {
mNumberFormatter[i].setMaximumFractionDigits(6);
} else if (highestvalue - lowestvalue < 1) {
mNumberFormatter[i].setMaximumFractionDigits(4);
} else if (highestvalue - lowestvalue < 20) {
mNumberFormatter[i].setMaximumFractionDigits(3);
} else if (highestvalue - lowestvalue < 100) {
mNumberFormatter[i].setMaximumFractionDigits(1);
} else {
mNumberFormatter[i].setMaximumFractionDigits(0);
}
}
return mNumberFormatter[i].format(value);
}
}

View file

@ -0,0 +1,548 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.jjoe64.graphview.series.Series;
import java.util.ArrayList;
import java.util.List;
/**
* @author jjoe64
* @version 4.0.0
*/
public class GraphView extends View {
/**
* Class to wrap style options that are general
* to graphs.
*
* @author jjoe64
*/
private static final class Styles {
/**
* The font size of the title that can be displayed
* above the graph.
*
* @see GraphView#setTitle(String)
*/
float titleTextSize;
/**
* The font color of the title that can be displayed
* above the graph.
*
* @see GraphView#setTitle(String)
*/
int titleColor;
}
/**
* Helper class to detect tap events on the
* graph.
*
* @author jjoe64
*/
private class TapDetector {
/**
* save the time of the last down event
*/
private long lastDown;
/**
* point of the tap down event
*/
private PointF lastPoint;
/**
* to be called to process the events
*
* @param event
* @return true if there was a tap event. otherwise returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
lastDown = System.currentTimeMillis();
lastPoint = new PointF(event.getX(), event.getY());
} else if (lastDown > 0 && event.getAction() == MotionEvent.ACTION_MOVE) {
if (Math.abs(event.getX() - lastPoint.x) > 60
|| Math.abs(event.getY() - lastPoint.y) > 60) {
lastDown = 0;
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if (System.currentTimeMillis() - lastDown < 400) {
return true;
}
}
return false;
}
}
/**
* our series (this does not contain the series
* that can be displayed on the right side. The
* right side series is a special feature of
* the {@link SecondScale} feature.
*/
private List<Series> mSeries;
/**
* the renderer for the grid and labels
*/
private GridLabelRenderer mGridLabelRenderer;
/**
* viewport that holds the current bounds of
* view.
*/
private Viewport mViewport;
/**
* title of the graph that will be shown above
*/
private String mTitle;
/**
* wraps the general styles
*/
private Styles mStyles;
/**
* feature to have a second scale e.g. on the
* right side
*/
protected SecondScale mSecondScale;
/**
* tap detector
*/
private TapDetector mTapDetector;
/**
* renderer for the legend
*/
private LegendRenderer mLegendRenderer;
/**
* paint for the graph title
*/
private Paint mPaintTitle;
/**
* paint for the preview (in the SDK)
*/
private Paint mPreviewPaint;
/**
* Initialize the GraphView view
* @param context
*/
public GraphView(Context context) {
super(context);
init();
}
/**
* Initialize the GraphView view.
*
* @param context
* @param attrs
*/
public GraphView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* Initialize the GraphView view
*
* @param context
* @param attrs
* @param defStyle
*/
public GraphView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* initialize the internal objects.
* This method has to be called directly
* in the constructors.
*/
protected void init() {
mPreviewPaint = new Paint();
mPreviewPaint.setTextAlign(Paint.Align.CENTER);
mPreviewPaint.setColor(Color.BLACK);
mPreviewPaint.setTextSize(50);
mStyles = new Styles();
mViewport = new Viewport(this);
mGridLabelRenderer = new GridLabelRenderer(this);
mLegendRenderer = new LegendRenderer(this);
mSeries = new ArrayList<Series>();
mPaintTitle = new Paint();
mTapDetector = new TapDetector();
loadStyles();
}
/**
* loads the font
*/
protected void loadStyles() {
mStyles.titleColor = mGridLabelRenderer.getHorizontalLabelsColor();
mStyles.titleTextSize = mGridLabelRenderer.getTextSize();
}
/**
* @return the renderer for the grid and labels
*/
public GridLabelRenderer getGridLabelRenderer() {
return mGridLabelRenderer;
}
/**
* Add a new series to the graph. This will
* automatically redraw the graph.
* @param s the series to be added
*/
public void addSeries(Series s) {
s.onGraphViewAttached(this);
mSeries.add(s);
onDataChanged(false, false);
}
/**
* important: do not do modifications on the list
* object that will be returned.
* Use {@link #removeSeries(com.jjoe64.graphview.series.Series)} and {@link #addSeries(com.jjoe64.graphview.series.Series)}
*
* @return all series
*/
public List<Series> getSeries() {
// TODO immutable array
return mSeries;
}
/**
* call this to let the graph redraw and
* recalculate the viewport.
* This will be called when a new series
* was added or removed and when data
* was appended via {@link com.jjoe64.graphview.series.BaseSeries#appendData(com.jjoe64.graphview.series.DataPointInterface, boolean, int)}
* or {@link com.jjoe64.graphview.series.BaseSeries#resetData(com.jjoe64.graphview.series.DataPointInterface[])}.
*
* @param keepLabelsSize true if you don't want
* to recalculate the size of
* the labels. It is recommended
* to use "true" because this will
* improve performance and prevent
* a flickering.
* @param keepViewport true if you don't want that
* the viewport will be recalculated.
* It is recommended to use "true" for
* performance.
*/
public void onDataChanged(boolean keepLabelsSize, boolean keepViewport) {
// adjust grid system
mViewport.calcCompleteRange();
mGridLabelRenderer.invalidate(keepLabelsSize, keepViewport);
invalidate();
}
/**
* will be called from Android system.
*
* @param canvas Canvas
*/
@Override
protected void onDraw(Canvas canvas) {
if (isInEditMode()) {
canvas.drawColor(Color.rgb(200, 200, 200));
canvas.drawText("GraphView: No Preview available", canvas.getWidth()/2, canvas.getHeight()/2, mPreviewPaint);
} else {
drawTitle(canvas);
mViewport.drawFirst(canvas);
mGridLabelRenderer.draw(canvas);
for (Series s : mSeries) {
s.draw(this, canvas, false);
}
if (mSecondScale != null) {
for (Series s : mSecondScale.getSeries()) {
s.draw(this, canvas, true);
}
}
mViewport.draw(canvas);
mLegendRenderer.draw(canvas);
}
}
/**
* Draws the Graphs title that will be
* shown above the viewport.
* Will be called by GraphView.
*
* @param canvas Canvas
*/
protected void drawTitle(Canvas canvas) {
if (mTitle != null && mTitle.length()>0) {
mPaintTitle.setColor(mStyles.titleColor);
mPaintTitle.setTextSize(mStyles.titleTextSize);
mPaintTitle.setTextAlign(Paint.Align.CENTER);
float x = canvas.getWidth()/2;
float y = mPaintTitle.getTextSize();
canvas.drawText(mTitle, x, y, mPaintTitle);
}
}
/**
* Calculates the height of the title.
*
* @return the actual size of the title.
* if there is no title, 0 will be
* returned.
*/
protected int getTitleHeight() {
if (mTitle != null && mTitle.length()>0) {
return (int) mPaintTitle.getTextSize();
} else {
return 0;
}
}
/**
* @return the viewport of the Graph.
* @see com.jjoe64.graphview.Viewport
*/
public Viewport getViewport() {
return mViewport;
}
/**
* Called by Android system if the size
* of the view was changed. Will recalculate
* the viewport and labels.
*
* @param w
* @param h
* @param oldw
* @param oldh
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
onDataChanged(false, false);
}
/**
* @return the space on the left side of the
* view from the left border to the
* beginning of the graph viewport.
*/
public int getGraphContentLeft() {
int border = getGridLabelRenderer().getStyles().padding;
return border + getGridLabelRenderer().getLabelVerticalWidth() + getGridLabelRenderer().getVerticalAxisTitleWidth();
}
/**
* @return the space on the top of the
* view from the top border to the
* beginning of the graph viewport.
*/
public int getGraphContentTop() {
int border = getGridLabelRenderer().getStyles().padding + getTitleHeight();
return border;
}
/**
* @return the height of the graph viewport.
*/
public int getGraphContentHeight() {
int border = getGridLabelRenderer().getStyles().padding;
int graphheight = getHeight() - (2 * border) - getGridLabelRenderer().getLabelHorizontalHeight() - getTitleHeight();
graphheight -= getGridLabelRenderer().getHorizontalAxisTitleHeight();
return graphheight;
}
/**
* @return the width of the graph viewport.
*/
public int getGraphContentWidth() {
int border = getGridLabelRenderer().getStyles().padding;
int graphwidth = getWidth() - (2 * border) - getGridLabelRenderer().getLabelVerticalWidth();
if (mSecondScale != null) {
graphwidth -= getGridLabelRenderer().getLabelVerticalSecondScaleWidth();
}
return graphwidth;
}
/**
* will be called from Android system.
*
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean b = mViewport.onTouchEvent(event);
boolean a = super.onTouchEvent(event);
// is it a click?
if (mTapDetector.onTouchEvent(event)) {
for (Series s : mSeries) {
s.onTap(event.getX(), event.getY());
}
if (mSecondScale != null) {
for (Series s : mSecondScale.getSeries()) {
s.onTap(event.getX(), event.getY());
}
}
}
return b || a;
}
/**
*
*/
@Override
public void computeScroll() {
super.computeScroll();
mViewport.computeScroll();
}
/**
* @return the legend renderer.
* @see com.jjoe64.graphview.LegendRenderer
*/
public LegendRenderer getLegendRenderer() {
return mLegendRenderer;
}
/**
* use a specific legend renderer
*
* @param mLegendRenderer the new legend renderer
*/
public void setLegendRenderer(LegendRenderer mLegendRenderer) {
this.mLegendRenderer = mLegendRenderer;
}
/**
* @return the title that will be shown
* above the graph.
*/
public String getTitle() {
return mTitle;
}
/**
* Set the title of the graph that will
* be shown above the graph's viewport.
*
* @param mTitle the title
* @see #setTitleColor(int) to set the font color
* @see #setTitleTextSize(float) to set the font size
*/
public void setTitle(String mTitle) {
this.mTitle = mTitle;
}
/**
* @return the title font size
*/
public float getTitleTextSize() {
return mStyles.titleTextSize;
}
/**
* Set the title's font size
*
* @param titleTextSize font size
* @see #setTitle(String)
*/
public void setTitleTextSize(float titleTextSize) {
mStyles.titleTextSize = titleTextSize;
}
/**
* @return font color of the title
*/
public int getTitleColor() {
return mStyles.titleColor;
}
/**
* Set the title's font color
*
* @param titleColor font color of the title
* @see #setTitle(String)
*/
public void setTitleColor(int titleColor) {
mStyles.titleColor = titleColor;
}
/**
*
* @return
*/
public SecondScale getSecondScale() {
if (mSecondScale == null) {
mSecondScale = new SecondScale(mViewport);
}
return mSecondScale;
}
/**
* Removes all series of the graph.
*/
public void removeAllSeries() {
mSeries.clear();
onDataChanged(false, false);
}
/**
* Remove a specific series of the graph.
* This will also re-render the graph, but
* without recalculating the viewport and
* label sizes.
* If you want this, you have to call {@link #onDataChanged(boolean, boolean)}
* manually.
*
* @param series
*/
public void removeSeries(Series<?> series) {
mSeries.remove(series);
onDataChanged(false, false);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview;
/**
* Interface to use as label formatter.
* Implement this in order to generate
* your own labels format.
* It is recommended to override {@link com.jjoe64.graphview.DefaultLabelFormatter}.
*
* @author jjoe64
*/
public interface LabelFormatter {
/**
* converts a raw number as input to
* a formatted string for the label.
*
* @param value raw input number
* @param isValueX true if it is a value for the x axis
* false if it is a value for the y axis
* @return the formatted number as string
*/
public String formatLabel(double value, boolean isValueX);
/**
* will be called in order to have a
* reference to the current viewport.
* This is useful if you need the bounds
* to generate your labels.
* You store this viewport in as member variable
* and access it e.g. in the {@link #formatLabel(double, boolean)}
* method.
*
* @param viewport the used viewport
*/
public void setViewport(Viewport viewport);
}

View file

@ -0,0 +1,394 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.TypedValue;
import com.jjoe64.graphview.series.Series;
import java.util.ArrayList;
import java.util.List;
/**
* The default renderer for the legend box
*
* @author jjoe64
*/
public class LegendRenderer {
/**
* wrapped styles regarding to the
* legend
*/
private final class Styles {
float textSize;
int spacing;
int padding;
int width;
int backgroundColor;
int textColor;
int margin;
LegendAlign align;
Point fixedPosition;
}
/**
* alignment of the legend
*/
public enum LegendAlign {
/**
* top right corner
*/
TOP,
/**
* middle right
*/
MIDDLE,
/**
* bottom right corner
*/
BOTTOM
}
/**
* wrapped styles
*/
private Styles mStyles;
/**
* reference to the graphview
*/
private final GraphView mGraphView;
/**
* flag whether legend will be
* drawn
*/
private boolean mIsVisible;
/**
* paint for the drawing
*/
private Paint mPaint;
/**
* cached legend width
* this will be filled in the drawing.
* Can be cleared via {@link #resetStyles()}
*/
private int cachedLegendWidth;
/**
* creates legend renderer
*
* @param graphView regarding graphview
*/
public LegendRenderer(GraphView graphView) {
mGraphView = graphView;
mIsVisible = false;
mPaint = new Paint();
mPaint.setTextAlign(Paint.Align.LEFT);
mStyles = new Styles();
cachedLegendWidth = 0;
resetStyles();
}
/**
* resets the styles to the defaults
* and clears the legend width cache
*/
public void resetStyles() {
mStyles.align = LegendAlign.MIDDLE;
mStyles.textSize = mGraphView.getGridLabelRenderer().getTextSize();
mStyles.spacing = (int) (mStyles.textSize / 5);
mStyles.padding = (int) (mStyles.textSize / 2);
mStyles.width = 0;
mStyles.backgroundColor = Color.argb(180, 100, 100, 100);
mStyles.margin = (int) (mStyles.textSize / 5);
// get matching styles from theme
TypedValue typedValue = new TypedValue();
mGraphView.getContext().getTheme().resolveAttribute(android.R.attr.textAppearanceSmall, typedValue, true);
int color1;
try {
TypedArray array = mGraphView.getContext().obtainStyledAttributes(typedValue.data, new int[]{
android.R.attr.textColorPrimary});
color1 = array.getColor(0, Color.BLACK);
array.recycle();
} catch (Exception e) {
color1 = Color.BLACK;
}
mStyles.textColor = color1;
cachedLegendWidth = 0;
}
/**
* draws the legend if it is visible
*
* @param canvas canvas
* @see #setVisible(boolean)
*/
public void draw(Canvas canvas) {
if (!mIsVisible) return;
mPaint.setTextSize(mStyles.textSize);
int shapeSize = (int) (mStyles.textSize*0.8d);
List<Series> allSeries = new ArrayList<Series>();
allSeries.addAll(mGraphView.getSeries());
if (mGraphView.mSecondScale != null) {
allSeries.addAll(mGraphView.getSecondScale().getSeries());
}
// width
int legendWidth = mStyles.width;
if (legendWidth == 0) {
// auto
legendWidth = cachedLegendWidth;
if (legendWidth == 0) {
Rect textBounds = new Rect();
for (Series s : allSeries) {
if (s.getTitle() != null) {
mPaint.getTextBounds(s.getTitle(), 0, s.getTitle().length(), textBounds);
legendWidth = Math.max(legendWidth, textBounds.width());
}
}
if (legendWidth == 0) legendWidth = 1;
// add shape size
legendWidth += shapeSize+mStyles.padding*2 + mStyles.spacing;
cachedLegendWidth = legendWidth;
}
}
// rect
float legendHeight = (mStyles.textSize+mStyles.spacing)*allSeries.size() -mStyles.spacing;
float lLeft;
float lTop;
if (mStyles.fixedPosition != null) {
// use fied position
lLeft = mGraphView.getGraphContentLeft() + mStyles.margin + mStyles.fixedPosition.x;
lTop = mGraphView.getGraphContentTop() + mStyles.margin + mStyles.fixedPosition.y;
} else {
lLeft = mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth() - legendWidth - mStyles.margin;
switch (mStyles.align) {
case TOP:
lTop = mGraphView.getGraphContentTop() + mStyles.margin;
break;
case MIDDLE:
lTop = mGraphView.getHeight() / 2 - legendHeight / 2;
break;
default:
lTop = mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight() - mStyles.margin - legendHeight - 2*mStyles.padding;
}
}
float lRight = lLeft+legendWidth;
float lBottom = lTop+legendHeight+2*mStyles.padding;
mPaint.setColor(mStyles.backgroundColor);
canvas.drawRoundRect(new RectF(lLeft, lTop, lRight, lBottom), 8, 8, mPaint);
int i=0;
for (Series series : allSeries) {
mPaint.setColor(series.getColor());
canvas.drawRect(new RectF(lLeft+mStyles.padding, lTop+mStyles.padding+(i*(mStyles.textSize+mStyles.spacing)), lLeft+mStyles.padding+shapeSize, lTop+mStyles.padding+(i*(mStyles.textSize+mStyles.spacing))+shapeSize), mPaint);
if (series.getTitle() != null) {
mPaint.setColor(mStyles.textColor);
canvas.drawText(series.getTitle(), lLeft+mStyles.padding+shapeSize+mStyles.spacing, lTop+mStyles.padding+mStyles.textSize+(i*(mStyles.textSize+mStyles.spacing)), mPaint);
}
i++;
}
}
/**
* @return the flag whether the legend will be drawn
*/
public boolean isVisible() {
return mIsVisible;
}
/**
* set the flag whether the legend will be drawn
*
* @param mIsVisible visible flag
*/
public void setVisible(boolean mIsVisible) {
this.mIsVisible = mIsVisible;
}
/**
* @return font size
*/
public float getTextSize() {
return mStyles.textSize;
}
/**
* sets the font size. this will clear
* the internal legend width cache
*
* @param textSize font size
*/
public void setTextSize(float textSize) {
mStyles.textSize = textSize;
cachedLegendWidth = 0;
}
/**
* @return the spacing between the text lines
*/
public int getSpacing() {
return mStyles.spacing;
}
/**
* set the spacing between the text lines
*
* @param spacing the spacing between the text lines
*/
public void setSpacing(int spacing) {
mStyles.spacing = spacing;
}
/**
* padding is the space between the edge of the box
* and the beginning of the text
*
* @return padding from edge to text
*/
public int getPadding() {
return mStyles.padding;
}
/**
* padding is the space between the edge of the box
* and the beginning of the text
*
* @param padding padding from edge to text
*/
public void setPadding(int padding) {
mStyles.padding = padding;
}
/**
* the width of the box exclusive padding
*
* @return the width of the box
* 0 => auto
*/
public int getWidth() {
return mStyles.width;
}
/**
* the width of the box exclusive padding
* @param width the width of the box exclusive padding
* 0 => auto
*/
public void setWidth(int width) {
mStyles.width = width;
}
/**
* @return background color of the box
* it is recommended to use semi-transparent
* color.
*/
public int getBackgroundColor() {
return mStyles.backgroundColor;
}
/**
* @param backgroundColor background color of the box
* it is recommended to use semi-transparent
* color.
*/
public void setBackgroundColor(int backgroundColor) {
mStyles.backgroundColor = backgroundColor;
}
/**
* @return margin from the edge of the box
* to the corner of the graphview
*/
public int getMargin() {
return mStyles.margin;
}
/**
* @param margin margin from the edge of the box
* to the corner of the graphview
*/
public void setMargin(int margin) {
mStyles.margin = margin;
}
/**
* @return the vertical alignment of the box
*/
public LegendAlign getAlign() {
return mStyles.align;
}
/**
* @param align the vertical alignment of the box
*/
public void setAlign(LegendAlign align) {
mStyles.align = align;
}
/**
* @return font color
*/
public int getTextColor() {
return mStyles.textColor;
}
/**
* @param textColor font color
*/
public void setTextColor(int textColor) {
mStyles.textColor = textColor;
}
/**
* Use fixed coordinates to position the legend.
* This will override the align setting.
*
* @param x x coordinates in pixel
* @param y y coordinates in pixel
*/
public void setFixedPosition(int x, int y) {
mStyles.fixedPosition = new Point(x, y);
}
}

View file

@ -0,0 +1,165 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview;
import com.jjoe64.graphview.series.Series;
import java.util.ArrayList;
import java.util.List;
/**
* To be used to plot a second scale
* on the graph.
* The second scale has always to have
* manual bounds.
* Use {@link #setMinY(double)} and {@link #setMaxY(double)}
* to set them.
* The second scale has it's own array of series.
*
* @author jjoe64
*/
public class SecondScale {
/**
* reference to the viewport of the graph
*/
protected final Viewport mViewport;
/**
* array of series for the second
* scale
*/
protected List<Series> mSeries;
/**
* flag whether the y axis bounds
* are manual.
* For the current version this is always
* true.
*/
private boolean mYAxisBoundsManual = true;
/**
* min y value for the y axis bounds
*/
private double mMinY;
/**
* max y value for the y axis bounds
*/
private double mMaxY;
/**
* label formatter for the y labels
* on the right side
*/
protected LabelFormatter mLabelFormatter;
/**
* creates the second scale.
* normally you do not call this contructor.
* Use {@link com.jjoe64.graphview.GraphView#getSecondScale()}
* in order to get the instance.
*/
SecondScale(Viewport viewport) {
mViewport = viewport;
mSeries = new ArrayList<Series>();
mLabelFormatter = new DefaultLabelFormatter();
mLabelFormatter.setViewport(mViewport);
}
/**
* add a series to the second scale.
* Don't add this series also to the GraphView
* object.
*
* @param s the series
*/
public void addSeries(Series s) {
mSeries.add(s);
}
//public void setYAxisBoundsManual(boolean mYAxisBoundsManual) {
// this.mYAxisBoundsManual = mYAxisBoundsManual;
//}
/**
* set the min y bounds
*
* @param d min y value
*/
public void setMinY(double d) {
mMinY = d;
}
/**
* set the max y bounds
*
* @param d max y value
*/
public void setMaxY(double d) {
mMaxY = d;
}
/**
* @return the series of the second scale
*/
public List<Series> getSeries() {
return mSeries;
}
/**
* @return min y bound
*/
public double getMinY() {
return mMinY;
}
/**
* @return max y bound
*/
public double getMaxY() {
return mMaxY;
}
/**
* @return always true for the current implementation
*/
public boolean isYAxisBoundsManual() {
return mYAxisBoundsManual;
}
/**
* @return label formatter for the y labels on the right side
*/
public LabelFormatter getLabelFormatter() {
return mLabelFormatter;
}
/**
* Set a custom label formatter that is used
* for the y labels on the right side.
*
* @param formatter label formatter for the y labels
*/
public void setLabelFormatter(LabelFormatter formatter) {
mLabelFormatter = formatter;
mLabelFormatter.setViewport(mViewport);
}
}

View file

@ -0,0 +1,41 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview;
import com.jjoe64.graphview.series.DataPointInterface;
/**
* you can change the color depending on the value.
* takes only effect for BarGraphSeries.
*
* @see com.jjoe64.graphview.series.BarGraphSeries#setValueDependentColor(ValueDependentColor)
*/
public interface ValueDependentColor<T extends DataPointInterface> {
/**
* this is called when a bar is about to draw
* and the color is be loaded.
*
* @param data the current input value
* @return the color that the bar should be drawn with
* Generate the int via the android.graphics.Color class.
*/
public int get(T data);
}

View file

@ -0,0 +1,996 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.OverScroller;
import androidx.core.view.ViewCompat;
import androidx.core.widget.EdgeEffectCompat;
import com.jjoe64.graphview.compat.OverScrollerCompat;
import com.jjoe64.graphview.series.DataPointInterface;
import com.jjoe64.graphview.series.Series;
import java.util.Iterator;
import java.util.List;
/**
* This is the default implementation for the viewport.
* This implementation so for a normal viewport
* where there is a horizontal x-axis and a
* vertical y-axis.
* This viewport is compatible with
* - {@link com.jjoe64.graphview.series.BarGraphSeries}
* - {@link com.jjoe64.graphview.series.LineGraphSeries}
* - {@link com.jjoe64.graphview.series.PointsGraphSeries}
*
* @author jjoe64
*/
public class Viewport {
/**
* listener for the scale gesture
*/
private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
= new ScaleGestureDetector.OnScaleGestureListener() {
/**
* called by android
* @param detector detector
* @return always true
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
float viewportWidth = mCurrentViewport.width();
float center = mCurrentViewport.left + viewportWidth / 2;
viewportWidth /= detector.getScaleFactor();
mCurrentViewport.left = center - viewportWidth / 2;
mCurrentViewport.right = mCurrentViewport.left+viewportWidth;
// viewportStart must not be < minX
float minX = (float) getMinX(true);
if (mCurrentViewport.left < minX) {
mCurrentViewport.left = minX;
mCurrentViewport.right = mCurrentViewport.left+viewportWidth;
}
// viewportStart + viewportSize must not be > maxX
float maxX = (float) getMaxX(true);
if (viewportWidth == 0) {
mCurrentViewport.right = maxX;
}
double overlap = mCurrentViewport.left + viewportWidth - maxX;
if (overlap > 0) {
// scroll left
if (mCurrentViewport.left-overlap > minX) {
mCurrentViewport.left -= overlap;
mCurrentViewport.right = mCurrentViewport.left+viewportWidth;
} else {
// maximal scale
mCurrentViewport.left = minX;
mCurrentViewport.right = maxX;
}
}
// adjust viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
/**
* called when scaling begins
*
* @param detector detector
* @return true if it is scalable
*/
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
if (mIsScalable) {
mScalingBeginWidth = mCurrentViewport.width();
mScalingBeginLeft = mCurrentViewport.left;
mScalingActive = true;
return true;
} else {
return false;
}
}
/**
* called when sacling ends
* This will re-adjust the viewport.
*
* @param detector detector
*/
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
mScalingActive = false;
// re-adjust
mXAxisBoundsStatus = AxisBoundsStatus.READJUST_AFTER_SCALE;
mScrollingReferenceX = Float.NaN;
// adjust viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
};
/**
* simple gesture listener to track scroll events
*/
private final GestureDetector.SimpleOnGestureListener mGestureListener
= new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (!mIsScrollable || mScalingActive) return false;
// Initiates the decay phase of any active edge effects.
releaseEdgeEffects();
mScrollerStartViewport.set(mCurrentViewport);
// Aborts any active scroll animations and invalidates.
mScroller.forceFinished(true);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!mIsScrollable || mScalingActive) return false;
if (Float.isNaN(mScrollingReferenceX)) {
mScrollingReferenceX = mCurrentViewport.left;
}
// Scrolling uses math based on the viewport (as opposed to math using pixels).
/**
* Pixel offset is the offset in screen pixels, while viewport offset is the
* offset within the current viewport. For additional information on surface sizes
* and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
* additional information about the viewport, see the comments for
* {@link mCurrentViewport}.
*/
float viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth();
float viewportOffsetY = -distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight();
int completeWidth = (int)((mCompleteRange.width()/mCurrentViewport.width()) * (float) mGraphView.getGraphContentWidth());
int completeHeight = (int)((mCompleteRange.height()/mCurrentViewport.height()) * (float) mGraphView.getGraphContentHeight());
int scrolledX = (int) (completeWidth
* (mCurrentViewport.left + viewportOffsetX - mCompleteRange.left)
/ mCompleteRange.width());
int scrolledY = (int) (completeHeight
* (mCompleteRange.bottom - mCurrentViewport.bottom - viewportOffsetY)
/ mCompleteRange.height());
boolean canScrollX = mCurrentViewport.left > mCompleteRange.left
|| mCurrentViewport.right < mCompleteRange.right;
boolean canScrollY = mCurrentViewport.bottom > mCompleteRange.bottom
|| mCurrentViewport.top < mCompleteRange.top;
if (canScrollX) {
if (viewportOffsetX < 0) {
float tooMuch = mCurrentViewport.left+viewportOffsetX - mCompleteRange.left;
if (tooMuch < 0) {
viewportOffsetX -= tooMuch;
}
} else {
float tooMuch = mCurrentViewport.right+viewportOffsetX - mCompleteRange.right;
if (tooMuch > 0) {
viewportOffsetX -= tooMuch;
}
}
mCurrentViewport.left += viewportOffsetX;
mCurrentViewport.right += viewportOffsetX;
}
if (canScrollY) {
//mCurrentViewport.top += viewportOffsetX;
//mCurrentViewport.bottom -= viewportOffsetX;
}
if (canScrollX && scrolledX < 0) {
mEdgeEffectLeft.onPull(scrolledX / (float) mGraphView.getGraphContentWidth());
mEdgeEffectLeftActive = true;
}
if (canScrollY && scrolledY < 0) {
mEdgeEffectBottom.onPull(scrolledY / (float) mGraphView.getGraphContentHeight());
mEdgeEffectBottomActive = true;
}
if (canScrollX && scrolledX > completeWidth - mGraphView.getGraphContentWidth()) {
mEdgeEffectRight.onPull((scrolledX - completeWidth + mGraphView.getGraphContentWidth())
/ (float) mGraphView.getGraphContentWidth());
mEdgeEffectRightActive = true;
}
//if (canScrollY && scrolledY > mSurfaceSizeBuffer.y - mContentRect.height()) {
// mEdgeEffectTop.onPull((scrolledY - mSurfaceSizeBuffer.y + mContentRect.height())
// / (float) mContentRect.height());
// mEdgeEffectTopActive = true;
//}
// adjust viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
//fling((int) -velocityX, (int) -velocityY);
return true;
}
};
/**
* the state of the axis bounds
*/
public enum AxisBoundsStatus {
/**
* initial means that the bounds gets
* auto adjusted if they are not manual.
* After adjusting the status comes to
* #AUTO_ADJUSTED.
*/
INITIAL,
/**
* after the bounds got auto-adjusted,
* this status will set.
*/
AUTO_ADJUSTED,
/**
* this flags the status that a scale was
* done and the bounds has to be auto-adjusted
* afterwards.
*/
READJUST_AFTER_SCALE,
/**
* means that the bounds are fix (manually) and
* are not to be auto-adjusted.
*/
FIX
}
/**
* paint to draw background
*/
private Paint mPaint;
/**
* reference to the graphview
*/
private final GraphView mGraphView;
/**
* this holds the current visible viewport
* left = minX, right = maxX
* bottom = minY, top = maxY
*/
protected RectF mCurrentViewport = new RectF();
/**
* this holds the whole range of the data
* left = minX, right = maxX
* bottom = minY, top = maxY
*/
protected RectF mCompleteRange = new RectF();
/**
* flag whether scaling is currently active
*/
protected boolean mScalingActive;
/**
* stores the width of the viewport at the time
* of beginning of the scaling.
*/
protected float mScalingBeginWidth;
/**
* stores the viewport left at the time of
* beginning of the scaling.
*/
protected float mScalingBeginLeft;
/**
* flag whether the viewport is scrollable
*/
private boolean mIsScrollable;
/**
* flag whether the viewport is scalable
*/
private boolean mIsScalable;
/**
* gesture detector to detect scrolling
*/
protected GestureDetector mGestureDetector;
/**
* detect scaling
*/
protected ScaleGestureDetector mScaleGestureDetector;
/**
* not used - for fling
*/
protected OverScroller mScroller;
/**
* not used
*/
private EdgeEffectCompat mEdgeEffectTop;
/**
* not used
*/
private EdgeEffectCompat mEdgeEffectBottom;
/**
* glow effect when scrolling left
*/
private EdgeEffectCompat mEdgeEffectLeft;
/**
* glow effect when scrolling right
*/
private EdgeEffectCompat mEdgeEffectRight;
/**
* not used
*/
private boolean mEdgeEffectTopActive;
/**
* not used
*/
private boolean mEdgeEffectBottomActive;
/**
* glow effect when scrolling left
*/
private boolean mEdgeEffectLeftActive;
/**
* glow effect when scrolling right
*/
private boolean mEdgeEffectRightActive;
/**
* stores the viewport at the time of
* the beginning of scaling
*/
private RectF mScrollerStartViewport = new RectF();
/**
* stores the viewport left value at the
* time of beginning of the scrolling
*/
protected float mScrollingReferenceX = Float.NaN;
/**
* state of the x axis
*/
private AxisBoundsStatus mXAxisBoundsStatus;
/**
* state of the y axis
*/
private AxisBoundsStatus mYAxisBoundsStatus;
/**
* flag whether the x axis bounds are manual
*/
private boolean mXAxisBoundsManual;
/**
* flag whether the y axis bounds are manual
*/
private boolean mYAxisBoundsManual;
/**
* background color of the viewport area
* it is recommended to use a semi-transparent color
*/
private int mBackgroundColor;
/**
* creates the viewport
*
* @param graphView graphview
*/
Viewport(GraphView graphView) {
mScroller = new OverScroller(graphView.getContext());
mEdgeEffectTop = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectBottom = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectLeft = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectRight = new EdgeEffectCompat(graphView.getContext());
mGestureDetector = new GestureDetector(graphView.getContext(), mGestureListener);
mScaleGestureDetector = new ScaleGestureDetector(graphView.getContext(), mScaleGestureListener);
mGraphView = graphView;
mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
mBackgroundColor = Color.TRANSPARENT;
mPaint = new Paint();
}
/**
* will be called on a touch event.
* needed to use scaling and scrolling
*
* @param event
* @return true if it was consumed
*/
public boolean onTouchEvent(MotionEvent event) {
boolean b = mScaleGestureDetector.onTouchEvent(event);
b |= mGestureDetector.onTouchEvent(event);
return b;
}
/**
* change the state of the x axis.
* normally you do not call this method.
* If you want to set manual axis use
* {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
*
* @param s state
*/
public void setXAxisBoundsStatus(AxisBoundsStatus s) {
mXAxisBoundsStatus = s;
}
/**
* change the state of the y axis.
* normally you do not call this method.
* If you want to set manual axis use
* {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
*
* @param s state
*/
public void setYAxisBoundsStatus(AxisBoundsStatus s) {
mYAxisBoundsStatus = s;
}
/**
* @return whether the viewport is scrollable
*/
public boolean isScrollable() {
return mIsScrollable;
}
/**
* @param mIsScrollable whether is viewport is scrollable
*/
public void setScrollable(boolean mIsScrollable) {
this.mIsScrollable = mIsScrollable;
}
/**
* @return the x axis state
*/
public AxisBoundsStatus getXAxisBoundsStatus() {
return mXAxisBoundsStatus;
}
/**
* @return the y axis state
*/
public AxisBoundsStatus getYAxisBoundsStatus() {
return mYAxisBoundsStatus;
}
/**
* caches the complete range (minX, maxX, minY, maxY)
* by iterating all series and all datapoints and
* stores it into #mCompleteRange
*/
public void calcCompleteRange() {
List<Series> series = mGraphView.getSeries();
mCompleteRange.set(0, 0, 0, 0);
if (!series.isEmpty() && !series.get(0).isEmpty()) {
double d = series.get(0).getLowestValueX();
for (Series s : series) {
if (!s.isEmpty() && d > s.getLowestValueX()) {
d = s.getLowestValueX();
}
}
mCompleteRange.left = (float) d;
d = series.get(0).getHighestValueX();
for (Series s : series) {
if (!s.isEmpty() && d < s.getHighestValueX()) {
d = s.getHighestValueX();
}
}
mCompleteRange.right = (float) d;
d = series.get(0).getLowestValueY();
for (Series s : series) {
if (!s.isEmpty() && d > s.getLowestValueY()) {
d = s.getLowestValueY();
}
}
mCompleteRange.bottom = (float) d;
d = series.get(0).getHighestValueY();
for (Series s : series) {
if (!s.isEmpty() && d < s.getHighestValueY()) {
d = s.getHighestValueY();
}
}
mCompleteRange.top = (float) d;
}
// calc current viewport bounds
if (mYAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
}
if (mYAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
mCurrentViewport.top = mCompleteRange.top;
mCurrentViewport.bottom = mCompleteRange.bottom;
}
if (mXAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
}
if (mXAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
mCurrentViewport.left = mCompleteRange.left;
mCurrentViewport.right = mCompleteRange.right;
} else if (mXAxisBoundsManual && !mYAxisBoundsManual && mCompleteRange.width() != 0) {
// get highest/lowest of current viewport
// lowest
double d = Double.MAX_VALUE;
for (Series s : series) {
Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
while (values.hasNext()) {
double v = values.next().getY();
if (d > v) {
d = v;
}
}
}
mCurrentViewport.bottom = (float) d;
// highest
d = Double.MIN_VALUE;
for (Series s : series) {
Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
while (values.hasNext()) {
double v = values.next().getY();
if (d < v) {
d = v;
}
}
}
mCurrentViewport.top = (float) d;
}
// fixes blank screen when range is zero
if (mCurrentViewport.left == mCurrentViewport.right) mCurrentViewport.right++;
if (mCurrentViewport.top == mCurrentViewport.bottom) mCurrentViewport.top++;
}
/**
* @param completeRange if true => minX of the complete range of all series
* if false => minX of the current visible viewport
* @return the min x value
*/
public double getMinX(boolean completeRange) {
if (completeRange) {
return (double) mCompleteRange.left;
} else {
return (double) mCurrentViewport.left;
}
}
/**
* @param completeRange if true => maxX of the complete range of all series
* if false => maxX of the current visible viewport
* @return the max x value
*/
public double getMaxX(boolean completeRange) {
if (completeRange) {
return (double) mCompleteRange.right;
} else {
return mCurrentViewport.right;
}
}
/**
* @param completeRange if true => minY of the complete range of all series
* if false => minY of the current visible viewport
* @return the min y value
*/
public double getMinY(boolean completeRange) {
if (completeRange) {
return (double) mCompleteRange.bottom;
} else {
return mCurrentViewport.bottom;
}
}
/**
* @param completeRange if true => maxY of the complete range of all series
* if false => maxY of the current visible viewport
* @return the max y value
*/
public double getMaxY(boolean completeRange) {
if (completeRange) {
return (double) mCompleteRange.top;
} else {
return mCurrentViewport.top;
}
}
/**
* set the maximal y value for the current viewport.
* Make sure to set the y bounds to manual via
* {@link #setYAxisBoundsManual(boolean)}
* @param y max / highest value
*/
public void setMaxY(double y) {
mCurrentViewport.top = (float) y;
}
/**
* set the minimal y value for the current viewport.
* Make sure to set the y bounds to manual via
* {@link #setYAxisBoundsManual(boolean)}
* @param y min / lowest value
*/
public void setMinY(double y) {
mCurrentViewport.bottom = (float) y;
}
/**
* set the maximal x value for the current viewport.
* Make sure to set the x bounds to manual via
* {@link #setXAxisBoundsManual(boolean)}
* @param x max / highest value
*/
public void setMaxX(double x) {
mCurrentViewport.right = (float) x;
}
/**
* set the minimal x value for the current viewport.
* Make sure to set the x bounds to manual via
* {@link #setXAxisBoundsManual(boolean)}
* @param x min / lowest value
*/
public void setMinX(double x) {
mCurrentViewport.left = (float) x;
}
/**
* release the glowing effects
*/
private void releaseEdgeEffects() {
mEdgeEffectLeftActive
= mEdgeEffectRightActive
= false;
mEdgeEffectLeft.onRelease();
mEdgeEffectRight.onRelease();
}
/**
* not used currently
*
* @param velocityX
* @param velocityY
*/
private void fling(int velocityX, int velocityY) {
velocityY = 0;
releaseEdgeEffects();
// Flings use math in pixels (as opposed to math based on the viewport).
mScrollerStartViewport.set(mCurrentViewport);
int maxX = (int)((mCurrentViewport.width()/mCompleteRange.width())*(float)mGraphView.getGraphContentWidth()) - mGraphView.getGraphContentWidth();
int maxY = (int)((mCurrentViewport.height()/mCompleteRange.height())*(float)mGraphView.getGraphContentHeight()) - mGraphView.getGraphContentHeight();
int startX = (int)((mCurrentViewport.left - mCompleteRange.left)/mCompleteRange.width())*maxX;
int startY = (int)((mCurrentViewport.top - mCompleteRange.top)/mCompleteRange.height())*maxY;
mScroller.forceFinished(true);
mScroller.fling(
startX,
startY,
velocityX,
velocityY,
0, maxX,
0, maxY,
mGraphView.getGraphContentWidth() / 2,
mGraphView.getGraphContentHeight() / 2);
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
/**
* not used currently
*/
public void computeScroll() {
if (true) return;
boolean needsInvalidate = false;
if (mScroller.computeScrollOffset()) {
// The scroller isn't finished, meaning a fling or programmatic pan operation is
// currently active.
int completeWidth = (int)((mCompleteRange.width()/mCurrentViewport.width()) * (float) mGraphView.getGraphContentWidth());
int completeHeight = (int)((mCompleteRange.height()/mCurrentViewport.height()) * (float) mGraphView.getGraphContentHeight());
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
boolean canScrollX = mCurrentViewport.left > mCompleteRange.left
|| mCurrentViewport.right < mCompleteRange.right;
boolean canScrollY = mCurrentViewport.bottom > mCompleteRange.bottom
|| mCurrentViewport.top < mCompleteRange.top;
if (canScrollX
&& currX < 0
&& mEdgeEffectLeft.isFinished()
&& !mEdgeEffectLeftActive) {
mEdgeEffectLeft.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
mEdgeEffectLeftActive = true;
needsInvalidate = true;
} else if (canScrollX
&& currX > (completeWidth - mGraphView.getGraphContentWidth())
&& mEdgeEffectRight.isFinished()
&& !mEdgeEffectRightActive) {
mEdgeEffectRight.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
mEdgeEffectRightActive = true;
needsInvalidate = true;
}
if (canScrollY
&& currY < 0
&& mEdgeEffectTop.isFinished()
&& !mEdgeEffectTopActive) {
mEdgeEffectTop.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
mEdgeEffectTopActive = true;
needsInvalidate = true;
} else if (canScrollY
&& currY > (completeHeight - mGraphView.getGraphContentHeight())
&& mEdgeEffectBottom.isFinished()
&& !mEdgeEffectBottomActive) {
mEdgeEffectBottom.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
mEdgeEffectBottomActive = true;
needsInvalidate = true;
}
float currXRange = mCompleteRange.left + mCompleteRange.width()
* currX / completeWidth;
float currYRange = mCompleteRange.top - mCompleteRange.height()
* currY / completeHeight;
float currWidth = mCurrentViewport.width();
float currHeight = mCurrentViewport.height();
mCurrentViewport.left = currXRange;
mCurrentViewport.right = currXRange + currWidth;
//mCurrentViewport.bottom = currYRange;
//mCurrentViewport.top = currYRange + currHeight;
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
}
/**
* Draws the overscroll "glow" at the four edges of the chart region, if necessary.
*
* @see EdgeEffectCompat
*/
private void drawEdgeEffectsUnclipped(Canvas canvas) {
// The methods below rotate and translate the canvas as needed before drawing the glow,
// since EdgeEffectCompat always draws a top-glow at 0,0.
boolean needsInvalidate = false;
if (!mEdgeEffectTop.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop());
mEdgeEffectTop.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
if (mEdgeEffectTop.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
//if (!mEdgeEffectBottom.isFinished()) {
// final int restoreCount = canvas.save();
// canvas.translate(2 * mContentRect.left - mContentRect.right, mContentRect.bottom);
// canvas.rotate(180, mContentRect.width(), 0);
// mEdgeEffectBottom.setSize(mContentRect.width(), mContentRect.height());
// if (mEdgeEffectBottom.draw(canvas)) {
// needsInvalidate = true;
// }
// canvas.restoreToCount(restoreCount);
//}
if (!mEdgeEffectLeft.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop()+ mGraphView.getGraphContentHeight());
canvas.rotate(-90, 0, 0);
mEdgeEffectLeft.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
if (mEdgeEffectLeft.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectRight.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft()+ mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop());
canvas.rotate(90, 0, 0);
mEdgeEffectRight.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
if (mEdgeEffectRight.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
}
/**
* will be first called in order to draw
* the canvas
* Used to draw the background
*
* @param c canvas.
*/
public void drawFirst(Canvas c) {
// draw background
if (mBackgroundColor != Color.TRANSPARENT) {
mPaint.setColor(mBackgroundColor);
c.drawRect(
mGraphView.getGraphContentLeft(),
mGraphView.getGraphContentTop(),
mGraphView.getGraphContentLeft()+mGraphView.getGraphContentWidth(),
mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(),
mPaint
);
}
}
/**
* draws the glowing edge effect
*
* @param c canvas
*/
public void draw(Canvas c) {
drawEdgeEffectsUnclipped(c);
}
/**
* @return background of the viewport area
*/
public int getBackgroundColor() {
return mBackgroundColor;
}
/**
* @param mBackgroundColor background of the viewport area
* use transparent to have no background
*/
public void setBackgroundColor(int mBackgroundColor) {
this.mBackgroundColor = mBackgroundColor;
}
/**
* @return whether the viewport is scalable
*/
public boolean isScalable() {
return mIsScalable;
}
/**
* active the scaling/zooming feature
* notice: sets the x axis bounds to manual
*
* @param mIsScalable whether the viewport is scalable
*/
public void setScalable(boolean mIsScalable) {
this.mIsScalable = mIsScalable;
if (mIsScalable) {
mIsScrollable = true;
// set viewport to manual
setXAxisBoundsManual(true);
}
}
/**
* @return whether the x axis bounds are manual.
* @see #setMinX(double)
* @see #setMaxX(double)
*/
public boolean isXAxisBoundsManual() {
return mXAxisBoundsManual;
}
/**
* @param mXAxisBoundsManual whether the x axis bounds are manual.
* @see #setMinX(double)
* @see #setMaxX(double)
*/
public void setXAxisBoundsManual(boolean mXAxisBoundsManual) {
this.mXAxisBoundsManual = mXAxisBoundsManual;
if (mXAxisBoundsManual) {
mXAxisBoundsStatus = AxisBoundsStatus.FIX;
}
}
/**
* @return whether the y axis bound are manual
*/
public boolean isYAxisBoundsManual() {
return mYAxisBoundsManual;
}
/**
* @param mYAxisBoundsManual whether the y axis bounds are manual
* @see #setMaxY(double)
* @see #setMinY(double)
*/
public void setYAxisBoundsManual(boolean mYAxisBoundsManual) {
this.mYAxisBoundsManual = mYAxisBoundsManual;
if (mYAxisBoundsManual) {
mYAxisBoundsStatus = AxisBoundsStatus.FIX;
}
}
/**
* forces the viewport to scroll to the end
* of the range by keeping the current viewport size.
*
* Important: Only takes effect if x axis bounds are manual.
*
* @see #setXAxisBoundsManual(boolean)
*/
public void scrollToEnd() {
if (mXAxisBoundsManual) {
float size = mCurrentViewport.width();
mCurrentViewport.right = mCompleteRange.right;
mCurrentViewport.left = mCompleteRange.right - size;
mScrollingReferenceX = Float.NaN;
mGraphView.onDataChanged(true, false);
} else {
Log.w("GraphView", "scrollToEnd works only with manual x axis bounds");
}
}
}

View file

@ -0,0 +1,46 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.compat;
import android.annotation.TargetApi;
import android.os.Build;
import android.widget.OverScroller;
/**
* A utility class for using {@link android.widget.OverScroller} in a backward-compatible fashion.
*/
public class OverScrollerCompat {
/**
* Disallow instantiation.
*/
private OverScrollerCompat() {
}
/**
* @see android.view.ScaleGestureDetector#getCurrentSpanY()
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static float getCurrVelocity(OverScroller overScroller) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return overScroller.getCurrVelocity();
} else {
return 0;
}
}
}

View file

@ -0,0 +1,94 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.helper;
import android.content.Context;
import com.jjoe64.graphview.DefaultLabelFormatter;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* Helper class to use date objects as x-values.
* This will use your own Date Format or by default
* the Android default date format to convert
* the x-values (that has to be millis from
* 01-01-1970) into a formatted date string.
*
* See the DateAsXAxis example in the GraphView-Demos project
* to see a working example.
*
* @author jjoe64
*/
public class DateAsXAxisLabelFormatter extends DefaultLabelFormatter {
/**
* the date format that will convert
* the unix timestamp to string
*/
protected final DateFormat mDateFormat;
/**
* calendar to avoid creating new date objects
*/
protected final Calendar mCalendar;
/**
* create the formatter with the Android default date format to convert
* the x-values.
*
* @param context the application context
*/
public DateAsXAxisLabelFormatter(Context context) {
mDateFormat = android.text.format.DateFormat.getDateFormat(context);
mCalendar = Calendar.getInstance();
}
/**
* create the formatter with your own custom
* date format to convert the x-values.
*
* @param context the application context
* @param dateFormat custom date format
*/
public DateAsXAxisLabelFormatter(Context context, DateFormat dateFormat) {
mDateFormat = dateFormat;
mCalendar = Calendar.getInstance();
}
/**
* formats the x-values as date string.
*
* @param value raw value
* @param isValueX true if it's a x value, otherwise false
* @return value converted to string
*/
@Override
public String formatLabel(double value, boolean isValueX) {
if (isValueX) {
// format as date
mCalendar.setTimeInMillis((long) value);
return mDateFormat.format(mCalendar.getTimeInMillis());
} else {
return super.formatLabel(value, isValueX);
}
}
}

View file

@ -0,0 +1,137 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.helper;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.R;
import com.jjoe64.graphview.series.BarGraphSeries;
import com.jjoe64.graphview.series.BaseSeries;
import com.jjoe64.graphview.series.DataPoint;
import com.jjoe64.graphview.series.DataPointInterface;
import com.jjoe64.graphview.series.LineGraphSeries;
import com.jjoe64.graphview.series.PointsGraphSeries;
import com.jjoe64.graphview.series.Series;
/**
* helper class to use GraphView directly
* in a XML layout file.
*
* You can set the data via attribute <b>app:seriesData</b>
* in the format: "X=Y;X=Y;..." e.g. "0=5.0;1=5;2=4;3=9"
*
* Other styling options:
* <li>app:seriesType="line|bar|points"</li>
* <li>app:seriesColor="#ff0000"</li>
* <li>app:seriesTitle="foobar" - if this is set, the legend will be drawn</li>
* <li>android:title="foobar"</li>
*
* Example:
* <pre>
* {@code
* <com.jjoe64.graphview.helper.GraphViewXML
* android:layout_width="match_parent"
* android:layout_height="100dip"
* app:seriesData="0=5;2=5;3=0;4=2"
* app:seriesType="line"
* app:seriesColor="#ee0000" />
* }
* </pre>
*
* @author jjoe64
*/
public class GraphViewXML extends GraphView {
/**
* creates the graphview object with data and
* other options from xml attributes.
*
* @param context
* @param attrs
*/
public GraphViewXML(Context context, AttributeSet attrs) {
super(context, attrs);
// get attributes
TypedArray a=context.obtainStyledAttributes(
attrs,
R.styleable.GraphViewXML);
String dataStr = a.getString(R.styleable.GraphViewXML_seriesData);
int color = a.getColor(R.styleable.GraphViewXML_seriesColor, Color.TRANSPARENT);
String type = a.getString(R.styleable.GraphViewXML_seriesType);
String seriesTitle = a.getString(R.styleable.GraphViewXML_seriesTitle);
String title = a.getString(R.styleable.GraphViewXML_android_title);
a.recycle();
// decode data
DataPoint[] data;
if (dataStr == null || dataStr.isEmpty()) {
throw new IllegalArgumentException("Attribute seriesData is required in the format: 0=5.0;1=5;2=4;3=9");
} else {
String[] d = dataStr.split(";");
try {
data = new DataPoint[d.length];
int i = 0;
for (String dd : d) {
String[] xy = dd.split("=");
data[i] = new DataPoint(Double.parseDouble(xy[0]), Double.parseDouble(xy[1]));
i++;
}
} catch (Exception e) {
Log.e("GraphViewXML", e.toString());
throw new IllegalArgumentException("Attribute seriesData is broken. Use this format: 0=5.0;1=5;2=4;3=9");
}
}
// create series
BaseSeries<DataPoint> series;
if (type == null || type.isEmpty()) {
type = "line";
}
if (type.equals("line")) {
series = new LineGraphSeries<DataPoint>(data);
} else if (type.equals("bar")) {
series = new BarGraphSeries<DataPoint>(data);
} else if (type.equals("points")) {
series = new PointsGraphSeries<DataPoint>(data);
} else {
throw new IllegalArgumentException("unknown graph type: "+type+". Possible is line|bar|points");
}
if (color != Color.TRANSPARENT) {
series.setColor(color);
}
addSeries(series);
if (seriesTitle != null && !seriesTitle.isEmpty()) {
series.setTitle(seriesTitle);
getLegendRenderer().setVisible(true);
}
if (title != null && !title.isEmpty()) {
setTitle(title);
}
}
}

View file

@ -0,0 +1,209 @@
package com.jjoe64.graphview.helper;
import com.jjoe64.graphview.DefaultLabelFormatter;
import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.LabelFormatter;
import com.jjoe64.graphview.Viewport;
/**
* Use this label formatter to show static labels.
* Static labels are not bound to the data. It is typical used
* for show text like "low", "middle", "high".
*
* You can set the static labels for vertical or horizontal
* individually and you can define a label formatter that
* is to be used if you don't define static labels.
*
* For example if you only use static labels for horizontal labels,
* graphview will use the dynamicLabelFormatter for the vertical labels.
*/
public class StaticLabelsFormatter implements LabelFormatter {
/**
* reference to the viewport
*/
protected Viewport mViewport;
/**
* the vertical labels, ordered from bottom to the top
* if it is null, the labels will be generated via the #dynamicLabelFormatter
*/
protected String[] mVerticalLabels;
/**
* the horizontal labels, ordered form the left to the right
* if it is null, the labels will be generated via the #dynamicLabelFormatter
*/
protected String[] mHorizontalLabels;
/**
* the label formatter that will format the labels
* for that there are no static labels defined.
*/
protected LabelFormatter mDynamicLabelFormatter;
/**
* reference to the graphview
*/
protected final GraphView mGraphView;
/**
* creates the formatter without any static labels
* define your static labels via {@link #setHorizontalLabels(String[])} and {@link #setVerticalLabels(String[])}
*
* @param graphView reference to the graphview
*/
public StaticLabelsFormatter(GraphView graphView) {
mGraphView = graphView;
init(null, null, null);
}
/**
* creates the formatter without any static labels.
* define your static labels via {@link #setHorizontalLabels(String[])} and {@link #setVerticalLabels(String[])}
*
* @param graphView reference to the graphview
* @param dynamicLabelFormatter the label formatter that will format the labels
* for that there are no static labels defined.
*/
public StaticLabelsFormatter(GraphView graphView, LabelFormatter dynamicLabelFormatter) {
mGraphView = graphView;
init(null, null, dynamicLabelFormatter);
}
/**
* creates the formatter with static labels defined.
*
* @param graphView reference to the graphview
* @param horizontalLabels the horizontal labels, ordered form the left to the right
* if it is null, the labels will be generated via the #dynamicLabelFormatter
* @param verticalLabels the vertical labels, ordered from bottom to the top
* if it is null, the labels will be generated via the #dynamicLabelFormatter
*/
public StaticLabelsFormatter(GraphView graphView, String[] horizontalLabels, String[] verticalLabels) {
mGraphView = graphView;
init(horizontalLabels, verticalLabels, null);
}
/**
* creates the formatter with static labels defined.
*
* @param graphView reference to the graphview
* @param horizontalLabels the horizontal labels, ordered form the left to the right
* if it is null, the labels will be generated via the #dynamicLabelFormatter
* @param verticalLabels the vertical labels, ordered from bottom to the top
* if it is null, the labels will be generated via the #dynamicLabelFormatter
* @param dynamicLabelFormatter the label formatter that will format the labels
* for that there are no static labels defined.
*/
public StaticLabelsFormatter(GraphView graphView, String[] horizontalLabels, String[] verticalLabels, LabelFormatter dynamicLabelFormatter) {
mGraphView = graphView;
init(horizontalLabels, verticalLabels, dynamicLabelFormatter);
}
/**
* @param horizontalLabels the horizontal labels, ordered form the left to the right
* if it is null, the labels will be generated via the #dynamicLabelFormatter
* @param verticalLabels the vertical labels, ordered from bottom to the top
* if it is null, the labels will be generated via the #dynamicLabelFormatter
* @param dynamicLabelFormatter the label formatter that will format the labels
* for that there are no static labels defined.
*/
protected void init(String[] horizontalLabels, String[] verticalLabels, LabelFormatter dynamicLabelFormatter) {
mDynamicLabelFormatter = dynamicLabelFormatter;
if (mDynamicLabelFormatter == null) {
mDynamicLabelFormatter = new DefaultLabelFormatter();
}
mHorizontalLabels = horizontalLabels;
mVerticalLabels = verticalLabels;
}
/**
* Set a label formatter that will be used for the labels
* that don't have static labels.
*
* For example if you only use static labels for horizontal labels,
* graphview will use the dynamicLabelFormatter for the vertical labels.
*
* @param dynamicLabelFormatter the label formatter that will format the labels
* for that there are no static labels defined.
*/
public void setDynamicLabelFormatter(LabelFormatter dynamicLabelFormatter) {
this.mDynamicLabelFormatter = dynamicLabelFormatter;
adjust();
}
/**
* @param horizontalLabels the horizontal labels, ordered form the left to the right
* if it is null, the labels will be generated via the #dynamicLabelFormatter
*/
public void setHorizontalLabels(String[] horizontalLabels) {
this.mHorizontalLabels = horizontalLabels;
adjust();
}
/**
* @param verticalLabels the vertical labels, ordered from bottom to the top
* if it is null, the labels will be generated via the #dynamicLabelFormatter
*/
public void setVerticalLabels(String[] verticalLabels) {
this.mVerticalLabels = verticalLabels;
adjust();
}
/**
*
* @param value raw input number
* @param isValueX true if it is a value for the x axis
* false if it is a value for the y axis
* @return
*/
@Override
public String formatLabel(double value, boolean isValueX) {
if (isValueX && mHorizontalLabels != null) {
double minX = mViewport.getMinX(false);
double maxX = mViewport.getMaxX(false);
double range = maxX - minX;
value = value-minX;
int idx = (int)((value/range) * (mHorizontalLabels.length-1));
return mHorizontalLabels[idx];
} else if (!isValueX && mVerticalLabels != null) {
double minY = mViewport.getMinY(false);
double maxY = mViewport.getMaxY(false);
double range = maxY - minY;
value = value-minY;
int idx = (int)((value/range) * (mVerticalLabels.length-1));
return mVerticalLabels[idx];
} else {
return mDynamicLabelFormatter.formatLabel(value, isValueX);
}
}
/**
* @param viewport the used viewport
*/
@Override
public void setViewport(Viewport viewport) {
mViewport = viewport;
adjust();
}
/**
* adjusts the number of vertical/horizontal labels
*/
protected void adjust() {
mDynamicLabelFormatter.setViewport(mViewport);
if (mVerticalLabels != null) {
if (mVerticalLabels.length < 2) {
throw new IllegalStateException("You need at least 2 vertical labels if you use static label formatter.");
}
mGraphView.getGridLabelRenderer().setNumVerticalLabels(mVerticalLabels.length);
}
if (mHorizontalLabels != null) {
if (mHorizontalLabels.length < 2) {
throw new IllegalStateException("You need at least 2 horizontal labels if you use static label formatter.");
}
mGraphView.getGridLabelRenderer().setNumHorizontalLabels(mHorizontalLabels.length);
}
}
}

View file

@ -0,0 +1,379 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.series;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.Log;
import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.ValueDependentColor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* Series with Bars to visualize the data.
* The Bars are always vertical.
*
* @author jjoe64
*/
public class BarGraphSeries<E extends DataPointInterface> extends BaseSeries<E> {
/**
* paint to do drawing on canvas
*/
private Paint mPaint;
/**
* spacing between the bars in percentage.
* 0 => no spacing
* 100 => the space bewetten the bars is as big as the bars itself
*/
private int mSpacing;
/**
* callback to generate value-dependent colors
* of the bars
*/
private ValueDependentColor<E> mValueDependentColor;
/**
* flag whether the values should drawn
* above the bars as text
*/
private boolean mDrawValuesOnTop;
/**
* color of the text above the bars.
*
* @see #mDrawValuesOnTop
*/
private int mValuesOnTopColor;
/**
* font size of the text above the bars.
*
* @see #mDrawValuesOnTop
*/
private float mValuesOnTopSize;
/**
* stores the coordinates of the bars to
* trigger tap on series events.
*/
private Map<RectF, E> mDataPoints = new HashMap<RectF, E>();
/**
* creates bar series without any data
*/
public BarGraphSeries() {
mPaint = new Paint();
}
/**
* creates bar series with data
*
* @param data values
*/
public BarGraphSeries(E[] data) {
super(data);
mPaint = new Paint();
}
/**
* draws the bars on the canvas
*
* @param graphView corresponding graphview
* @param canvas canvas
* @param isSecondScale whether we are plotting the second scale or not
*/
@Override
public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) {
mPaint.setTextAlign(Paint.Align.CENTER);
if (mValuesOnTopSize == 0) {
mValuesOnTopSize = graphView.getGridLabelRenderer().getTextSize();
}
mPaint.setTextSize(mValuesOnTopSize);
// get data
double maxX = graphView.getViewport().getMaxX(false);
double minX = graphView.getViewport().getMinX(false);
double maxY;
double minY;
if (isSecondScale) {
maxY = graphView.getSecondScale().getMaxY();
minY = graphView.getSecondScale().getMinY();
} else {
maxY = graphView.getViewport().getMaxY(false);
minY = graphView.getViewport().getMinY(false);
}
// Iterate through all bar graph series
// so we know how wide to make our bar,
// and in what position to put it in
int numBarSeries = 0;
int currentSeriesOrder = 0;
int numValues = 0;
boolean isCurrentSeries;
SortedSet<Double> xVals = new TreeSet<Double>();
for(Series inspectedSeries: graphView.getSeries()) {
if(inspectedSeries instanceof BarGraphSeries) {
isCurrentSeries = (inspectedSeries == this);
if(isCurrentSeries) {
currentSeriesOrder = numBarSeries;
}
numBarSeries++;
// calculate the number of slots for bars based on the minimum distance between
// x coordinates in the series. This is divided into the range to find
// the placement and width of bar slots
// (sections of the x axis for each bar or set of bars)
// TODO: Move this somewhere more general and cache it, so we don't recalculate it for each series
Iterator<E> curValues = inspectedSeries.getValues(minX, maxX);
if (curValues.hasNext()) {
xVals.add(curValues.next().getX());
if(isCurrentSeries) { numValues++; }
while (curValues.hasNext()) {
xVals.add(curValues.next().getX());
if(isCurrentSeries) { numValues++; }
}
}
}
}
if (numValues == 0) {
return;
}
Double lastVal = null;
double minGap = 0;
for(Double curVal: xVals) {
if(lastVal != null) {
double curGap = Math.abs(curVal - lastVal);
if (minGap == 0 || (curGap > 0 && curGap < minGap)) {
minGap = curGap;
}
}
lastVal = curVal;
}
int numBarSlots = (minGap == 0) ? 1 : (int)Math.round((maxX - minX)/minGap) + 1;
Iterator<E> values = getValues(minX, maxX);
// Calculate the overall bar slot width - this includes all bars across
// all series, and any spacing between sets of bars
float barSlotWidth = numBarSlots == 1
? graphView.getGraphContentWidth()
: graphView.getGraphContentWidth() / (numBarSlots-1);
Log.d("BarGraphSeries", "numBars=" + numBarSlots);
// Total spacing (both sides) between sets of bars
float spacing = Math.min((float) barSlotWidth*mSpacing/100, barSlotWidth*0.98f);
// Width of an individual bar
float barWidth = (barSlotWidth - spacing) / numBarSeries;
// Offset from the center of a given bar to start drawing
float offset = barSlotWidth/2;
double diffY = maxY - minY;
double diffX = maxX - minX;
float contentHeight = graphView.getGraphContentHeight();
float contentWidth = graphView.getGraphContentWidth();
float contentLeft = graphView.getGraphContentLeft();
float contentTop = graphView.getGraphContentTop();
// draw data
int i=0;
while (values.hasNext()) {
E value = values.next();
double valY = value.getY() - minY;
double ratY = valY / diffY;
double y = contentHeight * ratY;
double valY0 = 0 - minY;
double ratY0 = valY0 / diffY;
double y0 = contentHeight * ratY0;
double valX = value.getX() - minX;
double ratX = valX / diffX;
double x = contentWidth * ratX;
// hook for value dependent color
if (getValueDependentColor() != null) {
mPaint.setColor(getValueDependentColor().get(value));
} else {
mPaint.setColor(getColor());
}
float left = (float)x + contentLeft - offset + spacing/2 + currentSeriesOrder*barWidth;
float top = (contentTop - (float)y) + contentHeight;
float right = left + barWidth;
float bottom = (contentTop - (float)y0) + contentHeight - (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1);
boolean reverse = top > bottom;
if (reverse) {
float tmp = top;
top = bottom + (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1);
bottom = tmp;
}
// overdraw
left = Math.max(left, contentLeft);
right = Math.min(right, contentLeft+contentWidth);
bottom = Math.min(bottom, contentTop+contentHeight);
top = Math.max(top, contentTop);
mDataPoints.put(new RectF(left, top, right, bottom), value);
canvas.drawRect(left, top, right, bottom, mPaint);
// set values on top of graph
if (mDrawValuesOnTop) {
if (reverse) {
top = bottom + mValuesOnTopSize + 4;
if (top > contentTop+contentHeight) top = contentTop + contentHeight;
} else {
top -= 4;
if (top<=contentTop) top+=contentTop+4;
}
mPaint.setColor(mValuesOnTopColor);
canvas.drawText(
graphView.getGridLabelRenderer().getLabelFormatter().formatLabel(value.getY(), false)
, (left+right)/2, top, mPaint);
}
i++;
}
}
/**
* @return the hook to generate value-dependent color. default null
*/
public ValueDependentColor<E> getValueDependentColor() {
return mValueDependentColor;
}
/**
* set a hook to make the color of the bars depending
* on the actually value/data.
*
* @param mValueDependentColor hook
* null to disable
*/
public void setValueDependentColor(ValueDependentColor<E> mValueDependentColor) {
this.mValueDependentColor = mValueDependentColor;
}
/**
* @return the spacing between the bars in percentage
*/
public int getSpacing() {
return mSpacing;
}
/**
* @param mSpacing spacing between the bars in percentage.
* 0 => no spacing
* 100 => the space between the bars is as big as the bars itself
*/
public void setSpacing(int mSpacing) {
this.mSpacing = mSpacing;
}
/**
* @return whether the values should be drawn above the bars
*/
public boolean isDrawValuesOnTop() {
return mDrawValuesOnTop;
}
/**
* @param mDrawValuesOnTop flag whether the values should drawn
* above the bars as text
*/
public void setDrawValuesOnTop(boolean mDrawValuesOnTop) {
this.mDrawValuesOnTop = mDrawValuesOnTop;
}
/**
* @return font color of the values on top of the bars
* @see #setDrawValuesOnTop(boolean)
*/
public int getValuesOnTopColor() {
return mValuesOnTopColor;
}
/**
* @param mValuesOnTopColor the font color of the values on top of the bars
* @see #setDrawValuesOnTop(boolean)
*/
public void setValuesOnTopColor(int mValuesOnTopColor) {
this.mValuesOnTopColor = mValuesOnTopColor;
}
/**
* @return font size of the values above the bars
* @see #setDrawValuesOnTop(boolean)
*/
public float getValuesOnTopSize() {
return mValuesOnTopSize;
}
/**
* @param mValuesOnTopSize font size of the values above the bars
* @see #setDrawValuesOnTop(boolean)
*/
public void setValuesOnTopSize(float mValuesOnTopSize) {
this.mValuesOnTopSize = mValuesOnTopSize;
}
/**
* resets the cached coordinates of the bars
*/
@Override
protected void resetDataPoints() {
mDataPoints.clear();
}
/**
* find the corresponding data point by
* coordinates.
*
* @param x pixels
* @param y pixels
* @return datapoint or null
*/
@Override
protected E findDataPoint(float x, float y) {
for (Map.Entry<RectF, E> entry : mDataPoints.entrySet()) {
if (x >= entry.getKey().left && x <= entry.getKey().right
&& y >= entry.getKey().top && y <= entry.getKey().bottom) {
return entry.getValue();
}
}
return null;
}
}

View file

@ -0,0 +1,448 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.series;
import android.graphics.PointF;
import android.util.Log;
import com.jjoe64.graphview.GraphView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
/**
* Basis implementation for series.
* Used for series that are plotted on
* a default x/y 2d viewport.
*
* Extend this class to implement your own custom
* graph type.
*
* This implementation uses a internal Array to store
* the data. If you want to implement a custom data provider
* you may want to implement {@link com.jjoe64.graphview.series.Series}.
*
* @author jjoe64
*/
public abstract class BaseSeries<E extends DataPointInterface> implements Series<E> {
/**
* holds the data
*/
final private List<E> mData = new ArrayList<E>();
/**
* stores the used coordinates to find the
* corresponding data point on a tap
*
* Key => x/y pixel
* Value => Plotted Datapoint
*
* will be filled while drawing via {@link #registerDataPoint(float, float, DataPointInterface)}
*/
private Map<PointF, E> mDataPoints = new HashMap<PointF, E>();
/**
* title for this series that can be displayed
* in the legend.
*/
private String mTitle;
/**
* base color for this series. will be used also in
* the legend
*/
private int mColor = 0xff0077cc;
/**
* listener to handle tap events on a data point
*/
protected OnDataPointTapListener mOnDataPointTapListener;
/**
* stores the graphviews where this series is used.
* Can be more than one.
*/
private List<GraphView> mGraphViews;
/**
* creates series without data
*/
public BaseSeries() {
mGraphViews = new ArrayList<GraphView>();
}
/**
* creates series with data
*
* @param data data points
* important: array has to be sorted from lowest x-value to the highest
*/
public BaseSeries(E[] data) {
mGraphViews = new ArrayList<GraphView>();
for (E d : data) {
mData.add(d);
}
}
/**
* @return the lowest x value, or 0 if there is no data
*/
public double getLowestValueX() {
if (mData.isEmpty()) return 0d;
return mData.get(0).getX();
}
/**
* @return the highest x value, or 0 if there is no data
*/
public double getHighestValueX() {
if (mData.isEmpty()) return 0d;
return mData.get(mData.size()-1).getX();
}
/**
* @return the lowest y value, or 0 if there is no data
*/
public double getLowestValueY() {
if (mData.isEmpty()) return 0d;
double l = mData.get(0).getY();
for (int i = 1; i < mData.size(); i++) {
double c = mData.get(i).getY();
if (l > c) {
l = c;
}
}
return l;
}
/**
* @return the highest y value, or 0 if there is no data
*/
public double getHighestValueY() {
if (mData.isEmpty()) return 0d;
double h = mData.get(0).getY();
for (int i = 1; i < mData.size(); i++) {
double c = mData.get(i).getY();
if (h < c) {
h = c;
}
}
return h;
}
/**
* get the values for a given x range. if from and until are bigger or equal than
* all the data, the original data is returned.
* If it is only a part of the data, the range is returned plus one datapoint
* before and after to get a nice scrolling.
*
* @param from minimal x-value
* @param until maximal x-value
* @return data for the range +/- 1 datapoint
*/
@Override
public Iterator<E> getValues(final double from, final double until) {
if (from <= getLowestValueX() && until >= getHighestValueX()) {
return mData.iterator();
} else {
return new Iterator<E>() {
Iterator<E> org = mData.iterator();
E nextValue = null;
E nextNextValue = null;
boolean plusOne = true;
{
// go to first
boolean found = false;
E prevValue = null;
if (org.hasNext()) {
prevValue = org.next();
}
if (prevValue.getX() >= from) {
nextValue = prevValue;
found = true;
} else {
while (org.hasNext()) {
nextValue = org.next();
if (nextValue.getX() >= from) {
found = true;
nextNextValue = nextValue;
nextValue = prevValue;
break;
}
prevValue = nextValue;
}
}
if (!found) {
nextValue = null;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public E next() {
if (hasNext()) {
E r = nextValue;
if (r.getX() > until) {
plusOne = false;
}
if (nextNextValue != null) {
nextValue = nextNextValue;
nextNextValue = null;
} else if (org.hasNext()) nextValue = org.next();
else nextValue = null;
return r;
} else {
throw new NoSuchElementException();
}
}
@Override
public boolean hasNext() {
return nextValue != null && (nextValue.getX() <= until || plusOne);
}
};
}
}
/**
* @return the title of the series
*/
public String getTitle() {
return mTitle;
}
/**
* set the title of the series. This will be used in
* the legend.
*
* @param mTitle title of the series
*/
public void setTitle(String mTitle) {
this.mTitle = mTitle;
}
/**
* @return color of the series
*/
public int getColor() {
return mColor;
}
/**
* set the color of the series. This will be used in
* plotting (depends on the series implementation) and
* is used in the legend.
*
* @param mColor
*/
public void setColor(int mColor) {
this.mColor = mColor;
}
/**
* set a listener for tap on a data point.
*
* @param l listener
*/
public void setOnDataPointTapListener(OnDataPointTapListener l) {
this.mOnDataPointTapListener = l;
}
/**
* called by the tap detector in order to trigger
* the on tap on datapoint event.
*
* @param x pixel
* @param y pixel
*/
@Override
public void onTap(float x, float y) {
if (mOnDataPointTapListener != null) {
E p = findDataPoint(x, y);
if (p != null) {
mOnDataPointTapListener.onTap(this, p);
}
}
}
/**
* find the data point which is next to the
* coordinates
*
* @param x pixel
* @param y pixel
* @return the data point or null if nothing was found
*/
protected E findDataPoint(float x, float y) {
float shortestDistance = Float.NaN;
E shortest = null;
for (Map.Entry<PointF, E> entry : mDataPoints.entrySet()) {
float x1 = entry.getKey().x;
float y1 = entry.getKey().y;
float x2 = x;
float y2 = y;
float distance = (float) Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
if (shortest == null || distance < shortestDistance) {
shortestDistance = distance;
shortest = entry.getValue();
}
}
if (shortest != null) {
if (shortestDistance < 120) {
return shortest;
}
}
return null;
}
/**
* register the datapoint to find it at a tap
*
* @param x pixel
* @param y pixel
* @param dp the data point to save
*/
protected void registerDataPoint(float x, float y, E dp) {
mDataPoints.put(new PointF(x, y), dp);
}
/**
* clears the cached data point coordinates
*/
protected void resetDataPoints() {
mDataPoints.clear();
}
/**
* clears the data of this series and sets new.
* will redraw the graph
*
* @param data the values must be in the correct order!
* x-value has to be ASC. First the lowest x value and at least the highest x value.
*/
public void resetData(E[] data) {
mData.clear();
for (E d : data) {
mData.add(d);
}
checkValueOrder(null);
// update graphview
for (GraphView gv : mGraphViews) {
gv.onDataChanged(true, false);
}
}
/**
* called when the series was added to a graph
*
* @param graphView graphview
*/
@Override
public void onGraphViewAttached(GraphView graphView) {
mGraphViews.add(graphView);
}
/**
*
* @param dataPoint values the values must be in the correct order!
* x-value has to be ASC. First the lowest x value and at least the highest x value.
* @param scrollToEnd true => graphview will scroll to the end (maxX)
* @param maxDataPoints if max data count is reached, the oldest data
* value will be lost to avoid memory leaks
*/
public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints) {
checkValueOrder(dataPoint);
if (!mData.isEmpty() && dataPoint.getX() < mData.get(mData.size()-1).getX()) {
throw new IllegalArgumentException("new x-value must be greater then the last value. x-values has to be ordered in ASC.");
}
synchronized (mData) {
int curDataCount = mData.size();
if (curDataCount < maxDataPoints) {
// enough space
mData.add(dataPoint);
} else {
// we have to trim one data
mData.remove(0);
mData.add(dataPoint);
}
}
// recalc the labels when it was the first data
boolean keepLabels = mData.size() != 1;
// update linked graph views
// update graphview
for (GraphView gv : mGraphViews) {
gv.onDataChanged(keepLabels, scrollToEnd);
if (scrollToEnd) {
gv.getViewport().scrollToEnd();
}
}
}
/**
* @return whether there are data points
*/
@Override
public boolean isEmpty() {
return mData.isEmpty();
}
/**
* checks that the data is in the correct order
*
* @param onlyLast if not null, it will only check that this
* datapoint is after the last point.
*/
protected void checkValueOrder(DataPointInterface onlyLast) {
if (mData.size()>1) {
if (onlyLast != null) {
// only check last
if (onlyLast.getX() < mData.get(mData.size()-1).getX()) {
throw new IllegalArgumentException("new x-value must be greater then the last value. x-values has to be ordered in ASC.");
}
} else {
double lx = mData.get(0).getX();
for (int i = 1; i < mData.size(); i++) {
if (mData.get(i).getX() != Double.NaN) {
if (lx > mData.get(i).getX()) {
throw new IllegalArgumentException("The order of the values is not correct. X-Values have to be ordered ASC. First the lowest x value and at least the highest x value.");
}
lx = mData.get(i).getX();
}
}
}
}
}
}

View file

@ -0,0 +1,63 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.series;
import android.provider.ContactsContract;
import java.io.Serializable;
import java.util.Date;
/**
* default data point implementation.
* This stores the x and y values.
*
* @author jjoe64
*/
public class DataPoint implements DataPointInterface, Serializable {
private static final long serialVersionUID=1428263322645L;
private double x;
private double y;
public DataPoint(double x, double y) {
this.x=x;
this.y=y;
}
public DataPoint(Date x, double y) {
this.x = x.getTime();
this.y = y;
}
@Override
public double getX() {
return x;
}
@Override
public double getY() {
return y;
}
@Override
public String toString() {
return "["+x+"/"+y+"]";
}
}

View file

@ -0,0 +1,41 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.series;
/**
* interface of data points. Implement this in order
* to use your class in {@link com.jjoe64.graphview.series.Series}.
*
* You can also use the default implementation {@link com.jjoe64.graphview.series.DataPoint} so
* you do not have to implement it for yourself.
*
* @author jjoe64
*/
public interface DataPointInterface {
/**
* @return the x value
*/
public double getX();
/**
* @return the y value
*/
public double getY();
}

View file

@ -0,0 +1,409 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.series;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import com.jjoe64.graphview.GraphView;
import java.util.Iterator;
/**
* Series to plot the data as line.
* The line can be styled with many options.
*
* @author jjoe64
*/
public class LineGraphSeries<E extends DataPointInterface> extends BaseSeries<E> {
/**
* wrapped styles regarding the line
*/
private final class Styles {
/**
* the thickness of the line.
* This option will be ignored if you are
* using a custom paint via {@link #setCustomPaint(android.graphics.Paint)}
*/
private int thickness = 5;
/**
* flag whether the area under the line to the bottom
* of the viewport will be filled with a
* specific background color.
*
* @see #backgroundColor
*/
private boolean drawBackground = false;
/**
* flag whether the data points are highlighted as
* a visible point.
*
* @see #dataPointsRadius
*/
private boolean drawDataPoints = false;
/**
* the radius for the data points.
*
* @see #drawDataPoints
*/
private float dataPointsRadius = 10f;
/**
* the background color for the filling under
* the line.
*
* @see #drawBackground
*/
private int backgroundColor = Color.argb(100, 172, 218, 255);
}
/**
* wrapped styles
*/
private Styles mStyles;
/**
* internal paint object
*/
private Paint mPaint;
/**
* paint for the background
*/
private Paint mPaintBackground;
/**
* path for the background filling
*/
private Path mPathBackground;
/**
* path to the line
*/
private Path mPath;
/**
* custom paint that can be used.
* this will ignore the thickness and color styles.
*/
private Paint mCustomPaint;
/**
* creates a series without data
*/
public LineGraphSeries() {
init();
}
/**
* creates a series with data
*
* @param data data points
*/
public LineGraphSeries(E[] data) {
super(data);
init();
}
/**
* do the initialization
* creates internal objects
*/
protected void init() {
mStyles = new Styles();
mPaint = new Paint();
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStyle(Paint.Style.STROKE);
mPaintBackground = new Paint();
mPathBackground = new Path();
mPath = new Path();
}
/**
* plots the series
* draws the line and the background
*
* @param graphView graphview
* @param canvas canvas
* @param isSecondScale flag if it is the second scale
*/
@Override
public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) {
resetDataPoints();
// get data
double maxX = graphView.getViewport().getMaxX(false);
double minX = graphView.getViewport().getMinX(false);
double maxY;
double minY;
if (isSecondScale) {
maxY = graphView.getSecondScale().getMaxY();
minY = graphView.getSecondScale().getMinY();
} else {
maxY = graphView.getViewport().getMaxY(false);
minY = graphView.getViewport().getMinY(false);
}
Iterator<E> values = getValues(minX, maxX);
// draw background
double lastEndY = 0;
double lastEndX = 0;
// draw data
mPaint.setStrokeWidth(mStyles.thickness);
mPaint.setColor(getColor());
mPaintBackground.setColor(mStyles.backgroundColor);
Paint paint;
if (mCustomPaint != null) {
paint = mCustomPaint;
} else {
paint = mPaint;
}
if (mStyles.drawBackground) {
mPathBackground.reset();
}
double diffY = maxY - minY;
double diffX = maxX - minX;
float graphHeight = graphView.getGraphContentHeight();
float graphWidth = graphView.getGraphContentWidth();
float graphLeft = graphView.getGraphContentLeft();
float graphTop = graphView.getGraphContentTop();
lastEndY = 0;
lastEndX = 0;
double lastUsedEndX = 0;
float firstX = 0;
int i=0;
while (values.hasNext()) {
E value = values.next();
double valY = value.getY() - minY;
double ratY = valY / diffY;
double y = graphHeight * ratY;
double valX = value.getX() - minX;
double ratX = valX / diffX;
double x = graphWidth * ratX;
double orgX = x;
double orgY = y;
if (i > 0) {
// overdraw
if (x > graphWidth) { // end right
double b = ((graphWidth - lastEndX) * (y - lastEndY)/(x - lastEndX));
y = lastEndY+b;
x = graphWidth;
}
if (y < 0) { // end bottom
double b = ((0 - lastEndY) * (x - lastEndX)/(y - lastEndY));
x = lastEndX+b;
y = 0;
}
if (y > graphHeight) { // end top
double b = ((graphHeight - lastEndY) * (x - lastEndX)/(y - lastEndY));
x = lastEndX+b;
y = graphHeight;
}
if (lastEndY < 0) { // start bottom
double b = ((0 - y) * (x - lastEndX)/(lastEndY - y));
lastEndX = x-b;
lastEndY = 0;
}
if (lastEndX < 0) { // start left
double b = ((0 - x) * (y - lastEndY)/(lastEndX - x));
lastEndY = y-b;
lastEndX = 0;
}
if (lastEndY > graphHeight) { // start top
double b = ((graphHeight - y) * (x - lastEndX)/(lastEndY - y));
lastEndX = x-b;
lastEndY = graphHeight;
}
float startX = (float) lastEndX + (graphLeft + 1);
float startY = (float) (graphTop - lastEndY) + graphHeight;
float endX = (float) x + (graphLeft + 1);
float endY = (float) (graphTop - y) + graphHeight;
// draw data point
if (mStyles.drawDataPoints) {
//fix: last value was not drawn. Draw here now the end values
canvas.drawCircle(endX, endY, mStyles.dataPointsRadius, mPaint);
}
registerDataPoint(endX, endY, value);
mPath.reset();
mPath.moveTo(startX, startY);
mPath.lineTo(endX, endY);
canvas.drawPath(mPath, paint);
if (mStyles.drawBackground) {
if (i==1) {
firstX = startX;
mPathBackground.moveTo(startX, startY);
}
mPathBackground.lineTo(endX, endY);
}
lastUsedEndX = endX;
} else if (mStyles.drawDataPoints) {
//fix: last value not drawn as datapoint. Draw first point here, and then on every step the end values (above)
float first_X = (float) x + (graphLeft + 1);
float first_Y = (float) (graphTop - y) + graphHeight;
//TODO canvas.drawCircle(first_X, first_Y, dataPointsRadius, mPaint);
}
lastEndY = orgY;
lastEndX = orgX;
i++;
}
if (mStyles.drawBackground) {
// end / close path
mPathBackground.lineTo((float) lastUsedEndX, graphHeight + graphTop);
mPathBackground.lineTo(firstX, graphHeight + graphTop);
mPathBackground.close();
canvas.drawPath(mPathBackground, mPaintBackground);
}
}
/**
* the thickness of the line.
* This option will be ignored if you are
* using a custom paint via {@link #setCustomPaint(android.graphics.Paint)}
*
* @return the thickness of the line
*/
public int getThickness() {
return mStyles.thickness;
}
/**
* the thickness of the line.
* This option will be ignored if you are
* using a custom paint via {@link #setCustomPaint(android.graphics.Paint)}
*
* @param thickness thickness of the line
*/
public void setThickness(int thickness) {
mStyles.thickness = thickness;
}
/**
* flag whether the area under the line to the bottom
* of the viewport will be filled with a
* specific background color.
*
* @return whether the background will be drawn
* @see #getBackgroundColor()
*/
public boolean isDrawBackground() {
return mStyles.drawBackground;
}
/**
* flag whether the area under the line to the bottom
* of the viewport will be filled with a
* specific background color.
*
* @param drawBackground whether the background will be drawn
* @see #setBackgroundColor(int)
*/
public void setDrawBackground(boolean drawBackground) {
mStyles.drawBackground = drawBackground;
}
/**
* flag whether the data points are highlighted as
* a visible point.
*
* @return flag whether the data points are highlighted
* @see #setDataPointsRadius(float)
*/
public boolean isDrawDataPoints() {
return mStyles.drawDataPoints;
}
/**
* flag whether the data points are highlighted as
* a visible point.
*
* @param drawDataPoints flag whether the data points are highlighted
* @see #setDataPointsRadius(float)
*/
public void setDrawDataPoints(boolean drawDataPoints) {
mStyles.drawDataPoints = drawDataPoints;
}
/**
* @return the radius for the data points.
* @see #setDrawDataPoints(boolean)
*/
public float getDataPointsRadius() {
return mStyles.dataPointsRadius;
}
/**
* @param dataPointsRadius the radius for the data points.
* @see #setDrawDataPoints(boolean)
*/
public void setDataPointsRadius(float dataPointsRadius) {
mStyles.dataPointsRadius = dataPointsRadius;
}
/**
* @return the background color for the filling under
* the line.
* @see #setDrawBackground(boolean)
*/
public int getBackgroundColor() {
return mStyles.backgroundColor;
}
/**
* @param backgroundColor the background color for the filling under
* the line.
* @see #setDrawBackground(boolean)
*/
public void setBackgroundColor(int backgroundColor) {
mStyles.backgroundColor = backgroundColor;
}
/**
* custom paint that can be used.
* this will ignore the thickness and color styles.
*
* @param customPaint the custom paint to be used for rendering the line
*/
public void setCustomPaint(Paint customPaint) {
this.mCustomPaint = customPaint;
}
}

View file

@ -0,0 +1,38 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.series;
/**
* Listener for the tap event which will be
* triggered when the user touches on a datapoint.
*
* Use this in {@link com.jjoe64.graphview.series.BaseSeries#setOnDataPointTapListener(OnDataPointTapListener)}
*
* @author jjoe64
*/
public interface OnDataPointTapListener {
/**
* gets called when the user touches on a datapoint.
*
* @param series the corresponding series
* @param dataPoint the data point that was tapped on
*/
void onTap(Series series, DataPointInterface dataPoint);
}

View file

@ -0,0 +1,312 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.series;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import com.jjoe64.graphview.GraphView;
import java.util.Iterator;
/**
* Series that plots the data as points.
* The points can be different shapes or a
* complete custom drawing.
*
* @author jjoe64
*/
public class PointsGraphSeries<E extends DataPointInterface> extends BaseSeries<E> {
/**
* interface to implement a custom
* drawing for the data points.
*/
public static interface CustomShape {
/**
* called when drawing a single data point.
* use the x and y coordinates to render your
* drawing at this point.
*
* @param canvas canvas to draw on
* @param paint internal paint object. this has the correct color.
* But you can use your own paint.
* @param x x-coordinate the point has to be drawn to
* @param y y-coordinate the point has to be drawn to
* @param dataPoint the related data point
*/
void draw(Canvas canvas, Paint paint, float x, float y, DataPointInterface dataPoint);
}
/**
* choose a predefined shape to render for
* each data point.
* You can also render a custom drawing via {@link com.jjoe64.graphview.series.PointsGraphSeries.CustomShape}
*/
public enum Shape {
/**
* draws a point / circle
*/
POINT,
/**
* draws a triangle
*/
TRIANGLE,
/**
* draws a rectangle
*/
RECTANGLE
}
/**
* wrapped styles for this series
*/
private final class Styles {
/**
* this is used for the size of the shape that
* will be drawn.
* This is useless if you are using a custom shape.
*/
float size;
/**
* the shape that will be drawn for each point.
*/
Shape shape;
}
/**
* wrapped styles
*/
private Styles mStyles;
/**
* internal paint object
*/
private Paint mPaint;
/**
* handler to use a custom drawing
*/
private CustomShape mCustomShape;
/**
* creates the series without data
*/
public PointsGraphSeries() {
init();
}
/**
* creates the series with data
*
* @param data datapoints
*/
public PointsGraphSeries(E[] data) {
super(data);
init();
}
/**
* inits the internal objects
* set the defaults
*/
protected void init() {
mStyles = new Styles();
mStyles.size = 20f;
mPaint = new Paint();
mPaint.setStrokeCap(Paint.Cap.ROUND);
setShape(Shape.POINT);
}
/**
* plot the data to the viewport
*
* @param graphView graphview
* @param canvas canvas to draw on
* @param isSecondScale whether it is the second scale
*/
@Override
public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) {
resetDataPoints();
// get data
double maxX = graphView.getViewport().getMaxX(false);
double minX = graphView.getViewport().getMinX(false);
double maxY;
double minY;
if (isSecondScale) {
maxY = graphView.getSecondScale().getMaxY();
minY = graphView.getSecondScale().getMinY();
} else {
maxY = graphView.getViewport().getMaxY(false);
minY = graphView.getViewport().getMinY(false);
}
Iterator<E> values = getValues(minX, maxX);
// draw background
double lastEndY = 0;
double lastEndX = 0;
// draw data
mPaint.setColor(getColor());
double diffY = maxY - minY;
double diffX = maxX - minX;
float graphHeight = graphView.getGraphContentHeight();
float graphWidth = graphView.getGraphContentWidth();
float graphLeft = graphView.getGraphContentLeft();
float graphTop = graphView.getGraphContentTop();
lastEndY = 0;
lastEndX = 0;
float firstX = 0;
int i=0;
while (values.hasNext()) {
E value = values.next();
double valY = value.getY() - minY;
double ratY = valY / diffY;
double y = graphHeight * ratY;
double valX = value.getX() - minX;
double ratX = valX / diffX;
double x = graphWidth * ratX;
double orgX = x;
double orgY = y;
// overdraw
boolean overdraw = false;
if (x > graphWidth) { // end right
overdraw = true;
}
if (y < 0) { // end bottom
overdraw = true;
}
if (y > graphHeight) { // end top
overdraw = true;
}
float endX = (float) x + (graphLeft + 1);
float endY = (float) (graphTop - y) + graphHeight;
registerDataPoint(endX, endY, value);
// draw data point
if (!overdraw) {
if (mCustomShape != null) {
mCustomShape.draw(canvas, mPaint, endX, endY, value);
} else if (mStyles.shape == Shape.POINT) {
canvas.drawCircle(endX, endY, mStyles.size, mPaint);
} else if (mStyles.shape == Shape.RECTANGLE) {
canvas.drawRect(endX-mStyles.size, endY-mStyles.size, endX+mStyles.size, endY+mStyles.size, mPaint);
} else if (mStyles.shape == Shape.TRIANGLE) {
Point[] points = new Point[3];
points[0] = new Point((int)endX, (int)(endY-getSize()));
points[1] = new Point((int)(endX+getSize()), (int)(endY+getSize()*0.67));
points[2] = new Point((int)(endX-getSize()), (int)(endY+getSize()*0.67));
drawArrows(points, canvas, mPaint);
}
}
i++;
}
}
/**
* helper to render triangle
*
* @param point array with 3 coordinates
* @param canvas canvas to draw on
* @param paint paint object
*/
private void drawArrows(Point[] point, Canvas canvas, Paint paint) {
float [] points = new float[8];
points[0] = point[0].x;
points[1] = point[0].y;
points[2] = point[1].x;
points[3] = point[1].y;
points[4] = point[2].x;
points[5] = point[2].y;
points[6] = point[0].x;
points[7] = point[0].y;
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, 8, points, 0, null, 0, null, 0, null, 0, 0, paint);
Path path = new Path();
path.moveTo(point[0].x , point[0].y);
path.lineTo(point[1].x,point[1].y);
path.lineTo(point[2].x,point[2].y);
canvas.drawPath(path,paint);
}
/**
* This is used for the size of the shape that
* will be drawn.
* This is useless if you are using a custom shape.
*
* @return the size of the shape
*/
public float getSize() {
return mStyles.size;
}
/**
* This is used for the size of the shape that
* will be drawn.
* This is useless if you are using a custom shape.
*
* @param radius the size of the shape
*/
public void setSize(float radius) {
mStyles.size = radius;
}
/**
* @return the shape that will be drawn for each point
*/
public Shape getShape() {
return mStyles.shape;
}
/**
* @param s the shape that will be drawn for each point
*/
public void setShape(Shape s) {
mStyles.shape = s;
}
/**
* Use a custom handler to render your own
* drawing for each data point.
*
* @param shape handler to use a custom drawing
*/
public void setCustomShape(CustomShape shape) {
mCustomShape = shape;
}
}

View file

@ -0,0 +1,125 @@
/**
* GraphView
* Copyright (C) 2014 Jonas Gehring
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* with the "Linking Exception", which can be found at the license.txt
* file in this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* with the "Linking Exception" along with this program; if not,
* write to the author Jonas Gehring <g.jjoe64@gmail.com>.
*/
package com.jjoe64.graphview.series;
import android.graphics.Canvas;
import com.jjoe64.graphview.GraphView;
import java.util.Iterator;
/**
* Basis interface for series that can be plotted
* on the graph.
* You can implement this in order to create a completely
* custom series type.
* But it is recommended to extend {@link com.jjoe64.graphview.series.BaseSeries} or another
* implemented Series class to save time.
* Anyway this interface can make sense if you want to implement
* a custom data provider, because BaseSeries uses a internal Array to store
* the data.
*
* @author jjoe64
*/
public interface Series<E extends DataPointInterface> {
/**
* @return the lowest x-value of the data
*/
public double getLowestValueX();
/**
* @return the highest x-value of the data
*/
public double getHighestValueX();
/**
* @return the lowest y-value of the data
*/
public double getLowestValueY();
/**
* @return the highest y-value of the data
*/
public double getHighestValueY();
/**
* get the values for a specific range. It is
* important that the data comes in the sorted order
* (from lowest to highest x-value).
*
* @param from the minimal x-value
* @param until the maximal x-value
* @return all datapoints between the from and until x-value
* including the from and until data points.
*/
public Iterator<E> getValues(double from, double until);
/**
* Plots the series to the viewport.
* You have to care about overdrawing.
* This method may be called 2 times: one for
* the default scale and one time for the
* second scale.
*
* @param graphView corresponding graphview
* @param canvas canvas to draw on
* @param isSecondScale true if the drawing is for the second scale
*/
public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale);
/**
* @return the title of the series. Used in the legend
*/
public String getTitle();
/**
* @return the color of the series. Used in the legend and should
* be used for the plotted points or lines.
*/
public int getColor();
/**
* set a listener for tap on a data point.
*
* @param l listener
*/
public void setOnDataPointTapListener(OnDataPointTapListener l);
/**
* called by the tap detector in order to trigger
* the on tap on datapoint event.
*
* @param x pixel
* @param y pixel
*/
void onTap(float x, float y);
/**
* called when the series was added to a graph
*
* @param graphView graphview
*/
void onGraphViewAttached(GraphView graphView);
/**
* @return whether there are data points
*/
boolean isEmpty();
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="GraphViewXML">
<attr name="seriesData" format="string" />
<attr name="seriesType" format="string" />
<attr name="seriesTitle" format="string" />
<attr name="android:title" />
<attr name="seriesColor" format="color" />
</declare-styleable>
</resources>

View file

@ -19,3 +19,4 @@ include ':diaconn'
include ':openhumans' include ':openhumans'
include ':shared' include ':shared'
include ':iconify' include ':iconify'
include ':graphview'