diff --git a/app/build.gradle b/app/build.gradle
index bb72725052..51246830f1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -170,6 +170,7 @@ dependencies {
wearApp project(':wear')
implementation project(':iconify')
+ implementation project(':graphview')
implementation project(':shared')
implementation project(':core')
implementation project(':automation')
diff --git a/automation/build.gradle b/automation/build.gradle
index e1107c676f..f778782309 100644
--- a/automation/build.gradle
+++ b/automation/build.gradle
@@ -17,4 +17,5 @@ dependencies {
implementation project(':core')
implementation project(':database')
implementation project(':shared')
+ implementation project(':graphview')
}
\ No newline at end of file
diff --git a/core/build.gradle b/core/build.gradle
index f1b51650c2..567918dc1b 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -14,6 +14,7 @@ apply from: "${project.rootDir}/core/jacoco_global.gradle"
dependencies {
implementation project(':shared')
implementation project(':database')
+ implementation project(':graphview')
}
android {
diff --git a/core/core_dependencies.gradle b/core/core_dependencies.gradle
index 320e025b5c..60ef09a37d 100644
--- a/core/core_dependencies.gradle
+++ b/core/core_dependencies.gradle
@@ -46,9 +46,6 @@ dependencies {
api 'com.madgag.spongycastle:core:1.58.0.0'
api "com.google.crypto.tink:tink-android:$tink_version"
- // Graphview cannot be upgraded
- api "com.jjoe64:graphview:4.0.1"
-
//db
api "com.j256.ormlite:ormlite-core:$ormLite_version"
api "com.j256.ormlite:ormlite-android:$ormLite_version"
diff --git a/graphview/.gitignore b/graphview/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/graphview/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/graphview/build.gradle b/graphview/build.gradle
new file mode 100644
index 0000000000..e1138b7d40
--- /dev/null
+++ b/graphview/build.gradle
@@ -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"
+}
\ No newline at end of file
diff --git a/graphview/consumer-rules.pro b/graphview/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/graphview/proguard-rules.pro b/graphview/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/graphview/proguard-rules.pro
@@ -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
\ No newline at end of file
diff --git a/graphview/src/main/AndroidManifest.xml b/graphview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..a5918e68ab
--- /dev/null
+++ b/graphview/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/graphview/src/main/java/com/jjoe64/graphview/DefaultLabelFormatter.java b/graphview/src/main/java/com/jjoe64/graphview/DefaultLabelFormatter.java
new file mode 100644
index 0000000000..0a761e2d73
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/DefaultLabelFormatter.java
@@ -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 .
+ */
+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);
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/GraphView.java b/graphview/src/main/java/com/jjoe64/graphview/GraphView.java
new file mode 100644
index 0000000000..51debe1eef
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/GraphView.java
@@ -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 .
+ */
+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 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();
+ 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 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);
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java b/graphview/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java
new file mode 100644
index 0000000000..e6060c9b50
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java
@@ -0,0 +1,1467 @@
+/**
+ * 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 .
+ */
+package com.jjoe64.graphview;
+
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.TypedValue;
+
+import androidx.core.view.ViewCompat;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * The default renderer for the grid
+ * and the labels.
+ *
+ * @author jjoe64
+ */
+public class GridLabelRenderer {
+ /**
+ * wrapper for the styles regarding
+ * to the grid and the labels
+ */
+ public final class Styles {
+ /**
+ * the general text size of the axis titles.
+ * can be overwritten with #verticalAxisTitleTextSize
+ * and #horizontalAxisTitleTextSize
+ */
+ public float textSize;
+
+ /**
+ * the alignment of the vertical labels
+ */
+ public Paint.Align verticalLabelsAlign;
+
+ /**
+ * the alignment of the labels on the right side
+ */
+ public Paint.Align verticalLabelsSecondScaleAlign;
+
+ /**
+ * the color of the vertical labels
+ */
+ public int verticalLabelsColor;
+
+ /**
+ * the color of the labels on the right side
+ */
+ public int verticalLabelsSecondScaleColor;
+
+ /**
+ * the color of the horizontal labels
+ */
+ public int horizontalLabelsColor;
+
+ /**
+ * the color of the grid lines
+ */
+ public int gridColor;
+
+ /**
+ * flag whether the zero-lines (vertical+
+ * horizontal) shall be highlighted
+ */
+ public boolean highlightZeroLines;
+
+ /**
+ * the padding around the graph and labels
+ */
+ public int padding;
+
+ /**
+ * font size of the vertical axis title
+ */
+ public float verticalAxisTitleTextSize;
+
+ /**
+ * font color of the vertical axis title
+ */
+ public int verticalAxisTitleColor;
+
+ /**
+ * font size of the horizontal axis title
+ */
+ public float horizontalAxisTitleTextSize;
+
+ /**
+ * font color of the horizontal axis title
+ */
+ public int horizontalAxisTitleColor;
+
+ /**
+ * flag whether the horizontal labels are
+ * visible
+ */
+ boolean horizontalLabelsVisible;
+
+ /**
+ * flag whether the vertical labels are
+ * visible
+ */
+ boolean verticalLabelsVisible;
+
+ /**
+ * defines which lines will be drawn in the background
+ */
+ GridStyle gridStyle;
+
+ /**
+ * the space between the labels text and the graph content
+ */
+ int labelsSpace;
+ }
+
+ /**
+ * Definition which lines will be drawn in the background
+ */
+ public enum GridStyle {
+ BOTH, VERTICAL, HORIZONTAL, NONE;
+
+ public boolean drawVertical() { return this == BOTH || this == VERTICAL && this != NONE; }
+ public boolean drawHorizontal() { return this == BOTH || this == HORIZONTAL && this != NONE; }
+ }
+
+ /**
+ * wraps the styles regarding the
+ * grid and labels
+ */
+ protected Styles mStyles;
+
+ /**
+ * reference to graphview
+ */
+ private final GraphView mGraphView;
+
+ /**
+ * cache of the vertical steps
+ * (horizontal lines and vertical labels)
+ * Key = Pixel (y)
+ * Value = y-value
+ */
+ private Map mStepsVertical;
+
+ /**
+ * cache of the vertical steps for the
+ * second scale, which is on the right side
+ * (horizontal lines and vertical labels)
+ * Key = Pixel (y)
+ * Value = y-value
+ */
+ private Map mStepsVerticalSecondScale;
+
+ /**
+ * cache of the horizontal steps
+ * (vertical lines and horizontal labels)
+ * Key = Pixel (x)
+ * Value = x-value
+ */
+ private Map mStepsHorizontal;
+
+ /**
+ * the paint to draw the grid lines
+ */
+ private Paint mPaintLine;
+
+ /**
+ * the paint to draw the labels
+ */
+ private Paint mPaintLabel;
+
+ /**
+ * the paint to draw axis titles
+ */
+ private Paint mPaintAxisTitle;
+
+ /**
+ * flag whether is bounds are automatically
+ * adjusted for nice human-readable numbers
+ */
+ private boolean mIsAdjusted;
+
+ /**
+ * the width of the vertical labels
+ */
+ private Integer mLabelVerticalWidth;
+
+ /**
+ * indicates if the width was set manually
+ */
+ private boolean mLabelVerticalWidthFixed;
+
+ /**
+ * the height of the vertical labels
+ */
+ private Integer mLabelVerticalHeight;
+
+ /**
+ * indicates if the height was set manually
+ */
+ private boolean mLabelHorizontalHeightFixed;
+
+ /**
+ * the width of the vertical labels
+ * of the second scale
+ */
+ private Integer mLabelVerticalSecondScaleWidth;
+
+ /**
+ * the height of the vertical labels
+ * of the second scale
+ */
+ private Integer mLabelVerticalSecondScaleHeight;
+
+ /**
+ * the width of the horizontal labels
+ */
+ private Integer mLabelHorizontalWidth;
+
+ /**
+ * the height of the horizontal labels
+ */
+ private Integer mLabelHorizontalHeight;
+
+ /**
+ * the label formatter, that converts
+ * the raw numbers to strings
+ */
+ private LabelFormatter mLabelFormatter;
+
+ /**
+ * the title of the horizontal axis
+ */
+ private String mHorizontalAxisTitle;
+
+ /**
+ * the title of the vertical axis
+ */
+ private String mVerticalAxisTitle;
+
+ /**
+ * count of the vertical labels, that
+ * will be shown at one time.
+ */
+ private int mNumVerticalLabels;
+
+ /**
+ * count of the horizontal labels, that
+ * will be shown at one time.
+ */
+ private int mNumHorizontalLabels;
+
+ /**
+ * create the default grid label renderer.
+ *
+ * @param graphView the corresponding graphview object
+ */
+ public GridLabelRenderer(GraphView graphView) {
+ mGraphView = graphView;
+ setLabelFormatter(new DefaultLabelFormatter());
+ mStyles = new Styles();
+ resetStyles();
+ mNumVerticalLabels = 5;
+ mNumHorizontalLabels = 5;
+ }
+
+ /**
+ * resets the styles. This loads the style
+ * from reading the values of the current
+ * theme.
+ */
+ public void resetStyles() {
+ // get matching styles from theme
+ TypedValue typedValue = new TypedValue();
+ mGraphView.getContext().getTheme().resolveAttribute(android.R.attr.textAppearanceSmall, typedValue, true);
+
+ int color1;
+ int color2;
+ int size;
+ int size2;
+
+ TypedArray array = null;
+ try {
+ array = mGraphView.getContext().obtainStyledAttributes(typedValue.data, new int[]{
+ android.R.attr.textColorPrimary
+ , android.R.attr.textColorSecondary
+ , android.R.attr.textSize
+ , android.R.attr.horizontalGap});
+ color1 = array.getColor(0, Color.BLACK);
+ color2 = array.getColor(1, Color.GRAY);
+ size = array.getDimensionPixelSize(2, 20);
+ size2 = array.getDimensionPixelSize(3, 20);
+ array.recycle();
+ } catch (Exception e) {
+ color1 = Color.BLACK;
+ color2 = Color.GRAY;
+ size = 20;
+ size2 = 20;
+ }
+
+ mStyles.verticalLabelsColor = color1;
+ mStyles.verticalLabelsSecondScaleColor = color1;
+ mStyles.horizontalLabelsColor = color1;
+ mStyles.gridColor = color2;
+ mStyles.textSize = size;
+ mStyles.padding = size2;
+ mStyles.labelsSpace = (int) mStyles.textSize/5;
+
+ mStyles.verticalLabelsAlign = Paint.Align.RIGHT;
+ mStyles.verticalLabelsSecondScaleAlign = Paint.Align.LEFT;
+ mStyles.highlightZeroLines = true;
+
+ mStyles.verticalAxisTitleColor = mStyles.verticalLabelsColor;
+ mStyles.horizontalAxisTitleColor = mStyles.horizontalLabelsColor;
+ mStyles.verticalAxisTitleTextSize = mStyles.textSize;
+ mStyles.horizontalAxisTitleTextSize = mStyles.textSize;
+
+ mStyles.horizontalLabelsVisible = true;
+ mStyles.verticalLabelsVisible = true;
+
+ mStyles.gridStyle = GridStyle.BOTH;
+
+ reloadStyles();
+ }
+
+ /**
+ * will load the styles to the internal
+ * paint objects (color, text size, text align)
+ */
+ public void reloadStyles() {
+ mPaintLine = new Paint();
+ mPaintLine.setColor(mStyles.gridColor);
+ mPaintLine.setStrokeWidth(0);
+
+ mPaintLabel = new Paint();
+ mPaintLabel.setTextSize(getTextSize());
+
+ mPaintAxisTitle = new Paint();
+ mPaintAxisTitle.setTextSize(getTextSize());
+ mPaintAxisTitle.setTextAlign(Paint.Align.CENTER);
+ }
+
+ /**
+ * @return the general text size for the axis titles
+ */
+ public float getTextSize() {
+ return mStyles.textSize;
+ }
+
+ /**
+ * @return the font color of the vertical labels
+ */
+ public int getVerticalLabelsColor() {
+ return mStyles.verticalLabelsColor;
+ }
+
+ /**
+ * @return the alignment of the text of the
+ * vertical labels
+ */
+ public Paint.Align getVerticalLabelsAlign() {
+ return mStyles.verticalLabelsAlign;
+ }
+
+ /**
+ * @return the font color of the horizontal labels
+ */
+ public int getHorizontalLabelsColor() {
+ return mStyles.horizontalLabelsColor;
+ }
+
+ /**
+ * clears the internal cache and forces
+ * to redraw the grid and labels.
+ * Normally you should always call {@link GraphView#onDataChanged(boolean, boolean)}
+ * which will call this method.
+ *
+ * @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 invalidate(boolean keepLabelsSize, boolean keepViewport) {
+ if (!keepViewport) {
+ mIsAdjusted = false;
+ }
+ if (!keepLabelsSize) {
+ if (!mLabelVerticalWidthFixed) {
+ mLabelVerticalWidth = null;
+ }
+ mLabelVerticalHeight = null;
+ mLabelVerticalSecondScaleWidth = null;
+ mLabelVerticalSecondScaleHeight = null;
+ }
+ //reloadStyles();
+ }
+
+ /**
+ * calculates the vertical steps of
+ * the second scale.
+ * This will not do any automatically update
+ * of the bounds.
+ * Use always manual bounds for the second scale.
+ *
+ * @return true if it is ready
+ */
+ protected boolean adjustVerticalSecondScale() {
+ if (mLabelHorizontalHeight == null) {
+ return false;
+ }
+ if (mGraphView.mSecondScale == null) {
+ return true;
+ }
+
+ double minY = mGraphView.mSecondScale.getMinY();
+ double maxY = mGraphView.mSecondScale.getMaxY();
+
+ // TODO find the number of labels
+ int numVerticalLabels = mNumVerticalLabels;
+
+ double newMinY;
+ double exactSteps;
+
+ if (mGraphView.mSecondScale.isYAxisBoundsManual()) {
+ newMinY = minY;
+ double rangeY = maxY - newMinY;
+ exactSteps = rangeY / (numVerticalLabels - 1);
+ } else {
+ // TODO auto adjusting
+ throw new IllegalStateException("Not yet implemented");
+ }
+
+ double newMaxY = newMinY + (numVerticalLabels - 1) * exactSteps;
+
+ // TODO auto adjusting
+ //mGraphView.getViewport().setMinY(newMinY);
+ //mGraphView.getViewport().setMaxY(newMaxY);
+
+ //if (!mGraphView.getViewport().isYAxisBoundsManual()) {
+ // mGraphView.getViewport().setYAxisBoundsStatus(Viewport.AxisBoundsStatus.AUTO_ADJUSTED);
+ //}
+
+ if (mStepsVerticalSecondScale != null) {
+ mStepsVerticalSecondScale.clear();
+ } else {
+ mStepsVerticalSecondScale = new LinkedHashMap(numVerticalLabels);
+ }
+ int height = mGraphView.getGraphContentHeight();
+ double v = newMaxY;
+ int p = mGraphView.getGraphContentTop(); // start
+ int pixelStep = height / (numVerticalLabels - 1);
+ for (int i = 0; i < numVerticalLabels; i++) {
+ mStepsVerticalSecondScale.put(p, v);
+ p += pixelStep;
+ v -= exactSteps;
+ }
+
+ return true;
+ }
+
+ /**
+ * calculates the vertical steps. This will
+ * automatically change the bounds to nice
+ * human-readable min/max.
+ *
+ * @return true if it is ready
+ */
+ protected boolean adjustVertical() {
+ if (mLabelHorizontalHeight == null) {
+ return false;
+ }
+
+ double minY = mGraphView.getViewport().getMinY(false);
+ double maxY = mGraphView.getViewport().getMaxY(false);
+
+ if (minY == maxY) {
+ return false;
+ }
+
+ // TODO find the number of labels
+ int numVerticalLabels = mNumVerticalLabels;
+
+ double newMinY;
+ double exactSteps;
+
+ if (mGraphView.getViewport().isYAxisBoundsManual()) {
+ newMinY = minY;
+ double rangeY = maxY - newMinY;
+ exactSteps = rangeY / (numVerticalLabels - 1);
+ } else {
+ // find good steps
+ boolean adjusting = true;
+ newMinY = minY;
+ exactSteps = 0d;
+ while (adjusting) {
+ double rangeY = maxY - newMinY;
+ exactSteps = rangeY / (numVerticalLabels - 1);
+ exactSteps = humanRound(exactSteps, true);
+
+ // adjust viewport
+ // wie oft passt STEP in minY rein?
+ int count = 0;
+ if (newMinY >= 0d) {
+ // positive number
+ while (newMinY - exactSteps >= 0) {
+ newMinY -= exactSteps;
+ count++;
+ }
+ newMinY = exactSteps * count;
+ } else {
+ // negative number
+ count++;
+ while (newMinY + exactSteps < 0) {
+ newMinY += exactSteps;
+ count++;
+ }
+ newMinY = exactSteps * count * -1;
+ }
+
+ // wenn minY sich geändert hat, steps nochmal berechnen
+ // wenn nicht, fertig
+ if (newMinY == minY) {
+ adjusting = false;
+ } else {
+ minY = newMinY;
+ }
+ }
+ }
+
+ double newMaxY = newMinY + (numVerticalLabels - 1) * exactSteps;
+ mGraphView.getViewport().setMinY(newMinY);
+ mGraphView.getViewport().setMaxY(newMaxY);
+
+ if (!mGraphView.getViewport().isYAxisBoundsManual()) {
+ mGraphView.getViewport().setYAxisBoundsStatus(Viewport.AxisBoundsStatus.AUTO_ADJUSTED);
+ }
+
+ if (mStepsVertical != null) {
+ mStepsVertical.clear();
+ } else {
+ mStepsVertical = new LinkedHashMap(numVerticalLabels);
+ }
+ int height = mGraphView.getGraphContentHeight();
+ double v = newMaxY;
+ int p = mGraphView.getGraphContentTop(); // start
+ int pixelStep = height / (numVerticalLabels - 1);
+ for (int i = 0; i < numVerticalLabels; i++) {
+ mStepsVertical.put(p, v);
+ p += pixelStep;
+ v -= exactSteps;
+ }
+
+ return true;
+ }
+
+ /**
+ * calculates the horizontal steps. This will
+ * automatically change the bounds to nice
+ * human-readable min/max.
+ *
+ * @return true if it is ready
+ */
+ protected boolean adjustHorizontal() {
+ if (mLabelVerticalWidth == null) {
+ return false;
+ }
+
+ double minX = mGraphView.getViewport().getMinX(false);
+ double maxX = mGraphView.getViewport().getMaxX(false);
+ if (minX == maxX) return false;
+
+ // TODO find the number of labels
+ int numHorizontalLabels = mNumHorizontalLabels;
+
+ double newMinX;
+ double exactSteps;
+
+ float scalingOffset = 0f;
+ if (mGraphView.getViewport().isXAxisBoundsManual() && mGraphView.getViewport().getXAxisBoundsStatus() != Viewport.AxisBoundsStatus.READJUST_AFTER_SCALE) {
+ // scaling
+ if (mGraphView.getViewport().mScalingActive) {
+ minX = mGraphView.getViewport().mScalingBeginLeft;
+ maxX = minX + mGraphView.getViewport().mScalingBeginWidth;
+
+ //numHorizontalLabels *= (mGraphView.getViewport().mCurrentViewport.width()+oldStep)/(mGraphView.getViewport().mScalingBeginWidth+oldStep);
+ //numHorizontalLabels = (float) Math.ceil(numHorizontalLabels);
+ }
+
+ newMinX = minX;
+ double rangeX = maxX - newMinX;
+ exactSteps = rangeX / (numHorizontalLabels - 1);
+ } else {
+ // find good steps
+ boolean adjusting = true;
+ newMinX = minX;
+ exactSteps = 0d;
+ while (adjusting) {
+ double rangeX = maxX - newMinX;
+ exactSteps = rangeX / (numHorizontalLabels - 1);
+
+ boolean roundAlwaysUp = true;
+ if (mGraphView.getViewport().getXAxisBoundsStatus() == Viewport.AxisBoundsStatus.READJUST_AFTER_SCALE) {
+ // if viewports gets smaller, round down
+ if (mGraphView.getViewport().mCurrentViewport.width() < mGraphView.getViewport().mScalingBeginWidth) {
+ roundAlwaysUp = false;
+ }
+ }
+ exactSteps = humanRound(exactSteps, roundAlwaysUp);
+
+ // adjust viewport
+ // wie oft passt STEP in minX rein?
+ int count = 0;
+ if (newMinX >= 0d) {
+ // positive number
+ while (newMinX - exactSteps >= 0) {
+ newMinX -= exactSteps;
+ count++;
+ }
+ newMinX = exactSteps * count;
+ } else {
+ // negative number
+ count++;
+ while (newMinX + exactSteps < 0) {
+ newMinX += exactSteps;
+ count++;
+ }
+ newMinX = exactSteps * count * -1;
+ }
+
+ // wenn minX sich geändert hat, steps nochmal berechnen
+ // wenn nicht, fertig
+ if (newMinX == minX) {
+ adjusting = false;
+ } else {
+ minX = newMinX;
+ }
+ }
+
+ double newMaxX = newMinX + (numHorizontalLabels - 1) * exactSteps;
+ mGraphView.getViewport().setMinX(newMinX);
+ mGraphView.getViewport().setMaxX(newMaxX);
+ if (mGraphView.getViewport().getXAxisBoundsStatus() == Viewport.AxisBoundsStatus.READJUST_AFTER_SCALE) {
+ mGraphView.getViewport().setXAxisBoundsStatus(Viewport.AxisBoundsStatus.FIX);
+ } else {
+ mGraphView.getViewport().setXAxisBoundsStatus(Viewport.AxisBoundsStatus.AUTO_ADJUSTED);
+ }
+ }
+
+ if (mStepsHorizontal != null) {
+ mStepsHorizontal.clear();
+ } else {
+ mStepsHorizontal = new LinkedHashMap((int) numHorizontalLabels);
+ }
+ int width = mGraphView.getGraphContentWidth();
+
+ float scrolled = 0;
+ float scrolledPixels = 0;
+
+ double v = newMinX;
+ int p = mGraphView.getGraphContentLeft(); // start
+ float pixelStep = width / (numHorizontalLabels - 1);
+
+ if (mGraphView.getViewport().mScalingActive) {
+ float oldStep = mGraphView.getViewport().mScalingBeginWidth / (numHorizontalLabels - 1);
+ float factor = (mGraphView.getViewport().mCurrentViewport.width() + oldStep) / (mGraphView.getViewport().mScalingBeginWidth + oldStep);
+ pixelStep *= 1f / factor;
+
+ //numHorizontalLabels *= (mGraphView.getViewport().mCurrentViewport.width()+oldStep)/(mGraphView.getViewport().mScalingBeginWidth+oldStep);
+ //numHorizontalLabels = (float) Math.ceil(numHorizontalLabels);
+
+ //scrolled = ((float) mGraphView.getViewport().getMinX(false) - mGraphView.getViewport().mScalingBeginLeft)*2;
+ float newWidth = width * 1f / factor;
+ scrolledPixels = (newWidth - width) * -0.5f;
+
+ }
+
+ // scrolling
+ if (!Float.isNaN(mGraphView.getViewport().mScrollingReferenceX)) {
+ scrolled = mGraphView.getViewport().mScrollingReferenceX - (float) newMinX;
+ scrolledPixels += scrolled * (pixelStep / (float) exactSteps);
+
+ if (scrolled < 0 - exactSteps) {
+ mGraphView.getViewport().mScrollingReferenceX += exactSteps;
+ } else if (scrolled > exactSteps) {
+ mGraphView.getViewport().mScrollingReferenceX -= exactSteps;
+ }
+ }
+ p += scrolledPixels;
+ v += scrolled;
+
+ for (int i = 0; i < numHorizontalLabels; i++) {
+ // don't draw steps before 0 (scrolling)
+ if (p >= mGraphView.getGraphContentLeft()) {
+ mStepsHorizontal.put(p, v);
+ }
+ p += pixelStep;
+ v += exactSteps;
+ }
+
+ return true;
+ }
+
+ /**
+ * adjusts the grid and labels to match to the data
+ * this will automatically change the bounds to
+ * nice human-readable values, except the bounds
+ * are manual.
+ */
+ protected void adjust() {
+ mIsAdjusted = adjustVertical();
+ mIsAdjusted &= adjustVerticalSecondScale();
+ mIsAdjusted &= adjustHorizontal();
+ }
+
+ /**
+ * calculates the vertical label size
+ * @param canvas canvas
+ */
+ protected void calcLabelVerticalSize(Canvas canvas) {
+ // test label with first and last label
+ String testLabel = mLabelFormatter.formatLabel(mGraphView.getViewport().getMaxY(false), false);
+ if (testLabel == null) testLabel = "";
+
+ Rect textBounds = new Rect();
+ mPaintLabel.getTextBounds(testLabel, 0, testLabel.length(), textBounds);
+ mLabelVerticalWidth = textBounds.width();
+ mLabelVerticalHeight = textBounds.height();
+
+ testLabel = mLabelFormatter.formatLabel(mGraphView.getViewport().getMinY(false), false);
+ if (testLabel == null) testLabel = "";
+
+ mPaintLabel.getTextBounds(testLabel, 0, testLabel.length(), textBounds);
+ mLabelVerticalWidth = Math.max(mLabelVerticalWidth, textBounds.width());
+
+ // add some pixel to get a margin
+ mLabelVerticalWidth += 6;
+
+ // space between text and graph content
+ mLabelVerticalWidth += mStyles.labelsSpace;
+
+ // multiline
+ int lines = 1;
+ for (byte c : testLabel.getBytes()) {
+ if (c == '\n') lines++;
+ }
+ mLabelVerticalHeight *= lines;
+ }
+
+ /**
+ * calculates the vertical second scale
+ * label size
+ * @param canvas canvas
+ */
+ protected void calcLabelVerticalSecondScaleSize(Canvas canvas) {
+ if (mGraphView.mSecondScale == null) {
+ mLabelVerticalSecondScaleWidth = 0;
+ mLabelVerticalSecondScaleHeight = 0;
+ return;
+ }
+
+ // test label
+ double testY = ((mGraphView.mSecondScale.getMaxY() - mGraphView.mSecondScale.getMinY()) * 0.783) + mGraphView.mSecondScale.getMinY();
+ String testLabel = mGraphView.mSecondScale.getLabelFormatter().formatLabel(testY, false);
+ Rect textBounds = new Rect();
+ mPaintLabel.getTextBounds(testLabel, 0, testLabel.length(), textBounds);
+ mLabelVerticalSecondScaleWidth = textBounds.width();
+ mLabelVerticalSecondScaleHeight = textBounds.height();
+
+ // multiline
+ int lines = 1;
+ for (byte c : testLabel.getBytes()) {
+ if (c == '\n') lines++;
+ }
+ mLabelVerticalSecondScaleHeight *= lines;
+ }
+
+ /**
+ * calculates the horizontal label size
+ * @param canvas canvas
+ */
+ protected void calcLabelHorizontalSize(Canvas canvas) {
+ // test label
+ double testX = ((mGraphView.getViewport().getMaxX(false) - mGraphView.getViewport().getMinX(false)) * 0.783) + mGraphView.getViewport().getMinX(false);
+ String testLabel = mLabelFormatter.formatLabel(testX, true);
+ if (testLabel == null) {
+ testLabel = "";
+ }
+ Rect textBounds = new Rect();
+ mPaintLabel.getTextBounds(testLabel, 0, testLabel.length(), textBounds);
+ mLabelHorizontalWidth = textBounds.width();
+
+ if (!mLabelHorizontalHeightFixed) {
+ mLabelHorizontalHeight = textBounds.height();
+
+ // multiline
+ int lines = 1;
+ for (byte c : testLabel.getBytes()) {
+ if (c == '\n') lines++;
+ }
+ mLabelHorizontalHeight *= lines;
+
+ mLabelHorizontalHeight = (int) Math.max(mLabelHorizontalHeight, mStyles.textSize);
+ }
+
+ // space between text and graph content
+ mLabelHorizontalHeight += mStyles.labelsSpace;
+ }
+
+ /**
+ * do the drawing of the grid
+ * and labels
+ * @param canvas canvas
+ */
+ public void draw(Canvas canvas) {
+ boolean labelSizeChanged = false;
+ if (mLabelHorizontalWidth == null) {
+ calcLabelHorizontalSize(canvas);
+ labelSizeChanged = true;
+ }
+ if (mLabelVerticalWidth == null) {
+ calcLabelVerticalSize(canvas);
+ labelSizeChanged = true;
+ }
+ if (mLabelVerticalSecondScaleWidth == null) {
+ calcLabelVerticalSecondScaleSize(canvas);
+ labelSizeChanged = true;
+ }
+ if (labelSizeChanged) {
+ // redraw
+ ViewCompat.postInvalidateOnAnimation(mGraphView);
+ return;
+ }
+
+ if (!mIsAdjusted) {
+ adjust();
+ }
+
+ if (mIsAdjusted) {
+ drawVerticalSteps(canvas);
+ drawVerticalStepsSecondScale(canvas);
+ drawHorizontalSteps(canvas);
+ } else {
+ // we can not draw anything
+ return;
+ }
+
+ drawHorizontalAxisTitle(canvas);
+ drawVerticalAxisTitle(canvas);
+ }
+
+ /**
+ * draws the horizontal axis title if
+ * it is set
+ * @param canvas canvas
+ */
+ protected void drawHorizontalAxisTitle(Canvas canvas) {
+ if (mHorizontalAxisTitle != null && mHorizontalAxisTitle.length() > 0) {
+ mPaintAxisTitle.setColor(getHorizontalAxisTitleColor());
+ mPaintAxisTitle.setTextSize(getHorizontalAxisTitleTextSize());
+ float x = canvas.getWidth() / 2;
+ float y = canvas.getHeight() - mStyles.padding;
+ canvas.drawText(mHorizontalAxisTitle, x, y, mPaintAxisTitle);
+ }
+ }
+
+ /**
+ * draws the vertical axis title if
+ * it is set
+ * @param canvas canvas
+ */
+ protected void drawVerticalAxisTitle(Canvas canvas) {
+ if (mVerticalAxisTitle != null && mVerticalAxisTitle.length() > 0) {
+ mPaintAxisTitle.setColor(getVerticalAxisTitleColor());
+ mPaintAxisTitle.setTextSize(getVerticalAxisTitleTextSize());
+ float x = getVerticalAxisTitleWidth();
+ float y = canvas.getHeight() / 2;
+ canvas.save();
+ canvas.rotate(-90, x, y);
+ canvas.drawText(mVerticalAxisTitle, x, y, mPaintAxisTitle);
+ canvas.restore();
+ }
+ }
+
+ /**
+ * @return the horizontal axis title height
+ * or 0 if there is no title
+ */
+ public int getHorizontalAxisTitleHeight() {
+ if (mHorizontalAxisTitle != null && mHorizontalAxisTitle.length() > 0) {
+ return (int) getHorizontalAxisTitleTextSize();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * @return the vertical axis title width
+ * or 0 if there is no title
+ */
+ public int getVerticalAxisTitleWidth() {
+ if (mVerticalAxisTitle != null && mVerticalAxisTitle.length() > 0) {
+ return (int) getVerticalAxisTitleTextSize();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * draws the horizontal steps
+ * vertical lines and horizontal labels
+ *
+ * @param canvas canvas
+ */
+ protected void drawHorizontalSteps(Canvas canvas) {
+ // draw horizontal steps (vertical lines and horizontal labels)
+ mPaintLabel.setColor(getHorizontalLabelsColor());
+ int i = 0;
+ for (Map.Entry e : mStepsHorizontal.entrySet()) {
+ // draw line
+ if (mStyles.highlightZeroLines) {
+ if (e.getValue() == 0d) {
+ mPaintLine.setStrokeWidth(5);
+ } else {
+ mPaintLine.setStrokeWidth(0);
+ }
+ }
+ if (mStyles.gridStyle.drawVertical()) {
+ canvas.drawLine(e.getKey(), mGraphView.getGraphContentTop(), e.getKey(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mPaintLine);
+ }
+
+ // draw label
+ if (isHorizontalLabelsVisible()) {
+ mPaintLabel.setTextAlign(Paint.Align.CENTER);
+ if (i == mStepsHorizontal.size() - 1)
+ mPaintLabel.setTextAlign(Paint.Align.RIGHT);
+ if (i == 0)
+ mPaintLabel.setTextAlign(Paint.Align.LEFT);
+
+ // multiline labels
+ String label = mLabelFormatter.formatLabel(e.getValue(), true);
+ if (label == null) {
+ label = "";
+ }
+ String[] lines = label.split("\n");
+ for (int li = 0; li < lines.length; li++) {
+ // for the last line y = height
+ float y = (canvas.getHeight() - mStyles.padding - getHorizontalAxisTitleHeight()) - (lines.length - li - 1) * getTextSize() * 1.1f + mStyles.labelsSpace;
+ canvas.drawText(lines[li], e.getKey(), y, mPaintLabel);
+ }
+ }
+ i++;
+ }
+ }
+
+ /**
+ * draws the vertical steps for the
+ * second scale on the right side
+ *
+ * @param canvas canvas
+ */
+ protected void drawVerticalStepsSecondScale(Canvas canvas) {
+ if (mGraphView.mSecondScale == null) {
+ return;
+ }
+
+ // draw only the vertical labels on the right
+ float startLeft = mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth();
+ mPaintLabel.setColor(getVerticalLabelsSecondScaleColor());
+ mPaintLabel.setTextAlign(getVerticalLabelsSecondScaleAlign());
+ for (Map.Entry e : mStepsVerticalSecondScale.entrySet()) {
+ // draw label
+ int labelsWidth = mLabelVerticalSecondScaleWidth;
+ int labelsOffset = (int) startLeft;
+ if (getVerticalLabelsSecondScaleAlign() == Paint.Align.RIGHT) {
+ labelsOffset += labelsWidth;
+ } else if (getVerticalLabelsSecondScaleAlign() == Paint.Align.CENTER) {
+ labelsOffset += labelsWidth / 2;
+ }
+
+ float y = e.getKey();
+
+ String[] lines = mGraphView.mSecondScale.mLabelFormatter.formatLabel(e.getValue(), false).split("\n");
+ y += (lines.length * getTextSize() * 1.1f) / 2; // center text vertically
+ for (int li = 0; li < lines.length; li++) {
+ // for the last line y = height
+ float y2 = y - (lines.length - li - 1) * getTextSize() * 1.1f;
+ canvas.drawText(lines[li], labelsOffset, y2, mPaintLabel);
+ }
+ }
+ }
+
+ /**
+ * draws the vertical steps
+ * horizontal lines and vertical labels
+ *
+ * @param canvas canvas
+ */
+ protected void drawVerticalSteps(Canvas canvas) {
+ // draw vertical steps (horizontal lines and vertical labels)
+ float startLeft = mGraphView.getGraphContentLeft();
+ mPaintLabel.setColor(getVerticalLabelsColor());
+ mPaintLabel.setTextAlign(getVerticalLabelsAlign());
+ for (Map.Entry e : mStepsVertical.entrySet()) {
+ // draw line
+ if (mStyles.highlightZeroLines) {
+ if (e.getValue() == 0d) {
+ mPaintLine.setStrokeWidth(5);
+ } else {
+ mPaintLine.setStrokeWidth(0);
+ }
+ }
+ if (mStyles.gridStyle.drawHorizontal()) {
+ canvas.drawLine(startLeft, e.getKey(), startLeft + mGraphView.getGraphContentWidth(), e.getKey(), mPaintLine);
+ }
+
+ // draw label
+ if (isVerticalLabelsVisible()) {
+ int labelsWidth = mLabelVerticalWidth;
+ int labelsOffset = 0;
+ if (getVerticalLabelsAlign() == Paint.Align.RIGHT) {
+ labelsOffset = labelsWidth;
+ labelsOffset -= mStyles.labelsSpace;
+ } else if (getVerticalLabelsAlign() == Paint.Align.CENTER) {
+ labelsOffset = labelsWidth / 2;
+ }
+ labelsOffset += mStyles.padding + getVerticalAxisTitleWidth();
+
+ float y = e.getKey();
+
+ String label = mLabelFormatter.formatLabel(e.getValue(), false);
+ if (label == null) {
+ label = "";
+ }
+ String[] lines = label.split("\n");
+ y += (lines.length * getTextSize() * 1.1f) / 2; // center text vertically
+ for (int li = 0; li < lines.length; li++) {
+ // for the last line y = height
+ float y2 = y - (lines.length - li - 1) * getTextSize() * 1.1f;
+ canvas.drawText(lines[li], labelsOffset, y2, mPaintLabel);
+ }
+ }
+ }
+ }
+
+ /**
+ * this will do rounding to generate
+ * nice human-readable bounds.
+ *
+ * @param in the raw value that is to be rounded
+ * @param roundAlwaysUp true if it shall always round up (ceil)
+ * @return the rounded number
+ */
+ protected double humanRound(double in, boolean roundAlwaysUp) {
+ // round-up to 1-steps, 2-steps or 5-steps
+ int ten = 0;
+ while (in >= 10d) {
+ in /= 10d;
+ ten++;
+ }
+ while (in < 1d) {
+ in *= 10d;
+ ten--;
+ }
+ if (roundAlwaysUp) {
+ if (in == 1d) {
+ } else if (in <= 2d) {
+ in = 2d;
+ } else if (in <= 5d) {
+ in = 5d;
+ } else if (in < 10d) {
+ in = 10d;
+ }
+ } else { // always round down
+ if (in == 1d) {
+ } else if (in <= 4.9d) {
+ in = 2d;
+ } else if (in <= 9.9d) {
+ in = 5d;
+ } else if (in < 15d) {
+ in = 10d;
+ }
+ }
+ return in * Math.pow(10d, ten);
+ }
+
+ /**
+ * @return the wrapped styles
+ */
+ public Styles getStyles() {
+ return mStyles;
+ }
+
+ /**
+ * @return the vertical label width
+ * 0 if there are no vertical labels
+ */
+ public int getLabelVerticalWidth() {
+ return mLabelVerticalWidth == null || !isVerticalLabelsVisible() ? 0 : mLabelVerticalWidth;
+ }
+
+ /**
+ * sets a manual and fixed with of the space for
+ * the vertical labels. This will prevent GraphView to
+ * calculate the width automatically.
+ *
+ * @param width the width of the space for the vertical labels.
+ * Use null to let GraphView automatically calculate the width.
+ */
+ public void setLabelVerticalWidth(Integer width) {
+ mLabelVerticalWidth = width;
+ mLabelVerticalWidthFixed = mLabelVerticalWidth != null;
+ }
+
+ /**
+ * @return the horizontal label height
+ * 0 if there are no horizontal labels
+ */
+ public int getLabelHorizontalHeight() {
+ return mLabelHorizontalHeight == null || !isHorizontalLabelsVisible() ? 0 : mLabelHorizontalHeight;
+ }
+
+ /**
+ * sets a manual and fixed height of the space for
+ * the horizontal labels. This will prevent GraphView to
+ * calculate the height automatically.
+ *
+ * @param height the height of the space for the horizontal labels.
+ * Use null to let GraphView automatically calculate the height.
+ */
+ public void setLabelHorizontalHeight(Integer height) {
+ mLabelHorizontalHeight = height;
+ mLabelHorizontalHeightFixed = mLabelHorizontalHeight != null;
+ }
+
+ /**
+ * @return the grid line color
+ */
+ public int getGridColor() {
+ return mStyles.gridColor;
+ }
+
+ /**
+ * @return whether the line at 0 are highlighted
+ */
+ public boolean isHighlightZeroLines() {
+ return mStyles.highlightZeroLines;
+ }
+
+ /**
+ * @return the padding around the grid and labels
+ */
+ public int getPadding() {
+ return mStyles.padding;
+ }
+
+ /**
+ * @param textSize the general text size of the axis titles.
+ * can be overwritten with {@link #setVerticalAxisTitleTextSize(float)}
+ * and {@link #setHorizontalAxisTitleTextSize(float)}
+ */
+ public void setTextSize(float textSize) {
+ mStyles.textSize = textSize;
+ }
+
+ /**
+ * @param verticalLabelsAlign the alignment of the vertical labels
+ */
+ public void setVerticalLabelsAlign(Paint.Align verticalLabelsAlign) {
+ mStyles.verticalLabelsAlign = verticalLabelsAlign;
+ }
+
+ /**
+ * @param verticalLabelsColor the color of the vertical labels
+ */
+ public void setVerticalLabelsColor(int verticalLabelsColor) {
+ mStyles.verticalLabelsColor = verticalLabelsColor;
+ }
+
+ /**
+ * @param horizontalLabelsColor the color of the horizontal labels
+ */
+ public void setHorizontalLabelsColor(int horizontalLabelsColor) {
+ mStyles.horizontalLabelsColor = horizontalLabelsColor;
+ }
+
+ /**
+ * @param gridColor the color of the grid lines
+ */
+ public void setGridColor(int gridColor) {
+ mStyles.gridColor = gridColor;
+ }
+
+ /**
+ * @param highlightZeroLines flag whether the zero-lines (vertical+
+ * horizontal) shall be highlighted
+ */
+ public void setHighlightZeroLines(boolean highlightZeroLines) {
+ mStyles.highlightZeroLines = highlightZeroLines;
+ }
+
+ /**
+ * @param padding the padding around the graph and labels
+ */
+ public void setPadding(int padding) {
+ mStyles.padding = padding;
+ }
+
+ /**
+ * @return the label formatter, that converts
+ * the raw numbers to strings
+ */
+ public LabelFormatter getLabelFormatter() {
+ return mLabelFormatter;
+ }
+
+ /**
+ * @param mLabelFormatter the label formatter, that converts
+ * the raw numbers to strings
+ */
+ public void setLabelFormatter(LabelFormatter mLabelFormatter) {
+ this.mLabelFormatter = mLabelFormatter;
+ mLabelFormatter.setViewport(mGraphView.getViewport());
+ }
+
+ /**
+ * @return the title of the horizontal axis
+ */
+ public String getHorizontalAxisTitle() {
+ return mHorizontalAxisTitle;
+ }
+
+ /**
+ * @param mHorizontalAxisTitle the title of the horizontal axis
+ */
+ public void setHorizontalAxisTitle(String mHorizontalAxisTitle) {
+ this.mHorizontalAxisTitle = mHorizontalAxisTitle;
+ }
+
+ /**
+ * @return the title of the vertical axis
+ */
+ public String getVerticalAxisTitle() {
+ return mVerticalAxisTitle;
+ }
+
+ /**
+ * @param mVerticalAxisTitle the title of the vertical axis
+ */
+ public void setVerticalAxisTitle(String mVerticalAxisTitle) {
+ this.mVerticalAxisTitle = mVerticalAxisTitle;
+ }
+
+ /**
+ * @return font size of the vertical axis title
+ */
+ public float getVerticalAxisTitleTextSize() {
+ return mStyles.verticalAxisTitleTextSize;
+ }
+
+ /**
+ * @param verticalAxisTitleTextSize font size of the vertical axis title
+ */
+ public void setVerticalAxisTitleTextSize(float verticalAxisTitleTextSize) {
+ mStyles.verticalAxisTitleTextSize = verticalAxisTitleTextSize;
+ }
+
+ /**
+ * @return font color of the vertical axis title
+ */
+ public int getVerticalAxisTitleColor() {
+ return mStyles.verticalAxisTitleColor;
+ }
+
+ /**
+ * @param verticalAxisTitleColor font color of the vertical axis title
+ */
+ public void setVerticalAxisTitleColor(int verticalAxisTitleColor) {
+ mStyles.verticalAxisTitleColor = verticalAxisTitleColor;
+ }
+
+ /**
+ * @return font size of the horizontal axis title
+ */
+ public float getHorizontalAxisTitleTextSize() {
+ return mStyles.horizontalAxisTitleTextSize;
+ }
+
+ /**
+ * @param horizontalAxisTitleTextSize font size of the horizontal axis title
+ */
+ public void setHorizontalAxisTitleTextSize(float horizontalAxisTitleTextSize) {
+ mStyles.horizontalAxisTitleTextSize = horizontalAxisTitleTextSize;
+ }
+
+ /**
+ * @return font color of the horizontal axis title
+ */
+ public int getHorizontalAxisTitleColor() {
+ return mStyles.horizontalAxisTitleColor;
+ }
+
+ /**
+ * @param horizontalAxisTitleColor font color of the horizontal axis title
+ */
+ public void setHorizontalAxisTitleColor(int horizontalAxisTitleColor) {
+ mStyles.horizontalAxisTitleColor = horizontalAxisTitleColor;
+ }
+
+ /**
+ * @return the alignment of the labels on the right side
+ */
+ public Paint.Align getVerticalLabelsSecondScaleAlign() {
+ return mStyles.verticalLabelsSecondScaleAlign;
+ }
+
+ /**
+ * @param verticalLabelsSecondScaleAlign the alignment of the labels on the right side
+ */
+ public void setVerticalLabelsSecondScaleAlign(Paint.Align verticalLabelsSecondScaleAlign) {
+ mStyles.verticalLabelsSecondScaleAlign = verticalLabelsSecondScaleAlign;
+ }
+
+ /**
+ * @return the color of the labels on the right side
+ */
+ public int getVerticalLabelsSecondScaleColor() {
+ return mStyles.verticalLabelsSecondScaleColor;
+ }
+
+ /**
+ * @param verticalLabelsSecondScaleColor the color of the labels on the right side
+ */
+ public void setVerticalLabelsSecondScaleColor(int verticalLabelsSecondScaleColor) {
+ mStyles.verticalLabelsSecondScaleColor = verticalLabelsSecondScaleColor;
+ }
+
+ /**
+ * @return the width of the vertical labels
+ * of the second scale
+ */
+ public int getLabelVerticalSecondScaleWidth() {
+ return mLabelVerticalSecondScaleWidth==null?0:mLabelVerticalSecondScaleWidth;
+ }
+
+ /**
+ * @return flag whether the horizontal labels are
+ * visible
+ */
+ public boolean isHorizontalLabelsVisible() {
+ return mStyles.horizontalLabelsVisible;
+ }
+
+ /**
+ * @param horizontalTitleVisible flag whether the horizontal labels are
+ * visible
+ */
+ public void setHorizontalLabelsVisible(boolean horizontalTitleVisible) {
+ mStyles.horizontalLabelsVisible = horizontalTitleVisible;
+ }
+
+ /**
+ * @return flag whether the vertical labels are
+ * visible
+ */
+ public boolean isVerticalLabelsVisible() {
+ return mStyles.verticalLabelsVisible;
+ }
+
+ /**
+ * @param verticalTitleVisible flag whether the vertical labels are
+ * visible
+ */
+ public void setVerticalLabelsVisible(boolean verticalTitleVisible) {
+ mStyles.verticalLabelsVisible = verticalTitleVisible;
+ }
+
+ /**
+ * @return count of the vertical labels, that
+ * will be shown at one time.
+ */
+ public int getNumVerticalLabels() {
+ return mNumVerticalLabels;
+ }
+
+ /**
+ * @param mNumVerticalLabels count of the vertical labels, that
+ * will be shown at one time.
+ */
+ public void setNumVerticalLabels(int mNumVerticalLabels) {
+ this.mNumVerticalLabels = mNumVerticalLabels;
+ }
+
+ /**
+ * @return count of the horizontal labels, that
+ * will be shown at one time.
+ */
+ public int getNumHorizontalLabels() {
+ return mNumHorizontalLabels;
+ }
+
+ /**
+ * @param mNumHorizontalLabels count of the horizontal labels, that
+ * will be shown at one time.
+ */
+ public void setNumHorizontalLabels(int mNumHorizontalLabels) {
+ this.mNumHorizontalLabels = mNumHorizontalLabels;
+ }
+
+ /**
+ * @return the grid style
+ */
+ public GridStyle getGridStyle() {
+ return mStyles.gridStyle;
+ }
+
+ /**
+ * Define which grid lines shall be drawn
+ *
+ * @param gridStyle the grid style
+ */
+ public void setGridStyle(GridStyle gridStyle) {
+ mStyles.gridStyle = gridStyle;
+ }
+
+ /**
+ * @return the space between the labels text and the graph content
+ */
+ public int getLabelsSpace() {
+ return mStyles.labelsSpace;
+ }
+
+ /**
+ * the space between the labels text and the graph content
+ *
+ * @param labelsSpace the space between the labels text and the graph content
+ */
+ public void setLabelsSpace(int labelsSpace) {
+ mStyles.labelsSpace = labelsSpace;
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/LabelFormatter.java b/graphview/src/main/java/com/jjoe64/graphview/LabelFormatter.java
new file mode 100644
index 0000000000..80aad2990a
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/LabelFormatter.java
@@ -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 .
+ */
+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);
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/LegendRenderer.java b/graphview/src/main/java/com/jjoe64/graphview/LegendRenderer.java
new file mode 100644
index 0000000000..db901577f7
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/LegendRenderer.java
@@ -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 .
+ */
+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 allSeries = new ArrayList();
+ 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);
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/SecondScale.java b/graphview/src/main/java/com/jjoe64/graphview/SecondScale.java
new file mode 100644
index 0000000000..1fff8ee8a9
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/SecondScale.java
@@ -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 .
+ */
+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 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();
+ 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 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);
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/ValueDependentColor.java b/graphview/src/main/java/com/jjoe64/graphview/ValueDependentColor.java
new file mode 100644
index 0000000000..a02f22a21e
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/ValueDependentColor.java
@@ -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 .
+ */
+
+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 {
+ /**
+ * 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);
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/Viewport.java b/graphview/src/main/java/com/jjoe64/graphview/Viewport.java
new file mode 100644
index 0000000000..de90c05ea2
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/Viewport.java
@@ -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 .
+ */
+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 = 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 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 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");
+ }
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/compat/OverScrollerCompat.java b/graphview/src/main/java/com/jjoe64/graphview/compat/OverScrollerCompat.java
new file mode 100644
index 0000000000..20b497c91f
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/compat/OverScrollerCompat.java
@@ -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 .
+ */
+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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphview/src/main/java/com/jjoe64/graphview/helper/DateAsXAxisLabelFormatter.java b/graphview/src/main/java/com/jjoe64/graphview/helper/DateAsXAxisLabelFormatter.java
new file mode 100644
index 0000000000..1e9fa3cff4
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/helper/DateAsXAxisLabelFormatter.java
@@ -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 .
+ */
+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);
+ }
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/helper/GraphViewXML.java b/graphview/src/main/java/com/jjoe64/graphview/helper/GraphViewXML.java
new file mode 100644
index 0000000000..d036805bc8
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/helper/GraphViewXML.java
@@ -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 .
+ */
+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 app:seriesData
+ * in the format: "X=Y;X=Y;..." e.g. "0=5.0;1=5;2=4;3=9"
+ *
+ * Other styling options:
+ * app:seriesType="line|bar|points"
+ * app:seriesColor="#ff0000"
+ * app:seriesTitle="foobar" - if this is set, the legend will be drawn
+ * android:title="foobar"
+ *
+ * Example:
+ *
+ * {@code
+ *
+ * }
+ *
+ *
+ * @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 series;
+ if (type == null || type.isEmpty()) {
+ type = "line";
+ }
+ if (type.equals("line")) {
+ series = new LineGraphSeries(data);
+ } else if (type.equals("bar")) {
+ series = new BarGraphSeries(data);
+ } else if (type.equals("points")) {
+ series = new PointsGraphSeries(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);
+ }
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java b/graphview/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java
new file mode 100644
index 0000000000..4b38dbf4c2
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java
@@ -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);
+ }
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java b/graphview/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java
new file mode 100644
index 0000000000..8db7bac94b
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java
@@ -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 .
+ */
+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 extends BaseSeries {
+ /**
+ * 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 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 mDataPoints = new HashMap();
+
+ /**
+ * 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 xVals = new TreeSet();
+ 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 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 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 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 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 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;
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java b/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java
new file mode 100644
index 0000000000..8ecdf23a73
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java
@@ -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 .
+ */
+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 implements Series {
+ /**
+ * holds the data
+ */
+ final private List mData = new ArrayList();
+
+ /**
+ * 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 mDataPoints = new HashMap();
+
+ /**
+ * 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 mGraphViews;
+
+ /**
+ * creates series without data
+ */
+ public BaseSeries() {
+ mGraphViews = new ArrayList();
+ }
+
+ /**
+ * 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();
+ 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 getValues(final double from, final double until) {
+ if (from <= getLowestValueX() && until >= getHighestValueX()) {
+ return mData.iterator();
+ } else {
+ return new Iterator() {
+ Iterator 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 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();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/series/DataPoint.java b/graphview/src/main/java/com/jjoe64/graphview/series/DataPoint.java
new file mode 100644
index 0000000000..b5f5eb3ef3
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/series/DataPoint.java
@@ -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 .
+ */
+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+"]";
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java b/graphview/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java
new file mode 100644
index 0000000000..9be683bef8
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java
@@ -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 .
+ */
+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();
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java b/graphview/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java
new file mode 100644
index 0000000000..4d721c2e6c
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java
@@ -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 .
+ */
+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 extends BaseSeries {
+ /**
+ * 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 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;
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java b/graphview/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java
new file mode 100644
index 0000000000..748a1122ee
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java
@@ -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 .
+ */
+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);
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java b/graphview/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java
new file mode 100644
index 0000000000..c57d476d7b
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java
@@ -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 .
+ */
+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 extends BaseSeries {
+ /**
+ * 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 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;
+ }
+}
diff --git a/graphview/src/main/java/com/jjoe64/graphview/series/Series.java b/graphview/src/main/java/com/jjoe64/graphview/series/Series.java
new file mode 100644
index 0000000000..dce32eb78e
--- /dev/null
+++ b/graphview/src/main/java/com/jjoe64/graphview/series/Series.java
@@ -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 .
+ */
+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 {
+ /**
+ * @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 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();
+}
diff --git a/graphview/src/main/res/values/attr.xml b/graphview/src/main/res/values/attr.xml
new file mode 100644
index 0000000000..8b73838605
--- /dev/null
+++ b/graphview/src/main/res/values/attr.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
index 20cc1cc2b6..b11f11adfc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,3 +19,4 @@ include ':diaconn'
include ':openhumans'
include ':shared'
include ':iconify'
+include ':graphview'