diff --git a/.gitignore b/.gitignore
index 916a2c0cc..77cbeee5a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,45 @@
+_Store
+
+
+# Generated by http://gitignore.io
+
+### Eclipse ###
+*.pydevproject
+.project
+.metadata
+bin/**
+tmp/**
+tmp/**/*
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# PDT-specific
+.buildpath
+
+### IntelliJ ###
+*.iml
+*.ipr
+*.iws
+.idea/
+
+### Android ###
# built application files
+*.apk
*.ap_
# files for the dex VM
@@ -10,7 +51,6 @@
# generated files
bin/
gen/
-lint.xml
# Local configuration file (sdk path, etc)
local.properties
@@ -18,29 +58,42 @@ local.properties
# Eclipse project files
.classpath
.project
-.settings
-.checkstyle
-# Maven
-target
-release.properties
-pom.xml.*
+# Proguard folder generated by Eclipse
+proguard/
-# Ant
-build.xml
-ant.properties
-local.properties
-proguard.cfg
-proguard-project.txt
+# Proguard folder generated by Intellij
+proguard_logs/
# Intellij project files
*.iml
*.ipr
*.iws
.idea/
-out/
-gen-external-apklibs/
-# Gradle
-.gradle
-build
\ No newline at end of file
+adt-bundle-windows-x86_64/
+
+### Windows ###
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+### Linux ###
+.*
+!.gitignore
+*~
+# Generated by http://gitignore.io
+
+### Gradle ###
+# Exclude Folder List #
+.gradle/
+build/
+.gradletasknamecache
+gradle.properties
+
diff --git a/GVBar.png b/GVBar.png
deleted file mode 100644
index 7eabad12c..000000000
Binary files a/GVBar.png and /dev/null differ
diff --git a/GVLine.jpg b/GVLine.jpg
deleted file mode 100644
index cb93a390a..000000000
Binary files a/GVLine.jpg and /dev/null differ
diff --git a/README.markdown b/README.markdown
index 0215b7c1d..7732c6a8b 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,50 +1,86 @@
-Chart and Graph Library for Android
-====================================
+# Chart and Graph Library for Android
-
What is GraphView
-GraphView is a library for Android to programmatically create flexible and nice-looking diagramms. It is easy to understand, to integrate and to customize it.
-At the moment there are two different types:
-
-
Line Charts
-
Bar Charts
-
+## Project maintainer wanted! For time reasons I can not continue to maintain GraphView. Contact me if you are interested and serious about this project. g.jjoe64@gmail.com
-Tested on Android 1.6, 2.2, 2.3 and 3.0 (honeycomb, tablet), 4.0.
+## What is GraphView
-
-
-
+GraphView is a library for Android to programmatically create
+flexible and nice-looking diagrams.
+It is **easy** to understand, to integrate and to customize.
-
Features
+Supported graph types:
+* Line Graphs
+* Bar Graphs
+* Point Graphs
+* or implement your own custom types.
-* Two chart types
-Line Chart and Bar Chart.
+
+
+
+
+
+
+## Top Features
+
+* Line Chart, Bar Chart, Points
+* Combination of different graph types
+* Scrolling vertical and horizontal
+. You can scroll with a finger touch move gesture.
+* Scaling / Zooming vertical and horizontal
+. With two-fingers touch scale gesture (Multi-touch), the viewport can be changed.
+* Realtime Graph (Live change of data)
+* Second scale axis
* Draw multiple series of data
-Let the diagram show more that one series in a graph. You can set a color and a description for every series.
+. Let the diagram show more that one series in a graph. You can set a color and a description for every series.
* Show legend
-A legend can be displayed inline the chart. You can set the width and the vertical align (top, middle, bottom).
+. A legend can be displayed inline the chart. You can set the width and the vertical align (top, middle, bottom).
* Custom labels
-The labels for the x- and y-axis are generated automatically. But you can set your own labels, Strings are possible.
+. The labels for the x- and y-axis are generated automatically. But you can set your own labels, Strings are possible.
* Handle incomplete data
-It's possible to give the data in different frequency.
+. It's possible to give the data in different frequency.
* Viewport
-You can limit the viewport so that only a part of the data will be displayed.
-* Scrolling
-You can scroll with a finger touch move gesture.
-* Scaling / Zooming
-Since Android 2.3! With two-fingers touch scale gesture (Multi-touch), the viewport can be changed.
-* Background (line graph)
-Optionally draws a light background under the diagram stroke.
+. You can limit the viewport so that only a part of the data will be displayed.
* Manual Y axis limits
-* Realtime Graph (Live)
-* And more
+* And much more... Check out the project page and/or the demo app
+
+## How to use
+
+1) Add gradle dependency:
+```
+implementation 'com.jjoe64:graphview:4.2.2'
+```
+
+2) Add view to layout:
+```xml
+
+```
-
How to use
-View GraphView page http://android-graphview.org
+3) Add some data:
+```java
+GraphView graph = (GraphView) findViewById(R.id.graph);
+LineGraphSeries series = new LineGraphSeries(new DataPoint[] {
+ new DataPoint(0, 1),
+ new DataPoint(1, 5),
+ new DataPoint(2, 3),
+ new DataPoint(3, 2),
+ new DataPoint(4, 6)
+});
+graph.addSeries(series);
+```
+
+## Download Demo project at Google Play Store
+
+
+Showcase GraphView Demo App
+
+## More examples and documentation
+
+Get started at project wiki homepage
-
Important
To show you how to integrate the library into an existing project see the GraphView-Demos project!
See GraphView-Demos for examples.
https://github.com/jjoe64/GraphView-Demos
-View GraphView page http://android-graphview.org
-
+View GraphView wiki page https://github.com/jjoe64/GraphView/wiki
diff --git a/README.new-version.md b/README.new-version.md
new file mode 100644
index 000000000..8bd815f25
--- /dev/null
+++ b/README.new-version.md
@@ -0,0 +1,59 @@
+How to create a new version for maven repo
+--------------------------------------------
+create sources.jar
+- $ jar cvf sources.jar src
+
+create java doc jar
+- $ mkdir javadoc
+- $ javadoc -d javadoc -sourcepath src/main/java/ -subpackages com.jjoe64
+- $ jar cvf javadoc.jar javadoc
+
+change version in gradle.properties
+
+uncomment part for publishing in build.gradle
+
+(once) create a gpg file
+- gpg --gen-key
+
+(once) publish key
+- gpg --send-keys D8C3B041
+and/or here as ascii
+- gpg --export -a D8C3B041
+- http://keyserver.ubuntu.com:11371/
+
+=> needs some time
+
+hardcode gpg key password in maven_push.gradle
+
+hardcode user/pwd of nexus account in maven_push.gradle
+
+success gradle task uploadArchives
+- ./gradlew --rerun-tasks uploadArchives
+- enter gpg info (id:D8C3B041 / path: /Users/jonas/.gnupg/secring.gpg / PWD)
+
+open https://oss.sonatype.org
+
+login
+
+Staging Repositiories
+
+search: jjoe64
+
+Close entry
+
+Refresh/Wait
+
+Release entry
+
+Wait some days
+
+## update java doc
+
+$ javadoc -d javadoc -sourcepath src/main/java/ -subpackages com.jjoe64
+$ mv javadoc/ ..
+$ git checkout gh-pages
+$ rm -rf javadoc
+$ mv ../javadoc/ .
+$ git add javadoc
+$ git commit -a
+
diff --git a/anim.gif b/anim.gif
new file mode 100644
index 000000000..43e2d90f7
Binary files /dev/null and b/anim.gif differ
diff --git a/build.gradle b/build.gradle
index 8b86123cc..ddd7534ac 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,24 +1,67 @@
buildscript {
repositories {
mavenCentral()
+ google()
+ jcenter()
}
-
dependencies {
- classpath 'com.android.tools.build:gradle:0.6.+'
+ classpath 'com.android.tools.build:gradle:3.5.3'
}
}
-apply plugin: 'android-library'
+wrapper {
+ gradleVersion = '5.6'
+}
+
+apply plugin: 'com.android.library'
+
android {
- compileSdkVersion 17
- buildToolsVersion '17.0.0'
-
- sourceSets {
- main {
- manifest.srcFile 'AndroidManifest.xml'
- java.srcDirs = ['src']
- res.srcDirs = ['res']
+ compileSdkVersion 27
+ buildToolsVersion '28.0.3'
+
+ defaultConfig {
+ minSdkVersion 9
+ targetSdkVersion 27
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
}
}
+ lintOptions {
+ abortOnError false
+ }
+
+}
+
+dependencies {
+ implementation 'androidx.core:core:1.0.0-beta01'
+}
+
+
+//this is used to generate .jar files and push to maven repo
+// This is the actual solution, as in http://stackoverflow.com/a/19037807/1002054
+task clearJar(type: Delete) {
+ delete 'build/outputs/myCompiledLibrary.jar'
+}
+
+task makeJar(type: Copy) {
+ from('build/intermediates/bundles/release/')
+ into('build/outputs/')
+ include('classes.jar')
+ rename ('classes.jar', 'myCompiledLibrary.jar')
+}
+
+makeJar.dependsOn(clearJar, build)
+
+
+apply from: './maven_push.gradle'
+
+repositories {
+ google()
+ mavenCentral()
+ jcenter()
}
\ No newline at end of file
diff --git a/doc-assets/1059439_1.png b/doc-assets/1059439_1.png
new file mode 100644
index 000000000..8be6dde8b
Binary files /dev/null and b/doc-assets/1059439_1.png differ
diff --git a/doc-assets/4000611_1.png b/doc-assets/4000611_1.png
new file mode 100644
index 000000000..a373f0487
Binary files /dev/null and b/doc-assets/4000611_1.png differ
diff --git a/doc-assets/469160_orig_1.png b/doc-assets/469160_orig_1.png
new file mode 100644
index 000000000..7b96d263a
Binary files /dev/null and b/doc-assets/469160_orig_1.png differ
diff --git a/doc-assets/5901645_1.png b/doc-assets/5901645_1.png
new file mode 100644
index 000000000..962804de9
Binary files /dev/null and b/doc-assets/5901645_1.png differ
diff --git a/doc-assets/6316193_orig_1.png b/doc-assets/6316193_orig_1.png
new file mode 100644
index 000000000..f4f6e462a
Binary files /dev/null and b/doc-assets/6316193_orig_1.png differ
diff --git a/doc-assets/9303658_1.png b/doc-assets/9303658_1.png
new file mode 100644
index 000000000..bbba36477
Binary files /dev/null and b/doc-assets/9303658_1.png differ
diff --git a/doc-assets/Screen_Shot_2016_10_08_at_12_19_56_1.png b/doc-assets/Screen_Shot_2016_10_08_at_12_19_56_1.png
new file mode 100644
index 000000000..b5d610643
Binary files /dev/null and b/doc-assets/Screen_Shot_2016_10_08_at_12_19_56_1.png differ
diff --git a/doc-assets/Screen_Shot_2016_10_08_at_12_20_56_1.png b/doc-assets/Screen_Shot_2016_10_08_at_12_20_56_1.png
new file mode 100644
index 000000000..77780bb31
Binary files /dev/null and b/doc-assets/Screen_Shot_2016_10_08_at_12_20_56_1.png differ
diff --git a/doc-assets/Screen_Shot_2016_10_08_at_12_23_38_1.png b/doc-assets/Screen_Shot_2016_10_08_at_12_23_38_1.png
new file mode 100644
index 000000000..745fc291c
Binary files /dev/null and b/doc-assets/Screen_Shot_2016_10_08_at_12_23_38_1.png differ
diff --git a/doc-assets/Screen_Shot_2016_10_08_at_12_24_19_1.png b/doc-assets/Screen_Shot_2016_10_08_at_12_24_19_1.png
new file mode 100644
index 000000000..6f4433146
Binary files /dev/null and b/doc-assets/Screen_Shot_2016_10_08_at_12_24_19_1.png differ
diff --git a/doc-assets/Screenshot_20161008_122642_1_1.png b/doc-assets/Screenshot_20161008_122642_1_1.png
new file mode 100644
index 000000000..8aba95d2f
Binary files /dev/null and b/doc-assets/Screenshot_20161008_122642_1_1.png differ
diff --git a/doc-assets/Screenshot_20161011_210215_1.png b/doc-assets/Screenshot_20161011_210215_1.png
new file mode 100644
index 000000000..0ed502672
Binary files /dev/null and b/doc-assets/Screenshot_20161011_210215_1.png differ
diff --git a/doc-assets/Screenshot_20161012_180242_1.png b/doc-assets/Screenshot_20161012_180242_1.png
new file mode 100644
index 000000000..acb238f26
Binary files /dev/null and b/doc-assets/Screenshot_20161012_180242_1.png differ
diff --git a/doc-assets/Screenshot_20161012_180257_1.png b/doc-assets/Screenshot_20161012_180257_1.png
new file mode 100644
index 000000000..dca262fe7
Binary files /dev/null and b/doc-assets/Screenshot_20161012_180257_1.png differ
diff --git a/doc-assets/Screenshot_20161012_180325_1.png b/doc-assets/Screenshot_20161012_180325_1.png
new file mode 100644
index 000000000..f20b1436c
Binary files /dev/null and b/doc-assets/Screenshot_20161012_180325_1.png differ
diff --git a/doc-assets/Screenshot_20161012_180336_1.png b/doc-assets/Screenshot_20161012_180336_1.png
new file mode 100644
index 000000000..ae45497de
Binary files /dev/null and b/doc-assets/Screenshot_20161012_180336_1.png differ
diff --git a/doc-assets/Screenshot_20161012_180355_1.png b/doc-assets/Screenshot_20161012_180355_1.png
new file mode 100644
index 000000000..f9797826c
Binary files /dev/null and b/doc-assets/Screenshot_20161012_180355_1.png differ
diff --git a/doc-assets/Screenshot_20161012_180404_1.png b/doc-assets/Screenshot_20161012_180404_1.png
new file mode 100644
index 000000000..069978e9d
Binary files /dev/null and b/doc-assets/Screenshot_20161012_180404_1.png differ
diff --git a/doc-assets/snapshotshare_1.png b/doc-assets/snapshotshare_1.png
new file mode 100644
index 000000000..82901109b
Binary files /dev/null and b/doc-assets/snapshotshare_1.png differ
diff --git a/doc/allclasses-frame.html b/doc/allclasses-frame.html
deleted file mode 100644
index 999a3111e..000000000
--- a/doc/allclasses-frame.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-All Classes
-
-
-
-
-
-
-
-
-
-
-
-All Classes
-
-
-
-if you want to show different labels,
- you can use this label formatter.
- As Input you get the raw value (x or y) and
- you return a String that will be displayed.
- graphView.setCustomLabelFormatter(new CustomLabelFormatter() {
- public String formatLabel(double value, boolean isValueX) {
- if (isValueX) {
- if (value < 5) {
- return "small";
- } else if (value < 15) {
- return "middle";
- } else {
- return "big";
- }
- }
- return null; // let graphview generate Y-axis label for us
- }
- });
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Method Summary
-
-
-
- java.lang.String
-
formatLabel(double value,
- boolean isValueX)
-
-
- will be called when the labels were generated
Returns an array containing the constants of this enum type, in
-the order they are declared. This method may be used to iterate
-over the constants as follows:
-
-for (GraphView.LegendAlign c : GraphView.LegendAlign.values())
- System.out.println(c);
-
-
-
-
-
Returns:
an array containing the constants of this enum type, in
-the order they are declared
Returns the enum constant of this type with the specified name.
-The string must match exactly an identifier used to declare an
-enum constant in this type. (Extraneous whitespace characters are
-not permitted.)
-
-
-
Parameters:
name - the name of the enum constant to be returned.
-
Returns:
the enum constant with the specified name
-
Throws:
-
java.lang.IllegalArgumentException - if this enum type has no constant
-with the specified name
-
java.lang.NullPointerException - if the argument is null
-GraphView is a Android View for creating zoomable and scrollable graphs.
- This is the abstract base class for all graphs. Extend this class and implement #drawSeries(Canvas, GraphViewDataInterface[], float, float, float, double, double, double, double, float) to display a custom graph.
- Use LineGraphView for creating a line chart.
-
-
-
-
-
Author:
-
jjoe64 - jonas gehring - http://www.jjoe64.com
-
- Copyright (C) 2011 Jonas Gehring
- Licensed under the GNU Lesser General Public License (LGPL)
- http://www.gnu.org/licenses/lgpl.html
getMaxX(boolean ignoreViewport)
-
-
- returns the maximal X value of the current viewport (if viewport is set)
- otherwise maximal X value of all data.
-
-
-
-protected double
-
getMaxY()
-
-
- returns the maximal Y value of all data.
-
-
-
-protected double
-
getMinX(boolean ignoreViewport)
-
-
- returns the minimal X value of the current viewport (if viewport is set)
- otherwise minimal X value of all data.
-
-
-
-protected double
-
getMinY()
-
-
- returns the minimal Y value of all data.
setDisableTouch(boolean disableTouch)
-
-
- The user can disable any touch gestures, this is useful if you are using a real time graph, but don't want the user to interact
you have to set the bounds setManualYAxisBounds(double, double). That automatically enables manualYAxis-flag.
- if you want to disable the menual y axis, call this method with false.
-
-This API (Application Programming Interface) document has pages corresponding to the items in the navigation bar, described as follows.
-Package
-
-
-
-Each package has a page that contains a list of its classes and interfaces, with a summary for each. This page can contain four categories:
-
Interfaces (italic)
Classes
Enums
Exceptions
Errors
Annotation Types
-
-
-Class/Interface
-
-
-
-Each class, interface, nested class and nested interface has its own separate page. Each of these pages has three sections consisting of a class/interface description, summary tables, and detailed member descriptions:
-
Class inheritance diagram
Direct Subclasses
All Known Subinterfaces
All Known Implementing Classes
Class/interface declaration
Class/interface description
-
-
Nested Class Summary
Field Summary
Constructor Summary
Method Summary
-
-
Field Detail
Constructor Detail
Method Detail
-Each summary entry contains the first sentence from the detailed description for that item. The summary entries are alphabetical, while the detailed descriptions are in the order they appear in the source code. This preserves the logical groupings established by the programmer.
-
-
-Annotation Type
-
-
-
-Each annotation type has its own separate page with the following sections:
-
Annotation Type declaration
Annotation Type description
Required Element Summary
Optional Element Summary
Element Detail
-
-
-
-Enum
-
-
-
-Each enum has its own separate page with the following sections:
-
Enum declaration
Enum description
Enum Constant Summary
Enum Constant Detail
-
-
-Use
-
-Each documented package, class and interface has its own Use page. This page describes what packages, classes, methods, constructors and fields use any part of the given class or package. Given a class or interface A, its Use page includes subclasses of A, fields declared as A, methods that return A, and methods and constructors with parameters of type A. You can access this page by first going to the package, class or interface, then clicking on the "Use" link in the navigation bar.
-
-Tree (Class Hierarchy)
-
-There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. The classes are organized by inheritance structure starting with java.lang.Object. The interfaces do not inherit from java.lang.Object.
-
When viewing the Overview page, clicking on "Tree" displays the hierarchy for all packages.
When viewing a particular package, class or interface page, clicking "Tree" displays the hierarchy for only that package.
-
-
-Deprecated API
-
-The Deprecated API page lists all of the API that have been deprecated. A deprecated API is not recommended for use, generally due to improvements, and a replacement API is usually given. Deprecated APIs may be removed in future implementations.
-
-Index
-
-The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.
-
-Prev/Next
-These links take you to the next or previous class, interface, package, or related page.
-Frames/No Frames
-These links show and hide the HTML frames. All pages are available with or without frames.
-
-
-Serialized Form
-Each serializable or externalizable class has a description of its serialization fields and methods. This information is of interest to re-implementors, not to developers using the API. While there is no link in the navigation bar, you can get to this information by going to any serialized class and clicking "Serialized Form" in the "See also" section of the class description.
-
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/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java b/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java
new file mode 100644
index 000000000..3bb4156ca
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java
@@ -0,0 +1,225 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java
new file mode 100644
index 000000000..d8ab9ca13
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java
@@ -0,0 +1,526 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jjoe64.graphview.series;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import androidx.core.view.ViewCompat;
+import android.util.Log;
+import android.view.animation.AccelerateInterpolator;
+
+import com.jjoe64.graphview.GraphView;
+import com.jjoe64.graphview.RectD;
+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 {
+ private static final long ANIMATION_DURATION = 333;
+
+ /**
+ * paint to do drawing on canvas
+ */
+ private Paint mPaint;
+
+ /**
+ * custom paint that can be used.
+ * this will ignore the value dependent color.
+ */
+ private Paint mCustomPaint;
+
+ /**
+ * spacing between the bars in percentage.
+ * 0 => no spacing
+ * 100 => the space between the bars is as big as the bars itself
+ */
+ private int mSpacing;
+
+ /**
+ * width of a data point
+ * 0 => no prior knowledge of sampling period, interval between bars will be calculated automatically
+ * >0 => value is the total distance from one bar to another
+ */
+ private double mDataWidth;
+
+ /**
+ * 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();
+
+ /**
+ * flag for animated rendering
+ */
+ private boolean mAnimated;
+
+ /**
+ * store the last value that was animated
+ */
+ private double mLastAnimatedValue = Double.NaN;
+
+ /**
+ * time of start animation
+ */
+ private long mAnimationStart;
+
+ /**
+ * animation interpolator
+ */
+ private AccelerateInterpolator mAnimationInterpolator;
+
+ /**
+ * frame number of animation to avoid lagging
+ */
+ private int mAnimationStartFrameNo;
+
+
+ /**
+ * creates bar series without any data
+ */
+ public BarGraphSeries() {
+ mPaint = new Paint();
+ }
+
+ /**
+ * creates bar series with data
+ *
+ * @param data data points
+ * important: array has to be sorted from lowest x-value to the highest
+ */
+ public BarGraphSeries(E[] data) {
+ super(data);
+ mPaint = new Paint();
+ mAnimationInterpolator = new AccelerateInterpolator(2f);
+ }
+
+ /**
+ * 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);
+
+ 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(false);
+ minY = graphView.getSecondScale().getMinY(false);
+ } 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 minGap = 0;
+
+ if(mDataWidth > 0.0) {
+ minGap = mDataWidth;
+ } else {
+ Double lastVal = null;
+
+ 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
+ int barSlotWidth = numBarSlots == 1
+ ? graphView.getGraphContentWidth()
+ : graphView.getGraphContentWidth() / (numBarSlots-1);
+
+ // Total spacing (both sides) between sets of bars
+ double spacing = Math.min(barSlotWidth*mSpacing/100, barSlotWidth*0.98f);
+ // Width of an individual bar
+ double barWidth = (barSlotWidth - spacing) / numBarSeries;
+ // Offset from the center of a given bar to start drawing
+ double offset = barSlotWidth/2;
+
+ double diffY = maxY - minY;
+ double diffX = maxX - minX;
+ double contentHeight = graphView.getGraphContentHeight();
+ double contentWidth = graphView.getGraphContentWidth();
+ double contentLeft = graphView.getGraphContentLeft();
+ double 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 valueX = value.getX();
+ double valX = valueX - 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());
+ }
+
+ double left = x + contentLeft - offset + spacing/2 + currentSeriesOrder*barWidth;
+ double right = left + barWidth;
+ if (left > contentLeft + contentWidth || right < contentLeft) {
+ continue;
+ }
+ double top = (contentTop - y) + contentHeight;
+ double bottom = (contentTop - y0) + contentHeight - (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1);
+
+ boolean reverse = top > bottom;
+
+ if (mAnimated) {
+ if ((Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) {
+ long currentTime = System.currentTimeMillis();
+ if (mAnimationStart == 0) {
+ // start animation
+ mAnimationStart = currentTime;
+ mAnimationStartFrameNo = 0;
+ } else {
+ // anti-lag: wait a few frames
+ if (mAnimationStartFrameNo < 15) {
+ // second time
+ mAnimationStart = currentTime;
+ mAnimationStartFrameNo++;
+ }
+ }
+ float timeFactor = (float) (currentTime-mAnimationStart) / ANIMATION_DURATION;
+ float factor = mAnimationInterpolator.getInterpolation(timeFactor);
+ if (timeFactor <= 1.0) {
+ double barHeight = bottom - top;
+ barHeight = barHeight * factor;
+ top = bottom-barHeight;
+ ViewCompat.postInvalidateOnAnimation(graphView);
+ } else {
+ // animation finished
+ mLastAnimatedValue = valueX;
+ }
+ }
+ }
+
+ if (reverse) {
+ double 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 RectD(left, top, right, bottom), value);
+
+ Paint p;
+ if (mCustomPaint != null) {
+ p = mCustomPaint;
+ } else {
+ p = mPaint;
+ }
+ canvas.drawRect((float)left, (float)top, (float)right, (float)bottom, p);
+
+ // 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)
+ , (float) (left+right)/2, (float) 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 the interval between data points
+ */
+ public double getDataWidth() {
+ return mDataWidth;
+ }
+
+ /**
+ * @param mDataWidth width of a data point (sampling period)
+ * 0 => no prior knowledge of sampling period, interval between bars will be calculated automatically
+ * >0 => value is the total distance from one bar to another
+ */
+ public void setDataWidth(double mDataWidth) {
+ this.mDataWidth = mDataWidth;
+ }
+
+ /**
+ * @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;
+ }
+
+ /**
+ * custom paint that can be used.
+ * this will ignore the value dependent color.
+ *
+ * @return custom paint or null
+ */
+ public Paint getCustomPaint() {
+ return mCustomPaint;
+ }
+
+ /**
+ * custom paint that can be used.
+ * this will ignore the value dependent color.
+ *
+ * @param mCustomPaint custom paint to use or null
+ */
+ public void setCustomPaint(Paint mCustomPaint) {
+ this.mCustomPaint = mCustomPaint;
+ }
+
+ /**
+ * draw the series with an animation
+ *
+ * @param animated animation activated or not
+ */
+ public void setAnimated(boolean animated) {
+ this.mAnimated = animated;
+ }
+
+ /**
+ * @return rendering is animated or not
+ */
+ public boolean isAnimated() {
+ return mAnimated;
+ }
+
+ @Override
+ public void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value) {
+ // TODO
+ }
+}
diff --git a/src/main/java/com/jjoe64/graphview/series/BaseSeries.java b/src/main/java/com/jjoe64/graphview/series/BaseSeries.java
new file mode 100644
index 000000000..141b7497d
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/series/BaseSeries.java
@@ -0,0 +1,557 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jjoe64.graphview.series;
+
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.util.Log;
+
+import com.jjoe64.graphview.GraphView;
+
+import java.lang.ref.WeakReference;
+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;
+
+ /**
+ * cache for lowest y value
+ */
+ private double mLowestYCache = Double.NaN;
+
+ /**
+ * cahce for highest y value
+ */
+ private double mHighestYCache = Double.NaN;
+
+ /**
+ * 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;
+ private Boolean mIsCursorModeCache;
+
+ /**
+ * 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);
+ }
+ checkValueOrder(null);
+ }
+
+ /**
+ * @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;
+ if (!Double.isNaN(mLowestYCache)) {
+ return mLowestYCache;
+ }
+ 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 mLowestYCache = l;
+ }
+
+ /**
+ * @return the highest y value, or 0 if there is no data
+ */
+ public double getHighestValueY() {
+ if (mData.isEmpty()) return 0d;
+ if (!Double.isNaN(mHighestYCache)) {
+ return mHighestYCache;
+ }
+ 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 mHighestYCache = 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 != null) {
+ 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;
+ }
+
+ public E findDataPointAtX(float x) {
+ float shortestDistance = Float.NaN;
+ E shortest = null;
+ for (Map.Entry entry : mDataPoints.entrySet()) {
+ float x1 = entry.getKey().x;
+ float x2 = x;
+
+ float distance = Math.abs(x1 - x2);
+ if (shortest == null || distance < shortestDistance) {
+ shortestDistance = distance;
+ shortest = entry.getValue();
+ }
+ }
+ if (shortest != null) {
+ if (shortestDistance < 200) {
+ 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) {
+ // performance
+ // TODO maybe invalidate after setting the listener
+ if (mOnDataPointTapListener != null || isCursorMode()) {
+ mDataPoints.put(new PointF(x, y), dp);
+ }
+ }
+
+ private boolean isCursorMode() {
+ if (mIsCursorModeCache != null) {
+ return mIsCursorModeCache;
+ }
+ for (WeakReference graphView : mGraphViews) {
+ if (graphView != null && graphView.get() != null && graphView.get().isCursorMode()) {
+ return mIsCursorModeCache = true;
+ }
+ }
+ return mIsCursorModeCache = false;
+ }
+
+ /**
+ * 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);
+
+ mHighestYCache = mLowestYCache = Double.NaN;
+
+ // update graphview
+ for (WeakReference gv : mGraphViews) {
+ if (gv != null && gv.get() != null) {
+ gv.get().onDataChanged(true, false);
+ }
+ }
+ }
+
+ /**
+ * stores the reference of the used graph
+ *
+ * @param graphView graphview
+ */
+ @Override
+ public void onGraphViewAttached(GraphView graphView) {
+ mGraphViews.add(new WeakReference<>(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
+ * @param silent set true to avoid rerender the graph
+ */
+ public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, boolean silent) {
+ 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);
+ }
+
+ // update lowest/highest cache
+ double dataPointY = dataPoint.getY();
+ if (!Double.isNaN(mHighestYCache)) {
+ if (dataPointY > mHighestYCache) {
+ mHighestYCache = dataPointY;
+ }
+ }
+ if (!Double.isNaN(mLowestYCache)) {
+ if (dataPointY < mLowestYCache) {
+ mLowestYCache = dataPointY;
+ }
+ }
+
+ }
+
+ if (!silent) {
+ // recalc the labels when it was the first data
+ boolean keepLabels = mData.size() != 1;
+
+ // update linked graph views
+ // update graphview
+ for (WeakReference gv : mGraphViews) {
+ if (gv != null && gv.get() != null) {
+ if (scrollToEnd) {
+ gv.get().getViewport().scrollToEnd();
+ } else {
+ gv.get().onDataChanged(keepLabels, scrollToEnd);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ *
+ * @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) {
+ appendData(dataPoint, scrollToEnd, maxDataPoints, false);
+ }
+
+ /**
+ * @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();
+ }
+ }
+ }
+ }
+ }
+
+ public abstract void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value);
+
+ public void clearCursorModeCache() {
+ mIsCursorModeCache = null;
+ }
+
+ @Override
+ public void clearReference(GraphView graphView) {
+ // find and remove
+ for (WeakReference view : mGraphViews) {
+ if (view != null && view.get() != null && view.get() == graphView) {
+ mGraphViews.remove(view);
+ break;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/jjoe64/graphview/series/DataPoint.java b/src/main/java/com/jjoe64/graphview/series/DataPoint.java
new file mode 100644
index 000000000..097a29c02
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/series/DataPoint.java
@@ -0,0 +1,60 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java b/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java
new file mode 100644
index 000000000..5d641f7d0
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java
@@ -0,0 +1,38 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java
new file mode 100644
index 000000000..3d56125dd
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java
@@ -0,0 +1,714 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jjoe64.graphview.series;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import androidx.core.view.ViewCompat;
+import android.view.animation.AccelerateInterpolator;
+
+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 {
+ private static final long ANIMATION_DURATION = 333;
+
+ /**
+ * 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;
+
+ private Paint mSelectionPaint;
+
+ /**
+ * 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;
+
+ /**
+ * rendering is animated
+ */
+ private boolean mAnimated;
+
+ /**
+ * last animated value
+ */
+ private double mLastAnimatedValue = Double.NaN;
+
+ /**
+ * time of animation start
+ */
+ private long mAnimationStart;
+
+ /**
+ * animation interpolator
+ */
+ private AccelerateInterpolator mAnimationInterpolator;
+
+ /**
+ * number of animation frame to avoid lagging
+ */
+ private int mAnimationStartFrameNo;
+
+ /**
+ * flag whether the line should be drawn as a path
+ * or with single drawLine commands (more performance)
+ * By default we use drawLine because it has much more peformance.
+ * For some styling reasons it can make sense to draw as path.
+ */
+ private boolean mDrawAsPath = false;
+
+ /**
+ * creates a series without data
+ */
+ public LineGraphSeries() {
+ init();
+ }
+
+ /**
+ * creates a series with data
+ *
+ * @param data data points
+ * important: array has to be sorted from lowest x-value to the highest
+ */
+ 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();
+
+ mSelectionPaint = new Paint();
+ mSelectionPaint.setColor(Color.argb(80, 0, 0, 0));
+ mSelectionPaint.setStyle(Paint.Style.FILL);
+
+ mPathBackground = new Path();
+ mPath = new Path();
+
+ mAnimationInterpolator = new AccelerateInterpolator(2f);
+ }
+
+ /**
+ * 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(false);
+ minY = graphView.getSecondScale().getMinY(false);
+ } 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;
+ }
+
+ mPath.reset();
+
+ 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;
+
+ // needed to end the path for background
+ double lastUsedEndX = 0;
+ double lastUsedEndY = 0;
+ float firstX = -1;
+ float firstY = -1;
+ float lastRenderedX = Float.NaN;
+ int i = 0;
+ float lastAnimationReferenceX = graphLeft;
+
+ boolean sameXSkip = false;
+ float minYOnSameX = 0f;
+ float maxYOnSameX = 0f;
+
+ while (values.hasNext()) {
+ E value = values.next();
+
+ double valY = value.getY() - minY;
+ double ratY = valY / diffY;
+ double y = graphHeight * ratY;
+
+ double valueX = value.getX();
+ double valX = valueX - minX;
+ double ratX = valX / diffX;
+ double x = graphWidth * ratX;
+
+ double orgX = x;
+ double orgY = y;
+
+ if (i > 0) {
+ // overdraw
+ boolean isOverdrawY = false;
+ boolean isOverdrawEndPoint = false;
+ boolean skipDraw = false;
+
+ if (x > graphWidth) { // end right
+ double b = ((graphWidth - lastEndX) * (y - lastEndY) / (x - lastEndX));
+ y = lastEndY + b;
+ x = graphWidth;
+ isOverdrawEndPoint = true;
+ }
+ if (y < 0) { // end bottom
+ // skip when previous and this point is out of bound
+ if (lastEndY < 0) {
+ skipDraw = true;
+ } else {
+ double b = ((0 - lastEndY) * (x - lastEndX) / (y - lastEndY));
+ x = lastEndX + b;
+ }
+ y = 0;
+ isOverdrawY = isOverdrawEndPoint = true;
+ }
+ if (y > graphHeight) { // end top
+ // skip when previous and this point is out of bound
+ if (lastEndY > graphHeight) {
+ skipDraw = true;
+ } else {
+ double b = ((graphHeight - lastEndY) * (x - lastEndX) / (y - lastEndY));
+ x = lastEndX + b;
+ }
+ y = graphHeight;
+ isOverdrawY = isOverdrawEndPoint = true;
+ }
+ if (lastEndX < 0) { // start left
+ double b = ((0 - x) * (y - lastEndY) / (lastEndX - x));
+ lastEndY = y - b;
+ lastEndX = 0;
+ }
+
+ // we need to save the X before it will be corrected when overdraw y
+ float orgStartX = (float) lastEndX + (graphLeft + 1);
+
+ if (lastEndY < 0) { // start bottom
+ if (!skipDraw) {
+ double b = ((0 - y) * (x - lastEndX) / (lastEndY - y));
+ lastEndX = x - b;
+ }
+ lastEndY = 0;
+ isOverdrawY = true;
+ }
+ if (lastEndY > graphHeight) { // start top
+ // skip when previous and this point is out of bound
+ if (!skipDraw) {
+ double b = ((graphHeight - y) * (x - lastEndX) / (lastEndY - y));
+ lastEndX = x - b;
+ }
+ lastEndY = graphHeight;
+ isOverdrawY = true;
+ }
+
+ float startX = (float) lastEndX + (graphLeft + 1);
+ float startY = (float) (graphTop - lastEndY) + graphHeight;
+ float endX = (float) x + (graphLeft + 1);
+ float endY = (float) (graphTop - y) + graphHeight;
+ float startXAnimated = startX;
+ float endXAnimated = endX;
+
+ if (endX < startX) {
+ // dont draw from right to left
+ skipDraw = true;
+ }
+
+ // NaN can happen when previous and current value is out of y bounds
+ if (!skipDraw && !Float.isNaN(startY) && !Float.isNaN(endY)) {
+ // animation
+ if (mAnimated) {
+ if ((Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) {
+ long currentTime = System.currentTimeMillis();
+ if (mAnimationStart == 0) {
+ // start animation
+ mAnimationStart = currentTime;
+ mAnimationStartFrameNo = 0;
+ } else {
+ // anti-lag: wait a few frames
+ if (mAnimationStartFrameNo < 15) {
+ // second time
+ mAnimationStart = currentTime;
+ mAnimationStartFrameNo++;
+ }
+ }
+ float timeFactor = (float) (currentTime - mAnimationStart) / ANIMATION_DURATION;
+ float factor = mAnimationInterpolator.getInterpolation(timeFactor);
+ if (timeFactor <= 1.0) {
+ startXAnimated = (startX - lastAnimationReferenceX) * factor + lastAnimationReferenceX;
+ startXAnimated = Math.max(startXAnimated, lastAnimationReferenceX);
+ endXAnimated = (endX - lastAnimationReferenceX) * factor + lastAnimationReferenceX;
+ ViewCompat.postInvalidateOnAnimation(graphView);
+ } else {
+ // animation finished
+ mLastAnimatedValue = valueX;
+ }
+ } else {
+ lastAnimationReferenceX = endX;
+ }
+ }
+
+ // draw data point
+ if (!isOverdrawEndPoint) {
+ if (mStyles.drawDataPoints) {
+ // draw first datapoint
+ Paint.Style prevStyle = paint.getStyle();
+ paint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(endXAnimated, endY, mStyles.dataPointsRadius, paint);
+ paint.setStyle(prevStyle);
+ }
+ registerDataPoint(endX, endY, value);
+ }
+
+ if (mDrawAsPath) {
+ mPath.moveTo(startXAnimated, startY);
+ }
+ // performance opt.
+ if (Float.isNaN(lastRenderedX) || Math.abs(endX - lastRenderedX) > .3f) {
+ if (mDrawAsPath) {
+ mPath.lineTo(endXAnimated, endY);
+ } else {
+ // draw vertical lines that were skipped
+ if (sameXSkip) {
+ sameXSkip = false;
+ renderLine(canvas, new float[]{lastRenderedX, minYOnSameX, lastRenderedX, maxYOnSameX}, paint);
+ }
+ renderLine(canvas, new float[]{startXAnimated, startY, endXAnimated, endY}, paint);
+ }
+ lastRenderedX = endX;
+ } else {
+ // rendering on same x position
+ // save min+max y position and draw it as line
+ if (sameXSkip) {
+ minYOnSameX = Math.min(minYOnSameX, endY);
+ maxYOnSameX = Math.max(maxYOnSameX, endY);
+ } else {
+ // first
+ sameXSkip = true;
+ minYOnSameX = Math.min(startY, endY);
+ maxYOnSameX = Math.max(startY, endY);
+ }
+ }
+
+ }
+
+ if (mStyles.drawBackground) {
+ if (isOverdrawY) {
+ // start draw original x
+ if (firstX == -1) {
+ firstX = orgStartX;
+ firstY = startY;
+ mPathBackground.moveTo(orgStartX, startY);
+ }
+ // from original start to new start
+ mPathBackground.lineTo(startXAnimated, startY);
+ }
+ if (firstX == -1) {
+ firstX = startXAnimated;
+ firstY = startY;
+ mPathBackground.moveTo(startXAnimated, startY);
+ }
+ mPathBackground.lineTo(startXAnimated, startY);
+ mPathBackground.lineTo(endXAnimated, endY);
+ }
+
+ lastUsedEndX = endXAnimated;
+ lastUsedEndY = endY;
+ } 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;
+
+ if (first_X >= graphLeft && first_Y <= (graphTop + graphHeight)) {
+ if (mAnimated && (Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) {
+ long currentTime = System.currentTimeMillis();
+ if (mAnimationStart == 0) {
+ // start animation
+ mAnimationStart = currentTime;
+ }
+ float timeFactor = (float) (currentTime - mAnimationStart) / ANIMATION_DURATION;
+ float factor = mAnimationInterpolator.getInterpolation(timeFactor);
+ if (timeFactor <= 1.0) {
+ first_X = (first_X - lastAnimationReferenceX) * factor + lastAnimationReferenceX;
+ ViewCompat.postInvalidateOnAnimation(graphView);
+ } else {
+ // animation finished
+ mLastAnimatedValue = valueX;
+ }
+ }
+
+
+ Paint.Style prevStyle = paint.getStyle();
+ paint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(first_X, first_Y, mStyles.dataPointsRadius, paint);
+ paint.setStyle(prevStyle);
+ registerDataPoint(first_X, first_Y, value);
+ }
+ }
+ lastEndY = orgY;
+ lastEndX = orgX;
+ i++;
+ }
+
+ if (mDrawAsPath) {
+ // draw at the end
+ canvas.drawPath(mPath, paint);
+ }
+
+ if (mStyles.drawBackground && firstX != -1) {
+ // end / close path
+ if (lastUsedEndY != graphHeight + graphTop) {
+ // dont draw line to same point, otherwise the path is completely broken
+ mPathBackground.lineTo((float) lastUsedEndX, graphHeight + graphTop);
+ }
+ mPathBackground.lineTo(firstX, graphHeight + graphTop);
+ if (firstY != graphHeight + graphTop) {
+ // dont draw line to same point, otherwise the path is completely broken
+ mPathBackground.lineTo(firstX, firstY);
+ }
+ //mPathBackground.close();
+ canvas.drawPath(mPathBackground, mPaintBackground);
+ }
+ }
+
+ /**
+ * just a wrapper to draw lines on canvas
+ *
+ * @param canvas
+ * @param pts
+ * @param paint
+ */
+ private void renderLine(Canvas canvas, float[] pts, Paint paint) {
+ if (pts.length == 4 && pts[0] == pts[2] && pts[1] == pts[3]) {
+ // avoid zero length lines, to makes troubles on some devices
+ // see https://github.com/appsthatmatter/GraphView/issues/499
+ return;
+ }
+ canvas.drawLines(pts, paint);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * @param animated activate the animated rendering
+ */
+ public void setAnimated(boolean animated) {
+ this.mAnimated = animated;
+ }
+
+ /**
+ * flag whether the line should be drawn as a path
+ * or with single drawLine commands (more performance)
+ * By default we use drawLine because it has much more peformance.
+ * For some styling reasons it can make sense to draw as path.
+ */
+ public boolean isDrawAsPath() {
+ return mDrawAsPath;
+ }
+
+ /**
+ * flag whether the line should be drawn as a path
+ * or with single drawLine commands (more performance)
+ * By default we use drawLine because it has much more peformance.
+ * For some styling reasons it can make sense to draw as path.
+ *
+ * @param mDrawAsPath true to draw as path
+ */
+ public void setDrawAsPath(boolean mDrawAsPath) {
+ this.mDrawAsPath = mDrawAsPath;
+ }
+
+ /**
+ *
+ * @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
+ * @param silent set true to avoid rerender the graph
+ */
+ public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, boolean silent) {
+ if (!isAnimationActive()) {
+ mAnimationStart = 0;
+ }
+ super.appendData(dataPoint, scrollToEnd, maxDataPoints, silent);
+ }
+
+ /**
+ * @return currently animation is active
+ */
+ private boolean isAnimationActive() {
+ if (mAnimated) {
+ long curr = System.currentTimeMillis();
+ return curr - mAnimationStart <= ANIMATION_DURATION;
+ }
+ return false;
+ }
+
+ @Override
+ public void drawSelection(GraphView graphView, Canvas canvas, boolean b, DataPointInterface value) {
+ double spanX = graphView.getViewport().getMaxX(false) - graphView.getViewport().getMinX(false);
+ double spanXPixel = graphView.getGraphContentWidth();
+
+ double spanY = graphView.getViewport().getMaxY(false) - graphView.getViewport().getMinY(false);
+ double spanYPixel = graphView.getGraphContentHeight();
+
+ double pointX = (value.getX() - graphView.getViewport().getMinX(false)) * spanXPixel / spanX;
+ pointX += graphView.getGraphContentLeft();
+
+ double pointY = (value.getY() - graphView.getViewport().getMinY(false)) * spanYPixel / spanY;
+ pointY = graphView.getGraphContentTop() + spanYPixel - pointY;
+
+ // border
+ canvas.drawCircle((float) pointX, (float) pointY, 30f, mSelectionPaint);
+
+ // fill
+ Paint.Style prevStyle = mPaint.getStyle();
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle((float) pointX, (float) pointY, 23f, mPaint);
+ mPaint.setStyle(prevStyle);
+ }
+}
diff --git a/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java b/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java
new file mode 100644
index 000000000..e846e329e
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java
@@ -0,0 +1,35 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java
new file mode 100644
index 000000000..2ec74a526
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java
@@ -0,0 +1,318 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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 draw 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 draw for
+ * each data point.
+ * You can also draw 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(false);
+ minY = graphView.getSecondScale().getMinY(false);
+ } 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;
+ }
+ /* Fix a bug that continue to show the DOT after Y axis */
+ if(x < 0) {
+ 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 draw 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 draw your own
+ * drawing for each data point.
+ *
+ * @param shape handler to use a custom drawing
+ */
+ public void setCustomShape(CustomShape shape) {
+ mCustomShape = shape;
+ }
+
+ @Override
+ public void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value) {
+ // TODO
+ }
+}
diff --git a/src/main/java/com/jjoe64/graphview/series/Series.java b/src/main/java/com/jjoe64/graphview/series/Series.java
new file mode 100644
index 000000000..95e82a7e1
--- /dev/null
+++ b/src/main/java/com/jjoe64/graphview/series/Series.java
@@ -0,0 +1,129 @@
+/**
+ * GraphView
+ * Copyright 2016 Jonas Gehring
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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();
+
+ /**
+ * clear reference to view and activity
+ *
+ * @param graphView
+ */
+ void clearReference(GraphView graphView);
+}
diff --git a/src/main/res/values/attr.xml b/src/main/res/values/attr.xml
new file mode 100644
index 000000000..8b7383860
--- /dev/null
+++ b/src/main/res/values/attr.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/zooming.gif b/zooming.gif
new file mode 100644
index 000000000..2b3dc215c
Binary files /dev/null and b/zooming.gif differ