├── .gitignore ├── README.markdown ├── README.new-version.md ├── anim.gif ├── build.gradle ├── doc-assets ├── 1059439_1.png ├── 4000611_1.png ├── 469160_orig_1.png ├── 5901645_1.png ├── 6316193_orig_1.png ├── 9303658_1.png ├── Screen_Shot_2016_10_08_at_12_19_56_1.png ├── Screen_Shot_2016_10_08_at_12_20_56_1.png ├── Screen_Shot_2016_10_08_at_12_23_38_1.png ├── Screen_Shot_2016_10_08_at_12_24_19_1.png ├── Screenshot_20161008_122642_1_1.png ├── Screenshot_20161011_210215_1.png ├── Screenshot_20161012_180242_1.png ├── Screenshot_20161012_180257_1.png ├── Screenshot_20161012_180325_1.png ├── Screenshot_20161012_180336_1.png ├── Screenshot_20161012_180355_1.png ├── Screenshot_20161012_180404_1.png └── snapshotshare_1.png ├── favicon.ico ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── license.txt ├── maven_push.gradle ├── proguard-rules.txt ├── public ├── GraphView-3.1.1.jar ├── GraphView-3.1.2.jar ├── GraphView-3.1.3.jar ├── GraphView-3.1.4.jar ├── GraphView-4.0.0.jar ├── GraphView-4.0.1.jar ├── GraphView-4.1.0.jar ├── GraphView-4.1.1.jar ├── GraphView-4.2.0.jar ├── GraphView-4.2.1.jar └── graphview-3.1.jar ├── src └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── jjoe64 │ │ └── graphview │ │ ├── CursorMode.java │ │ ├── DefaultLabelFormatter.java │ │ ├── GraphView.java │ │ ├── GridLabelRenderer.java │ │ ├── LabelFormatter.java │ │ ├── LegendRenderer.java │ │ ├── RectD.java │ │ ├── SecondScale.java │ │ ├── UniqueLegendRenderer.java │ │ ├── ValueDependentColor.java │ │ ├── Viewport.java │ │ ├── compat │ │ └── OverScrollerCompat.java │ │ ├── helper │ │ ├── DateAsXAxisLabelFormatter.java │ │ ├── GraphViewXML.java │ │ └── StaticLabelsFormatter.java │ │ └── series │ │ ├── BarGraphSeries.java │ │ ├── BaseSeries.java │ │ ├── DataPoint.java │ │ ├── DataPointInterface.java │ │ ├── LineGraphSeries.java │ │ ├── OnDataPointTapListener.java │ │ ├── PointsGraphSeries.java │ │ └── Series.java │ └── res │ └── values │ └── attr.xml └── zooming.gif /.gitignore: -------------------------------------------------------------------------------- 1 | _Store 2 | 3 | 4 | # Generated by http://gitignore.io 5 | 6 | ### Eclipse ### 7 | *.pydevproject 8 | .project 9 | .metadata 10 | bin/** 11 | tmp/** 12 | tmp/**/* 13 | *.tmp 14 | *.bak 15 | *.swp 16 | *~.nib 17 | local.properties 18 | .classpath 19 | .settings/ 20 | .loadpath 21 | 22 | # External tool builders 23 | .externalToolBuilders/ 24 | 25 | # Locally stored "Eclipse launch configurations" 26 | *.launch 27 | 28 | # CDT-specific 29 | .cproject 30 | 31 | # PDT-specific 32 | .buildpath 33 | 34 | ### IntelliJ ### 35 | *.iml 36 | *.ipr 37 | *.iws 38 | .idea/ 39 | 40 | ### Android ### 41 | # built application files 42 | *.apk 43 | *.ap_ 44 | 45 | # files for the dex VM 46 | *.dex 47 | 48 | # Java class files 49 | *.class 50 | 51 | # generated files 52 | bin/ 53 | gen/ 54 | 55 | # Local configuration file (sdk path, etc) 56 | local.properties 57 | 58 | # Eclipse project files 59 | .classpath 60 | .project 61 | 62 | # Proguard folder generated by Eclipse 63 | proguard/ 64 | 65 | # Proguard folder generated by Intellij 66 | proguard_logs/ 67 | 68 | # Intellij project files 69 | *.iml 70 | *.ipr 71 | *.iws 72 | .idea/ 73 | 74 | adt-bundle-windows-x86_64/ 75 | 76 | ### Windows ### 77 | # Windows image file caches 78 | Thumbs.db 79 | ehthumbs.db 80 | 81 | # Folder config file 82 | Desktop.ini 83 | 84 | # Recycle Bin used on file shares 85 | $RECYCLE.BIN/ 86 | 87 | ### Linux ### 88 | .* 89 | !.gitignore 90 | *~ 91 | # Generated by http://gitignore.io 92 | 93 | ### Gradle ### 94 | # Exclude Folder List # 95 | .gradle/ 96 | build/ 97 | .gradletasknamecache 98 | gradle.properties 99 | 100 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Chart and Graph Library for Android 2 | 3 | ## 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 4 | 5 | ## What is GraphView 6 | 7 | GraphView is a library for Android to programmatically create 8 | flexible and nice-looking diagrams. 9 | It is **easy** to understand, to integrate and to customize. 10 | 11 | Supported graph types: 12 | * Line Graphs 13 | * Bar Graphs 14 | * Point Graphs 15 | * or implement your own custom types. 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ## Top Features 24 | 25 | * Line Chart, Bar Chart, Points 26 | * Combination of different graph types 27 | * Scrolling vertical and horizontal 28 | . You can scroll with a finger touch move gesture. 29 | * Scaling / Zooming vertical and horizontal 30 | . With two-fingers touch scale gesture (Multi-touch), the viewport can be changed. 31 | * Realtime Graph (Live change of data) 32 | * Second scale axis 33 | * Draw multiple series of data 34 | . Let the diagram show more that one series in a graph. You can set a color and a description for every series. 35 | * Show legend 36 | . A legend can be displayed inline the chart. You can set the width and the vertical align (top, middle, bottom). 37 | * Custom labels 38 | . The labels for the x- and y-axis are generated automatically. But you can set your own labels, Strings are possible. 39 | * Handle incomplete data 40 | . It's possible to give the data in different frequency. 41 | * Viewport 42 | . You can limit the viewport so that only a part of the data will be displayed. 43 | * Manual Y axis limits 44 | * And much more... Check out the project page and/or the demo app 45 | 46 | ## How to use 47 | 48 | 1) Add gradle dependency: 49 | ``` 50 | implementation 'com.jjoe64:graphview:4.2.2' 51 | ``` 52 | 53 | 2) Add view to layout: 54 | ```xml 55 | 59 | ``` 60 | 61 | 3) Add some data: 62 | ```java 63 | GraphView graph = (GraphView) findViewById(R.id.graph); 64 | LineGraphSeries series = new LineGraphSeries(new DataPoint[] { 65 | new DataPoint(0, 1), 66 | new DataPoint(1, 5), 67 | new DataPoint(2, 3), 68 | new DataPoint(3, 2), 69 | new DataPoint(4, 6) 70 | }); 71 | graph.addSeries(series); 72 | ``` 73 | 74 | ## Download Demo project at Google Play Store 75 | 76 |
77 | Showcase GraphView Demo App 78 | 79 | ## More examples and documentation 80 | 81 | Get started at project wiki homepage 82 | 83 | To show you how to integrate the library into an existing project see the GraphView-Demos project! 84 | See GraphView-Demos for examples. 85 | https://github.com/jjoe64/GraphView-Demos
86 |
View GraphView wiki page https://github.com/jjoe64/GraphView/wiki 87 | -------------------------------------------------------------------------------- /README.new-version.md: -------------------------------------------------------------------------------- 1 | How to create a new version for maven repo 2 | -------------------------------------------- 3 | create sources.jar 4 | - $ jar cvf sources.jar src 5 | 6 | create java doc jar 7 | - $ mkdir javadoc 8 | - $ javadoc -d javadoc -sourcepath src/main/java/ -subpackages com.jjoe64 9 | - $ jar cvf javadoc.jar javadoc 10 | 11 | change version in gradle.properties 12 | 13 | uncomment part for publishing in build.gradle 14 | 15 | (once) create a gpg file 16 | - gpg --gen-key 17 | 18 | (once) publish key 19 | - gpg --send-keys D8C3B041 20 | and/or here as ascii 21 | - gpg --export -a D8C3B041 22 | - http://keyserver.ubuntu.com:11371/ 23 | 24 | => needs some time 25 | 26 | hardcode gpg key password in maven_push.gradle 27 | 28 | hardcode user/pwd of nexus account in maven_push.gradle 29 | 30 | success gradle task uploadArchives 31 | - ./gradlew --rerun-tasks uploadArchives 32 | - enter gpg info (id:D8C3B041 / path: /Users/jonas/.gnupg/secring.gpg / PWD) 33 | 34 | open https://oss.sonatype.org 35 | 36 | login 37 | 38 | Staging Repositiories 39 | 40 | search: jjoe64 41 | 42 | Close entry 43 | 44 | Refresh/Wait 45 | 46 | Release entry 47 | 48 | Wait some days 49 | 50 | ## update java doc 51 | 52 | $ javadoc -d javadoc -sourcepath src/main/java/ -subpackages com.jjoe64 53 | $ mv javadoc/ .. 54 | $ git checkout gh-pages 55 | $ rm -rf javadoc 56 | $ mv ../javadoc/ . 57 | $ git add javadoc 58 | $ git commit -a 59 | 60 | -------------------------------------------------------------------------------- /anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/anim.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | google() 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.3' 9 | } 10 | } 11 | 12 | wrapper { 13 | gradleVersion = '5.6' 14 | } 15 | 16 | apply plugin: 'com.android.library' 17 | 18 | 19 | android { 20 | compileSdkVersion 27 21 | buildToolsVersion '28.0.3' 22 | 23 | defaultConfig { 24 | minSdkVersion 9 25 | targetSdkVersion 27 26 | versionCode 1 27 | versionName "1.0" 28 | } 29 | buildTypes { 30 | release { 31 | minifyEnabled false 32 | } 33 | } 34 | lintOptions { 35 | abortOnError false 36 | } 37 | 38 | } 39 | 40 | dependencies { 41 | implementation 'androidx.core:core:1.0.0-beta01' 42 | } 43 | 44 | 45 | //this is used to generate .jar files and push to maven repo 46 | // This is the actual solution, as in http://stackoverflow.com/a/19037807/1002054 47 | task clearJar(type: Delete) { 48 | delete 'build/outputs/myCompiledLibrary.jar' 49 | } 50 | 51 | task makeJar(type: Copy) { 52 | from('build/intermediates/bundles/release/') 53 | into('build/outputs/') 54 | include('classes.jar') 55 | rename ('classes.jar', 'myCompiledLibrary.jar') 56 | } 57 | 58 | makeJar.dependsOn(clearJar, build) 59 | 60 | 61 | apply from: './maven_push.gradle' 62 | 63 | repositories { 64 | google() 65 | mavenCentral() 66 | jcenter() 67 | } -------------------------------------------------------------------------------- /doc-assets/1059439_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/1059439_1.png -------------------------------------------------------------------------------- /doc-assets/4000611_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/4000611_1.png -------------------------------------------------------------------------------- /doc-assets/469160_orig_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/469160_orig_1.png -------------------------------------------------------------------------------- /doc-assets/5901645_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/5901645_1.png -------------------------------------------------------------------------------- /doc-assets/6316193_orig_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/6316193_orig_1.png -------------------------------------------------------------------------------- /doc-assets/9303658_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/9303658_1.png -------------------------------------------------------------------------------- /doc-assets/Screen_Shot_2016_10_08_at_12_19_56_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screen_Shot_2016_10_08_at_12_19_56_1.png -------------------------------------------------------------------------------- /doc-assets/Screen_Shot_2016_10_08_at_12_20_56_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screen_Shot_2016_10_08_at_12_20_56_1.png -------------------------------------------------------------------------------- /doc-assets/Screen_Shot_2016_10_08_at_12_23_38_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screen_Shot_2016_10_08_at_12_23_38_1.png -------------------------------------------------------------------------------- /doc-assets/Screen_Shot_2016_10_08_at_12_24_19_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screen_Shot_2016_10_08_at_12_24_19_1.png -------------------------------------------------------------------------------- /doc-assets/Screenshot_20161008_122642_1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screenshot_20161008_122642_1_1.png -------------------------------------------------------------------------------- /doc-assets/Screenshot_20161011_210215_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screenshot_20161011_210215_1.png -------------------------------------------------------------------------------- /doc-assets/Screenshot_20161012_180242_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screenshot_20161012_180242_1.png -------------------------------------------------------------------------------- /doc-assets/Screenshot_20161012_180257_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screenshot_20161012_180257_1.png -------------------------------------------------------------------------------- /doc-assets/Screenshot_20161012_180325_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screenshot_20161012_180325_1.png -------------------------------------------------------------------------------- /doc-assets/Screenshot_20161012_180336_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screenshot_20161012_180336_1.png -------------------------------------------------------------------------------- /doc-assets/Screenshot_20161012_180355_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screenshot_20161012_180355_1.png -------------------------------------------------------------------------------- /doc-assets/Screenshot_20161012_180404_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/Screenshot_20161012_180404_1.png -------------------------------------------------------------------------------- /doc-assets/snapshotshare_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/doc-assets/snapshotshare_1.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/favicon.ico -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=4.2.2 2 | VERSION_CODE=17 3 | GROUP=com.jjoe64 4 | 5 | POM_DESCRIPTION=Android Graph Library for creating zoomable and scrollable charts. 6 | POM_URL=http://android-graphview.org/ 7 | POM_SCM_URL=https://github.com/jjoe64/GraphView 8 | POM_SCM_CONNECTION=scm:git@github.com:jjoe64/GraphView.git 9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:jjoe64/GraphView.git 10 | POM_LICENCE_NAME=Apache License, Version 2.0 11 | POM_LICENCE_URL=https://github.com/jjoe64/GraphView/blob/master/license.txt 12 | POM_LICENCE_DIST=repo 13 | POM_DEVELOPER_ID=jjoe64 14 | POM_DEVELOPER_NAME=Jonas Gehring 15 | 16 | POM_NAME=GraphView 17 | POM_ARTIFACT_ID=graphview 18 | POM_PACKAGING=aar 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-all.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 Jonas Gehring 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | Apache License 17 | 18 | Version 2.0, January 2004 19 | 20 | http://www.apache.org/licenses/ 21 | 22 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 23 | 24 | 1. Definitions. 25 | 26 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 27 | 28 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 29 | 30 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 31 | 32 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 33 | 34 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 35 | 36 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 41 | 42 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 43 | 44 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 45 | 46 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 47 | 48 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 49 | 50 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 51 | 52 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 53 | You must cause any modified files to carry prominent notices stating that You changed the files; and 54 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 55 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 56 | 57 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 58 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 59 | 60 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 61 | 62 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 63 | 64 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 65 | 66 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 67 | 68 | END OF TERMS AND CONDITIONS 69 | -------------------------------------------------------------------------------- /maven_push.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | 4 | import org.gradle.plugins.signing.Sign 5 | 6 | gradle.taskGraph.whenReady { taskGraph -> 7 | if (taskGraph.allTasks.any { it instanceof Sign }) { 8 | // Use Java 6's console to read from the console (no good for 9 | // a CI environment) 10 | Console console = System.console() 11 | console.printf "\n\nWe have to sign some things in this build." + 12 | "\n\nPlease enter your signing details.\n\n" 13 | 14 | def id = "D8C3B041" 15 | def file = "/Users/jonas/.gnupg/secring.gpg" 16 | def password = "" 17 | 18 | allprojects { ext."signing.keyId" = id } 19 | allprojects { ext."signing.secretKeyRingFile" = file } 20 | allprojects { ext."signing.password" = password } 21 | 22 | console.printf "\nThanks.\n\n" + file 23 | } 24 | } 25 | 26 | def sonatypeRepositoryUrl 27 | if (isReleaseBuild()) { 28 | println 'RELEASE BUILD' 29 | sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 30 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 31 | } else { 32 | println 'DEBUG BUILD' 33 | sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 34 | : "https://oss.sonatype.org/content/repositories/snapshots/" 35 | } 36 | 37 | def getRepositoryUsername() { 38 | return "" 39 | } 40 | 41 | def getRepositoryPassword() { 42 | return "" 43 | } 44 | 45 | def isReleaseBuild() { 46 | return version.contains("SNAPSHOT") == false 47 | } 48 | 49 | afterEvaluate { project -> 50 | uploadArchives { 51 | repositories { 52 | mavenDeployer { 53 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 54 | 55 | pom.groupId = GROUP 56 | pom.artifactId = POM_ARTIFACT_ID 57 | pom.version = VERSION_NAME 58 | 59 | repository(url: sonatypeRepositoryUrl) { 60 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 61 | } 62 | 63 | pom.project { 64 | name POM_NAME 65 | packaging POM_PACKAGING 66 | description POM_DESCRIPTION 67 | url POM_URL 68 | 69 | 70 | scm { 71 | url POM_SCM_URL 72 | connection POM_SCM_CONNECTION 73 | developerConnection POM_SCM_DEV_CONNECTION 74 | } 75 | 76 | licenses { 77 | license { 78 | name POM_LICENCE_NAME 79 | url POM_LICENCE_URL 80 | distribution POM_LICENCE_DIST 81 | } 82 | } 83 | 84 | developers { 85 | developer { 86 | id POM_DEVELOPER_ID 87 | name POM_DEVELOPER_NAME 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | signing { 96 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 97 | sign configurations.archives 98 | } 99 | 100 | task androidJavadocs(type: Javadoc) { 101 | source = android.sourceSets.main.java.sourceFiles 102 | } 103 | 104 | task androidJavadocsJar(type: Jar) { 105 | classifier = 'javadoc' 106 | //basename = artifact_id 107 | from androidJavadocs.destinationDir 108 | } 109 | 110 | task androidSourcesJar(type: Jar) { 111 | classifier = 'sources' 112 | //basename = artifact_id 113 | from android.sourceSets.main.java.sourceFiles 114 | } 115 | 116 | artifacts { 117 | //archives packageReleaseJar 118 | archives androidSourcesJar 119 | archives androidJavadocsJar 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/jonas/android-studio/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} -------------------------------------------------------------------------------- /public/GraphView-3.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-3.1.1.jar -------------------------------------------------------------------------------- /public/GraphView-3.1.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-3.1.2.jar -------------------------------------------------------------------------------- /public/GraphView-3.1.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-3.1.3.jar -------------------------------------------------------------------------------- /public/GraphView-3.1.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-3.1.4.jar -------------------------------------------------------------------------------- /public/GraphView-4.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-4.0.0.jar -------------------------------------------------------------------------------- /public/GraphView-4.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-4.0.1.jar -------------------------------------------------------------------------------- /public/GraphView-4.1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-4.1.0.jar -------------------------------------------------------------------------------- /public/GraphView-4.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-4.1.1.jar -------------------------------------------------------------------------------- /public/GraphView-4.2.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-4.2.0.jar -------------------------------------------------------------------------------- /public/GraphView-4.2.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/GraphView-4.2.1.jar -------------------------------------------------------------------------------- /public/graphview-3.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/public/graphview-3.1.jar -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/CursorMode.java: -------------------------------------------------------------------------------- 1 | package com.jjoe64.graphview; 2 | 3 | import android.content.res.TypedArray; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.Rect; 8 | import android.graphics.RectF; 9 | import android.util.Log; 10 | import android.util.TypedValue; 11 | import android.view.MotionEvent; 12 | 13 | import com.jjoe64.graphview.series.BaseSeries; 14 | import com.jjoe64.graphview.series.DataPointInterface; 15 | import com.jjoe64.graphview.series.Series; 16 | 17 | import java.util.ArrayList; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | /** 23 | * Created by jonas on 22/02/2017. 24 | */ 25 | 26 | public class CursorMode { 27 | private final static class Styles { 28 | public float textSize; 29 | public int spacing; 30 | public int padding; 31 | public int width; 32 | public int backgroundColor; 33 | public int margin; 34 | public int textColor; 35 | } 36 | 37 | protected final Paint mPaintLine; 38 | protected final GraphView mGraphView; 39 | protected float mPosX; 40 | protected float mPosY; 41 | protected boolean mCursorVisible; 42 | protected final Map mCurrentSelection; 43 | protected final Paint mRectPaint; 44 | protected final Paint mTextPaint; 45 | protected double mCurrentSelectionX; 46 | protected Styles mStyles; 47 | protected int cachedLegendWidth; 48 | 49 | public CursorMode(GraphView graphView) { 50 | mStyles = new Styles(); 51 | mGraphView = graphView; 52 | mPaintLine = new Paint(); 53 | mPaintLine.setColor(Color.argb(128, 180, 180, 180)); 54 | mPaintLine.setStrokeWidth(10f); 55 | mCurrentSelection = new HashMap<>(); 56 | mRectPaint = new Paint(); 57 | mTextPaint = new Paint(); 58 | resetStyles(); 59 | } 60 | 61 | /** 62 | * resets the styles to the defaults 63 | * and clears the legend width cache 64 | */ 65 | public void resetStyles() { 66 | mStyles.textSize = mGraphView.getGridLabelRenderer().getTextSize(); 67 | mStyles.spacing = (int) (mStyles.textSize / 5); 68 | mStyles.padding = (int) (mStyles.textSize / 2); 69 | mStyles.width = 0; 70 | mStyles.backgroundColor = Color.argb(180, 100, 100, 100); 71 | mStyles.margin = (int) (mStyles.textSize); 72 | 73 | // get matching styles from theme 74 | TypedValue typedValue = new TypedValue(); 75 | mGraphView.getContext().getTheme().resolveAttribute(android.R.attr.textAppearanceSmall, typedValue, true); 76 | 77 | int color1; 78 | 79 | try { 80 | TypedArray array = mGraphView.getContext().obtainStyledAttributes(typedValue.data, new int[]{ 81 | android.R.attr.textColorPrimary}); 82 | color1 = array.getColor(0, Color.BLACK); 83 | array.recycle(); 84 | } catch (Exception e) { 85 | color1 = Color.BLACK; 86 | } 87 | 88 | mStyles.textColor = color1; 89 | 90 | cachedLegendWidth = 0; 91 | } 92 | 93 | 94 | public void onDown(MotionEvent e) { 95 | mPosX = Math.max(e.getX(), mGraphView.getGraphContentLeft()); 96 | mPosX = Math.min(mPosX, mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth()); 97 | mPosY = e.getY(); 98 | mCursorVisible = true; 99 | findCurrentDataPoint(); 100 | mGraphView.invalidate(); 101 | } 102 | 103 | public void onMove(MotionEvent e) { 104 | if (mCursorVisible) { 105 | mPosX = Math.max(e.getX(), mGraphView.getGraphContentLeft()); 106 | mPosX = Math.min(mPosX, mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth()); 107 | mPosY = e.getY(); 108 | findCurrentDataPoint(); 109 | mGraphView.invalidate(); 110 | } 111 | } 112 | 113 | public void draw(Canvas canvas) { 114 | if (mCursorVisible) { 115 | canvas.drawLine(mPosX, 0, mPosX, canvas.getHeight(), mPaintLine); 116 | } 117 | 118 | // selection 119 | for (Map.Entry entry : mCurrentSelection.entrySet()) { 120 | entry.getKey().drawSelection(mGraphView, canvas, false, entry.getValue()); 121 | } 122 | 123 | if (!mCurrentSelection.isEmpty()) { 124 | drawLegend(canvas); 125 | } 126 | } 127 | 128 | protected String getTextForSeries(Series s, DataPointInterface value) { 129 | StringBuffer txt = new StringBuffer(); 130 | if (s.getTitle() != null) { 131 | txt.append(s.getTitle()); 132 | txt.append(": "); 133 | } 134 | txt.append(mGraphView.getGridLabelRenderer().getLabelFormatter().formatLabel(value.getY(), false)); 135 | return txt.toString(); 136 | } 137 | 138 | protected void drawLegend(Canvas canvas) { 139 | mTextPaint.setTextSize(mStyles.textSize); 140 | mTextPaint.setColor(mStyles.textColor); 141 | 142 | int shapeSize = (int) (mStyles.textSize*0.8d); 143 | 144 | // width 145 | int legendWidth = mStyles.width; 146 | if (legendWidth == 0) { 147 | // auto 148 | legendWidth = cachedLegendWidth; 149 | 150 | if (legendWidth == 0) { 151 | Rect textBounds = new Rect(); 152 | for (Map.Entry entry : mCurrentSelection.entrySet()) { 153 | String txt = getTextForSeries(entry.getKey(), entry.getValue()); 154 | mTextPaint.getTextBounds(txt, 0, txt.length(), textBounds); 155 | legendWidth = Math.max(legendWidth, textBounds.width()); 156 | } 157 | if (legendWidth == 0) legendWidth = 1; 158 | 159 | // add shape size 160 | legendWidth += shapeSize+mStyles.padding*2 + mStyles.spacing; 161 | cachedLegendWidth = legendWidth; 162 | } 163 | } 164 | 165 | float legendPosX = mPosX - mStyles.margin - legendWidth; 166 | if (legendPosX < 0) { 167 | legendPosX = 0; 168 | } 169 | 170 | // rect 171 | float legendHeight = (mStyles.textSize+mStyles.spacing) * (mCurrentSelection.size() + 1) -mStyles.spacing; 172 | 173 | float legendPosY = mPosY - legendHeight - 4.5f * mStyles.textSize; 174 | if (legendPosY < 0) { 175 | legendPosY = 0; 176 | } 177 | 178 | float lLeft; 179 | float lTop; 180 | lLeft = legendPosX; 181 | lTop = legendPosY; 182 | 183 | float lRight = lLeft+legendWidth; 184 | float lBottom = lTop+legendHeight+2*mStyles.padding; 185 | mRectPaint.setColor(mStyles.backgroundColor); 186 | canvas.drawRoundRect(new RectF(lLeft, lTop, lRight, lBottom), 8, 8, mRectPaint); 187 | 188 | mTextPaint.setFakeBoldText(true); 189 | canvas.drawText(mGraphView.getGridLabelRenderer().getLabelFormatter().formatLabel(mCurrentSelectionX, true), lLeft+mStyles.padding, lTop+mStyles.padding/2+mStyles.textSize, mTextPaint); 190 | 191 | mTextPaint.setFakeBoldText(false); 192 | 193 | int i=1; 194 | for (Map.Entry entry : mCurrentSelection.entrySet()) { 195 | mRectPaint.setColor(entry.getKey().getColor()); 196 | canvas.drawRect(new RectF(lLeft+mStyles.padding, lTop+mStyles.padding+(i*(mStyles.textSize+mStyles.spacing)), lLeft+mStyles.padding+shapeSize, lTop+mStyles.padding+(i*(mStyles.textSize+mStyles.spacing))+shapeSize), mRectPaint); 197 | canvas.drawText(getTextForSeries(entry.getKey(), entry.getValue()), lLeft+mStyles.padding+shapeSize+mStyles.spacing, lTop+mStyles.padding/2+mStyles.textSize+(i*(mStyles.textSize+mStyles.spacing)), mTextPaint); 198 | i++; 199 | } 200 | } 201 | 202 | public boolean onUp(MotionEvent event) { 203 | mCursorVisible = false; 204 | findCurrentDataPoint(); 205 | mGraphView.invalidate(); 206 | return true; 207 | } 208 | 209 | private void findCurrentDataPoint() { 210 | double selX = 0; 211 | mCurrentSelection.clear(); 212 | for (Series series : mGraphView.getSeries()) { 213 | if (series instanceof BaseSeries) { 214 | DataPointInterface p = ((BaseSeries) series).findDataPointAtX(mPosX); 215 | if (p != null) { 216 | selX = p.getX(); 217 | mCurrentSelection.put((BaseSeries) series, p); 218 | } 219 | } 220 | } 221 | 222 | if (!mCurrentSelection.isEmpty()) { 223 | mCurrentSelectionX = selX; 224 | } 225 | } 226 | 227 | public void setTextSize(float t) { 228 | mStyles.textSize = t; 229 | } 230 | 231 | public void setTextColor(int color) { 232 | mStyles.textColor = color; 233 | } 234 | 235 | public void setBackgroundColor(int color) { 236 | mStyles.backgroundColor = color; 237 | } 238 | 239 | public void setSpacing(int s) { 240 | mStyles.spacing = s; 241 | } 242 | 243 | public void setPadding(int s) { 244 | mStyles.padding = s; 245 | } 246 | 247 | public void setMargin(int s) { 248 | mStyles.margin = s; 249 | } 250 | 251 | public void setWidth(int s) { 252 | mStyles.width = s; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/DefaultLabelFormatter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview; 18 | 19 | import java.text.NumberFormat; 20 | 21 | /** 22 | * The label formatter that will be used 23 | * by default. 24 | * It will use the NumberFormat from Android 25 | * and sets the maximal fraction digits 26 | * depending on the range between min and max 27 | * value of the current viewport. 28 | * 29 | * It is recommended to use this label formatter 30 | * as base class to implement a custom formatter. 31 | * 32 | * @author jjoe64 33 | */ 34 | public class DefaultLabelFormatter implements LabelFormatter { 35 | /** 36 | * number formatter for x and y values 37 | */ 38 | protected NumberFormat[] mNumberFormatter = new NumberFormat[2]; 39 | 40 | /** 41 | * reference to the viewport of the 42 | * graph. 43 | * Will be used to calculate the current 44 | * range of values. 45 | */ 46 | protected Viewport mViewport; 47 | 48 | /** 49 | * uses the default number format for the labels 50 | */ 51 | public DefaultLabelFormatter() { 52 | } 53 | 54 | /** 55 | * use custom number format 56 | * 57 | * @param xFormat the number format for the x labels 58 | * @param yFormat the number format for the y labels 59 | */ 60 | public DefaultLabelFormatter(NumberFormat xFormat, NumberFormat yFormat) { 61 | mNumberFormatter[0] = yFormat; 62 | mNumberFormatter[1] = xFormat; 63 | } 64 | 65 | /** 66 | * @param viewport the viewport of the graph 67 | */ 68 | @Override 69 | public void setViewport(Viewport viewport) { 70 | mViewport = viewport; 71 | } 72 | 73 | /** 74 | * Formats the raw value to a nice 75 | * looking label, depending on the 76 | * current range of the viewport. 77 | * 78 | * @param value raw value 79 | * @param isValueX true if it's a x value, otherwise false 80 | * @return the formatted value as string 81 | */ 82 | public String formatLabel(double value, boolean isValueX) { 83 | int i = isValueX ? 1 : 0; 84 | if (mNumberFormatter[i] == null) { 85 | mNumberFormatter[i] = NumberFormat.getNumberInstance(); 86 | double highestvalue = isValueX ? mViewport.getMaxX(false) : mViewport.getMaxY(false); 87 | double lowestvalue = isValueX ? mViewport.getMinX(false) : mViewport.getMinY(false); 88 | if (highestvalue - lowestvalue < 0.1) { 89 | mNumberFormatter[i].setMaximumFractionDigits(6); 90 | } else if (highestvalue - lowestvalue < 1) { 91 | mNumberFormatter[i].setMaximumFractionDigits(4); 92 | } else if (highestvalue - lowestvalue < 20) { 93 | mNumberFormatter[i].setMaximumFractionDigits(3); 94 | } else if (highestvalue - lowestvalue < 100) { 95 | mNumberFormatter[i].setMaximumFractionDigits(1); 96 | } else { 97 | mNumberFormatter[i].setMaximumFractionDigits(0); 98 | } 99 | } 100 | return mNumberFormatter[i].format(value); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/GraphView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview; 18 | 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.graphics.Bitmap; 22 | import android.graphics.Canvas; 23 | import android.graphics.Color; 24 | import android.graphics.Paint; 25 | import android.graphics.PointF; 26 | import android.net.Uri; 27 | import android.provider.MediaStore; 28 | import android.util.AttributeSet; 29 | import android.util.Log; 30 | import android.view.MotionEvent; 31 | import android.view.View; 32 | 33 | import com.jjoe64.graphview.series.BaseSeries; 34 | import com.jjoe64.graphview.series.Series; 35 | 36 | import java.io.ByteArrayOutputStream; 37 | import java.util.ArrayList; 38 | import java.util.List; 39 | 40 | /** 41 | * @author jjoe64 42 | */ 43 | public class GraphView extends View { 44 | /** 45 | * Class to wrap style options that are general 46 | * to graphs. 47 | * 48 | * @author jjoe64 49 | */ 50 | private static final class Styles { 51 | /** 52 | * The font size of the title that can be displayed 53 | * above the graph. 54 | * 55 | * @see GraphView#setTitle(String) 56 | */ 57 | float titleTextSize; 58 | 59 | /** 60 | * The font color of the title that can be displayed 61 | * above the graph. 62 | * 63 | * @see GraphView#setTitle(String) 64 | */ 65 | int titleColor; 66 | } 67 | 68 | /** 69 | * Helper class to detect tap events on the 70 | * graph. 71 | * 72 | * @author jjoe64 73 | */ 74 | private class TapDetector { 75 | /** 76 | * save the time of the last down event 77 | */ 78 | private long lastDown; 79 | 80 | /** 81 | * point of the tap down event 82 | */ 83 | private PointF lastPoint; 84 | 85 | /** 86 | * to be called to process the events 87 | * 88 | * @param event 89 | * @return true if there was a tap event. otherwise returns false. 90 | */ 91 | public boolean onTouchEvent(MotionEvent event) { 92 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 93 | lastDown = System.currentTimeMillis(); 94 | lastPoint = new PointF(event.getX(), event.getY()); 95 | } else if (lastDown > 0 && event.getAction() == MotionEvent.ACTION_MOVE) { 96 | if (Math.abs(event.getX() - lastPoint.x) > 60 97 | || Math.abs(event.getY() - lastPoint.y) > 60) { 98 | lastDown = 0; 99 | } 100 | } else if (event.getAction() == MotionEvent.ACTION_UP) { 101 | if (System.currentTimeMillis() - lastDown < 400) { 102 | return true; 103 | } 104 | } 105 | return false; 106 | } 107 | } 108 | 109 | /** 110 | * our series (this does not contain the series 111 | * that can be displayed on the right side. The 112 | * right side series is a special feature of 113 | * the {@link SecondScale} feature. 114 | */ 115 | private List mSeries; 116 | 117 | /** 118 | * the renderer for the grid and labels 119 | */ 120 | private GridLabelRenderer mGridLabelRenderer; 121 | 122 | /** 123 | * viewport that holds the current bounds of 124 | * view. 125 | */ 126 | private Viewport mViewport; 127 | 128 | /** 129 | * title of the graph that will be shown above 130 | */ 131 | private String mTitle; 132 | 133 | /** 134 | * wraps the general styles 135 | */ 136 | private Styles mStyles; 137 | 138 | /** 139 | * feature to have a second scale e.g. on the 140 | * right side 141 | */ 142 | protected SecondScale mSecondScale; 143 | 144 | /** 145 | * tap detector 146 | */ 147 | private TapDetector mTapDetector; 148 | 149 | /** 150 | * renderer for the legend 151 | */ 152 | private LegendRenderer mLegendRenderer; 153 | 154 | /** 155 | * paint for the graph title 156 | */ 157 | private Paint mPaintTitle; 158 | 159 | private boolean mIsCursorMode; 160 | 161 | /** 162 | * paint for the preview (in the SDK) 163 | */ 164 | private Paint mPreviewPaint; 165 | 166 | private CursorMode mCursorMode; 167 | 168 | /** 169 | * Initialize the GraphView view 170 | * @param context 171 | */ 172 | public GraphView(Context context) { 173 | super(context); 174 | init(); 175 | } 176 | 177 | /** 178 | * Initialize the GraphView view. 179 | * 180 | * @param context 181 | * @param attrs 182 | */ 183 | public GraphView(Context context, AttributeSet attrs) { 184 | super(context, attrs); 185 | init(); 186 | } 187 | 188 | /** 189 | * Initialize the GraphView view 190 | * 191 | * @param context 192 | * @param attrs 193 | * @param defStyle 194 | */ 195 | public GraphView(Context context, AttributeSet attrs, int defStyle) { 196 | super(context, attrs, defStyle); 197 | init(); 198 | } 199 | 200 | /** 201 | * initialize the internal objects. 202 | * This method has to be called directly 203 | * in the constructors. 204 | */ 205 | protected void init() { 206 | mPreviewPaint = new Paint(); 207 | mPreviewPaint.setTextAlign(Paint.Align.CENTER); 208 | mPreviewPaint.setColor(Color.BLACK); 209 | mPreviewPaint.setTextSize(50); 210 | 211 | mStyles = new Styles(); 212 | mViewport = new Viewport(this); 213 | mGridLabelRenderer = new GridLabelRenderer(this); 214 | mLegendRenderer = new LegendRenderer(this); 215 | 216 | mSeries = new ArrayList(); 217 | mPaintTitle = new Paint(); 218 | 219 | mTapDetector = new TapDetector(); 220 | 221 | loadStyles(); 222 | } 223 | 224 | /** 225 | * loads the font 226 | */ 227 | protected void loadStyles() { 228 | mStyles.titleColor = mGridLabelRenderer.getHorizontalLabelsColor(); 229 | mStyles.titleTextSize = mGridLabelRenderer.getTextSize(); 230 | } 231 | 232 | /** 233 | * @return the renderer for the grid and labels 234 | */ 235 | public GridLabelRenderer getGridLabelRenderer() { 236 | return mGridLabelRenderer; 237 | } 238 | 239 | /** 240 | * Add a new series to the graph. This will 241 | * automatically redraw the graph. 242 | * @param s the series to be added 243 | */ 244 | public void addSeries(Series s) { 245 | s.onGraphViewAttached(this); 246 | mSeries.add(s); 247 | onDataChanged(false, false); 248 | } 249 | 250 | /** 251 | * important: do not do modifications on the list 252 | * object that will be returned. 253 | * Use {@link #removeSeries(com.jjoe64.graphview.series.Series)} and {@link #addSeries(com.jjoe64.graphview.series.Series)} 254 | * 255 | * @return all series 256 | */ 257 | public List getSeries() { 258 | // TODO immutable array 259 | return mSeries; 260 | } 261 | 262 | /** 263 | * call this to let the graph redraw and 264 | * recalculate the viewport. 265 | * This will be called when a new series 266 | * was added or removed and when data 267 | * was appended via {@link com.jjoe64.graphview.series.BaseSeries#appendData(com.jjoe64.graphview.series.DataPointInterface, boolean, int)} 268 | * or {@link com.jjoe64.graphview.series.BaseSeries#resetData(com.jjoe64.graphview.series.DataPointInterface[])}. 269 | * 270 | * @param keepLabelsSize true if you don't want 271 | * to recalculate the size of 272 | * the labels. It is recommended 273 | * to use "true" because this will 274 | * improve performance and prevent 275 | * a flickering. 276 | * @param keepViewport true if you don't want that 277 | * the viewport will be recalculated. 278 | * It is recommended to use "true" for 279 | * performance. 280 | */ 281 | public void onDataChanged(boolean keepLabelsSize, boolean keepViewport) { 282 | // adjustSteps grid system 283 | mViewport.calcCompleteRange(); 284 | if (mSecondScale != null) { 285 | mSecondScale.calcCompleteRange(); 286 | } 287 | mGridLabelRenderer.invalidate(keepLabelsSize, keepViewport); 288 | postInvalidate(); 289 | } 290 | 291 | /** 292 | * draw all the stuff on canvas 293 | * 294 | * @param canvas 295 | */ 296 | protected void drawGraphElements(Canvas canvas) { 297 | // must be in hardware accelerated mode 298 | if (android.os.Build.VERSION.SDK_INT >= 11 && !canvas.isHardwareAccelerated()) { 299 | // just warn about it, because it is ok when making a snapshot 300 | Log.w("GraphView", "GraphView should be used in hardware accelerated mode." + 301 | "You can use android:hardwareAccelerated=\"true\" on your activity. Read this for more info:" + 302 | "https://developer.android.com/guide/topics/graphics/hardware-accel.html"); 303 | } 304 | 305 | drawTitle(canvas); 306 | mViewport.drawFirst(canvas); 307 | mGridLabelRenderer.draw(canvas); 308 | for (Series s : mSeries) { 309 | s.draw(this, canvas, false); 310 | } 311 | if (mSecondScale != null) { 312 | for (Series s : mSecondScale.getSeries()) { 313 | s.draw(this, canvas, true); 314 | } 315 | } 316 | 317 | if (mCursorMode != null) { 318 | mCursorMode.draw(canvas); 319 | } 320 | 321 | mViewport.draw(canvas); 322 | mLegendRenderer.draw(canvas); 323 | } 324 | 325 | /** 326 | * will be called from Android system. 327 | * 328 | * @param canvas Canvas 329 | */ 330 | @Override 331 | protected void onDraw(Canvas canvas) { 332 | if (isInEditMode()) { 333 | canvas.drawColor(Color.rgb(200, 200, 200)); 334 | canvas.drawText("GraphView: No Preview available", canvas.getWidth()/2, canvas.getHeight()/2, mPreviewPaint); 335 | } else { 336 | drawGraphElements(canvas); 337 | } 338 | } 339 | 340 | /** 341 | * Draws the Graphs title that will be 342 | * shown above the viewport. 343 | * Will be called by GraphView. 344 | * 345 | * @param canvas Canvas 346 | */ 347 | protected void drawTitle(Canvas canvas) { 348 | if (mTitle != null && mTitle.length()>0) { 349 | mPaintTitle.setColor(mStyles.titleColor); 350 | mPaintTitle.setTextSize(mStyles.titleTextSize); 351 | mPaintTitle.setTextAlign(Paint.Align.CENTER); 352 | float x = canvas.getWidth()/2; 353 | float y = mPaintTitle.getTextSize(); 354 | canvas.drawText(mTitle, x, y, mPaintTitle); 355 | } 356 | } 357 | 358 | /** 359 | * Calculates the height of the title. 360 | * 361 | * @return the actual size of the title. 362 | * if there is no title, 0 will be 363 | * returned. 364 | */ 365 | protected int getTitleHeight() { 366 | if (mTitle != null && mTitle.length()>0) { 367 | return (int) mPaintTitle.getTextSize(); 368 | } else { 369 | return 0; 370 | } 371 | } 372 | 373 | /** 374 | * @return the viewport of the Graph. 375 | * @see com.jjoe64.graphview.Viewport 376 | */ 377 | public Viewport getViewport() { 378 | return mViewport; 379 | } 380 | 381 | /** 382 | * Called by Android system if the size 383 | * of the view was changed. Will recalculate 384 | * the viewport and labels. 385 | * 386 | * @param w 387 | * @param h 388 | * @param oldw 389 | * @param oldh 390 | */ 391 | @Override 392 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 393 | super.onSizeChanged(w, h, oldw, oldh); 394 | onDataChanged(false, false); 395 | } 396 | 397 | /** 398 | * @return the space on the left side of the 399 | * view from the left border to the 400 | * beginning of the graph viewport. 401 | */ 402 | public int getGraphContentLeft() { 403 | int border = getGridLabelRenderer().getStyles().padding; 404 | return border + getGridLabelRenderer().getLabelVerticalWidth() + getGridLabelRenderer().getVerticalAxisTitleWidth(); 405 | } 406 | 407 | /** 408 | * @return the space on the top of the 409 | * view from the top border to the 410 | * beginning of the graph viewport. 411 | */ 412 | public int getGraphContentTop() { 413 | int border = getGridLabelRenderer().getStyles().padding + getTitleHeight(); 414 | return border; 415 | } 416 | 417 | /** 418 | * @return the height of the graph viewport. 419 | */ 420 | public int getGraphContentHeight() { 421 | int border = getGridLabelRenderer().getStyles().padding; 422 | int graphheight = getHeight() - (2 * border) - getGridLabelRenderer().getLabelHorizontalHeight() - getTitleHeight(); 423 | graphheight -= getGridLabelRenderer().getHorizontalAxisTitleHeight(); 424 | return graphheight; 425 | } 426 | 427 | /** 428 | * @return the width of the graph viewport. 429 | */ 430 | public int getGraphContentWidth() { 431 | int border = getGridLabelRenderer().getStyles().padding; 432 | int graphwidth = getWidth() - (2 * border) - getGridLabelRenderer().getLabelVerticalWidth(); 433 | if (mSecondScale != null) { 434 | graphwidth -= getGridLabelRenderer().getLabelVerticalSecondScaleWidth(); 435 | graphwidth -= mSecondScale.getVerticalAxisTitleTextSize(); 436 | } 437 | return graphwidth; 438 | } 439 | 440 | /** 441 | * will be called from Android system. 442 | * 443 | * @param event 444 | * @return 445 | */ 446 | @Override 447 | public boolean onTouchEvent(MotionEvent event) { 448 | boolean b = mViewport.onTouchEvent(event); 449 | boolean a = super.onTouchEvent(event); 450 | 451 | // is it a click? 452 | if (mTapDetector.onTouchEvent(event)) { 453 | for (Series s : mSeries) { 454 | s.onTap(event.getX(), event.getY()); 455 | } 456 | if (mSecondScale != null) { 457 | for (Series s : mSecondScale.getSeries()) { 458 | s.onTap(event.getX(), event.getY()); 459 | } 460 | } 461 | } 462 | 463 | return b || a; 464 | } 465 | 466 | /** 467 | * 468 | */ 469 | @Override 470 | public void computeScroll() { 471 | super.computeScroll(); 472 | mViewport.computeScroll(); 473 | } 474 | 475 | /** 476 | * @return the legend renderer. 477 | * @see com.jjoe64.graphview.LegendRenderer 478 | */ 479 | public LegendRenderer getLegendRenderer() { 480 | return mLegendRenderer; 481 | } 482 | 483 | /** 484 | * use a specific legend renderer 485 | * 486 | * @param mLegendRenderer the new legend renderer 487 | */ 488 | public void setLegendRenderer(LegendRenderer mLegendRenderer) { 489 | this.mLegendRenderer = mLegendRenderer; 490 | } 491 | 492 | /** 493 | * @return the title that will be shown 494 | * above the graph. 495 | */ 496 | public String getTitle() { 497 | return mTitle; 498 | } 499 | 500 | /** 501 | * Set the title of the graph that will 502 | * be shown above the graph's viewport. 503 | * 504 | * @param mTitle the title 505 | * @see #setTitleColor(int) to set the font color 506 | * @see #setTitleTextSize(float) to set the font size 507 | */ 508 | public void setTitle(String mTitle) { 509 | this.mTitle = mTitle; 510 | } 511 | 512 | /** 513 | * @return the title font size 514 | */ 515 | public float getTitleTextSize() { 516 | return mStyles.titleTextSize; 517 | } 518 | 519 | /** 520 | * Set the title's font size 521 | * 522 | * @param titleTextSize font size 523 | * @see #setTitle(String) 524 | */ 525 | public void setTitleTextSize(float titleTextSize) { 526 | mStyles.titleTextSize = titleTextSize; 527 | } 528 | 529 | /** 530 | * @return font color of the title 531 | */ 532 | public int getTitleColor() { 533 | return mStyles.titleColor; 534 | } 535 | 536 | /** 537 | * Set the title's font color 538 | * 539 | * @param titleColor font color of the title 540 | * @see #setTitle(String) 541 | */ 542 | public void setTitleColor(int titleColor) { 543 | mStyles.titleColor = titleColor; 544 | } 545 | 546 | /** 547 | * creates the second scale logic and returns it 548 | * 549 | * @return second scale object 550 | */ 551 | public SecondScale getSecondScale() { 552 | if (mSecondScale == null) { 553 | // this creates the second scale 554 | mSecondScale = new SecondScale(this); 555 | mSecondScale.setVerticalAxisTitleTextSize(mGridLabelRenderer.mStyles.textSize); 556 | } 557 | return mSecondScale; 558 | } 559 | 560 | /** 561 | * clears the second scale 562 | */ 563 | public void clearSecondScale() { 564 | if (mSecondScale != null) { 565 | mSecondScale.removeAllSeries(); 566 | mSecondScale = null; 567 | } 568 | } 569 | 570 | /** 571 | * Removes all series of the graph. 572 | */ 573 | public void removeAllSeries() { 574 | mSeries.clear(); 575 | onDataChanged(false, false); 576 | } 577 | 578 | /** 579 | * Remove a specific series of the graph. 580 | * This will also re-draw the graph, but 581 | * without recalculating the viewport and 582 | * label sizes. 583 | * If you want this, you have to call {@link #onDataChanged(boolean, boolean)} 584 | * manually. 585 | * 586 | * @param series 587 | */ 588 | public void removeSeries(Series series) { 589 | mSeries.remove(series); 590 | onDataChanged(false, false); 591 | } 592 | 593 | /** 594 | * takes a snapshot and return it as bitmap 595 | * 596 | * @return snapshot of graph 597 | */ 598 | public Bitmap takeSnapshot() { 599 | Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 600 | Canvas canvas = new Canvas(bitmap); 601 | draw(canvas); 602 | return bitmap; 603 | } 604 | 605 | /** 606 | * takes a snapshot, stores it and open the share dialog. 607 | * Notice that you need the permission android.permission.WRITE_EXTERNAL_STORAGE 608 | * 609 | * @param context 610 | * @param imageName 611 | * @param title 612 | */ 613 | public void takeSnapshotAndShare(Context context, String imageName, String title) { 614 | ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 615 | Bitmap inImage = takeSnapshot(); 616 | inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes); 617 | 618 | String path = MediaStore.Images.Media.insertImage(context.getContentResolver(), inImage, imageName, null); 619 | if (path == null) { 620 | // most likely a security problem 621 | throw new SecurityException("Could not get path from MediaStore. Please check permissions."); 622 | } 623 | 624 | Intent i = new Intent(Intent.ACTION_SEND); 625 | i.setType("image/*"); 626 | i.putExtra(Intent.EXTRA_STREAM, Uri.parse(path)); 627 | try { 628 | context.startActivity(Intent.createChooser(i, title)); 629 | } catch (android.content.ActivityNotFoundException ex) { 630 | ex.printStackTrace(); 631 | } 632 | } 633 | 634 | public void setCursorMode(boolean b) { 635 | mIsCursorMode = b; 636 | if (mIsCursorMode) { 637 | if (mCursorMode == null) { 638 | mCursorMode = new CursorMode(this); 639 | } 640 | } else { 641 | mCursorMode = null; 642 | invalidate(); 643 | } 644 | for (Series series : mSeries) { 645 | if (series instanceof BaseSeries) { 646 | ((BaseSeries) series).clearCursorModeCache(); 647 | } 648 | } 649 | } 650 | 651 | public CursorMode getCursorMode() { 652 | return mCursorMode; 653 | } 654 | 655 | public boolean isCursorMode() { 656 | return mIsCursorMode; 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/LabelFormatter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview; 18 | 19 | /** 20 | * Interface to use as label formatter. 21 | * Implement this in order to generate 22 | * your own labels format. 23 | * It is recommended to override {@link com.jjoe64.graphview.DefaultLabelFormatter}. 24 | * 25 | * @author jjoe64 26 | */ 27 | public interface LabelFormatter { 28 | /** 29 | * converts a raw number as input to 30 | * a formatted string for the label. 31 | * 32 | * @param value raw input number 33 | * @param isValueX true if it is a value for the x axis 34 | * false if it is a value for the y axis 35 | * @return the formatted number as string 36 | */ 37 | public String formatLabel(double value, boolean isValueX); 38 | 39 | /** 40 | * will be called in order to have a 41 | * reference to the current viewport. 42 | * This is useful if you need the bounds 43 | * to generate your labels. 44 | * You store this viewport in as member variable 45 | * and access it e.g. in the {@link #formatLabel(double, boolean)} 46 | * method. 47 | * 48 | * @param viewport the used viewport 49 | */ 50 | public void setViewport(Viewport viewport); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/LegendRenderer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview; 18 | 19 | import android.content.res.TypedArray; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Paint; 23 | import android.graphics.Point; 24 | import android.graphics.Rect; 25 | import android.graphics.RectF; 26 | import android.util.TypedValue; 27 | 28 | import com.jjoe64.graphview.series.Series; 29 | 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | 33 | /** 34 | * The default renderer for the legend box 35 | * 36 | * @author jjoe64 37 | */ 38 | public class LegendRenderer { 39 | /** 40 | * wrapped styles regarding to the 41 | * legend 42 | */ 43 | private final class Styles { 44 | float textSize; 45 | int spacing; 46 | int padding; 47 | int width; 48 | int backgroundColor; 49 | int textColor; 50 | int margin; 51 | LegendAlign align; 52 | Point fixedPosition; 53 | } 54 | 55 | /** 56 | * alignment of the legend 57 | */ 58 | public enum LegendAlign { 59 | /** 60 | * top right corner 61 | */ 62 | TOP, 63 | 64 | /** 65 | * middle right 66 | */ 67 | MIDDLE, 68 | 69 | /** 70 | * bottom right corner 71 | */ 72 | BOTTOM 73 | } 74 | 75 | /** 76 | * wrapped styles 77 | */ 78 | private Styles mStyles; 79 | 80 | /** 81 | * reference to the graphview 82 | */ 83 | private final GraphView mGraphView; 84 | 85 | /** 86 | * flag whether legend will be 87 | * drawn 88 | */ 89 | private boolean mIsVisible; 90 | 91 | /** 92 | * paint for the drawing 93 | */ 94 | private Paint mPaint; 95 | 96 | /** 97 | * cached legend width 98 | * this will be filled in the drawing. 99 | * Can be cleared via {@link #resetStyles()} 100 | */ 101 | private int cachedLegendWidth; 102 | 103 | /** 104 | * creates legend renderer 105 | * 106 | * @param graphView regarding graphview 107 | */ 108 | public LegendRenderer(GraphView graphView) { 109 | mGraphView = graphView; 110 | mIsVisible = false; 111 | mPaint = new Paint(); 112 | mPaint.setTextAlign(Paint.Align.LEFT); 113 | mStyles = new Styles(); 114 | cachedLegendWidth = 0; 115 | resetStyles(); 116 | } 117 | 118 | /** 119 | * resets the styles to the defaults 120 | * and clears the legend width cache 121 | */ 122 | public void resetStyles() { 123 | mStyles.align = LegendAlign.MIDDLE; 124 | mStyles.textSize = mGraphView.getGridLabelRenderer().getTextSize(); 125 | mStyles.spacing = (int) (mStyles.textSize / 5); 126 | mStyles.padding = (int) (mStyles.textSize / 2); 127 | mStyles.width = 0; 128 | mStyles.backgroundColor = Color.argb(180, 100, 100, 100); 129 | mStyles.margin = (int) (mStyles.textSize / 5); 130 | 131 | // get matching styles from theme 132 | TypedValue typedValue = new TypedValue(); 133 | mGraphView.getContext().getTheme().resolveAttribute(android.R.attr.textAppearanceSmall, typedValue, true); 134 | 135 | int color1; 136 | 137 | try { 138 | TypedArray array = mGraphView.getContext().obtainStyledAttributes(typedValue.data, new int[]{ 139 | android.R.attr.textColorPrimary}); 140 | color1 = array.getColor(0, Color.BLACK); 141 | array.recycle(); 142 | } catch (Exception e) { 143 | color1 = Color.BLACK; 144 | } 145 | 146 | mStyles.textColor = color1; 147 | 148 | cachedLegendWidth = 0; 149 | } 150 | 151 | protected List getAllSeries() { 152 | List allSeries = new ArrayList(); 153 | allSeries.addAll(mGraphView.getSeries()); 154 | if (mGraphView.mSecondScale != null) { 155 | allSeries.addAll(mGraphView.getSecondScale().getSeries()); 156 | } 157 | return allSeries; 158 | } 159 | 160 | /** 161 | * draws the legend if it is visible 162 | * 163 | * @param canvas canvas 164 | * @see #setVisible(boolean) 165 | */ 166 | public void draw(Canvas canvas) { 167 | if (!mIsVisible) return; 168 | 169 | mPaint.setTextSize(mStyles.textSize); 170 | 171 | int shapeSize = (int) (mStyles.textSize*0.8d); 172 | 173 | List allSeries = getAllSeries(); 174 | 175 | // width 176 | int legendWidth = mStyles.width; 177 | if (legendWidth == 0) { 178 | // auto 179 | legendWidth = cachedLegendWidth; 180 | 181 | if (legendWidth == 0) { 182 | Rect textBounds = new Rect(); 183 | for (Series s : allSeries) { 184 | if (s.getTitle() != null) { 185 | mPaint.getTextBounds(s.getTitle(), 0, s.getTitle().length(), textBounds); 186 | legendWidth = Math.max(legendWidth, textBounds.width()); 187 | } 188 | } 189 | if (legendWidth == 0) legendWidth = 1; 190 | 191 | // add shape size 192 | legendWidth += shapeSize+mStyles.padding*2 + mStyles.spacing; 193 | cachedLegendWidth = legendWidth; 194 | } 195 | } 196 | 197 | // rect 198 | float legendHeight = (mStyles.textSize+mStyles.spacing)*allSeries.size() -mStyles.spacing; 199 | float lLeft; 200 | float lTop; 201 | if (mStyles.fixedPosition != null) { 202 | // use fied position 203 | lLeft = mGraphView.getGraphContentLeft() + mStyles.margin + mStyles.fixedPosition.x; 204 | lTop = mGraphView.getGraphContentTop() + mStyles.margin + mStyles.fixedPosition.y; 205 | } else { 206 | lLeft = mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth() - legendWidth - mStyles.margin; 207 | switch (mStyles.align) { 208 | case TOP: 209 | lTop = mGraphView.getGraphContentTop() + mStyles.margin; 210 | break; 211 | case MIDDLE: 212 | lTop = mGraphView.getHeight() / 2 - legendHeight / 2; 213 | break; 214 | default: 215 | lTop = mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight() - mStyles.margin - legendHeight - 2*mStyles.padding; 216 | } 217 | } 218 | float lRight = lLeft+legendWidth; 219 | float lBottom = lTop+legendHeight+2*mStyles.padding; 220 | mPaint.setColor(mStyles.backgroundColor); 221 | canvas.drawRoundRect(new RectF(lLeft, lTop, lRight, lBottom), 8, 8, mPaint); 222 | 223 | int i=0; 224 | for (Series series : allSeries) { 225 | mPaint.setColor(series.getColor()); 226 | canvas.drawRect(new RectF(lLeft+mStyles.padding, lTop+mStyles.padding+(i*(mStyles.textSize+mStyles.spacing)), lLeft+mStyles.padding+shapeSize, lTop+mStyles.padding+(i*(mStyles.textSize+mStyles.spacing))+shapeSize), mPaint); 227 | if (series.getTitle() != null) { 228 | mPaint.setColor(mStyles.textColor); 229 | canvas.drawText(series.getTitle(), lLeft+mStyles.padding+shapeSize+mStyles.spacing, lTop+mStyles.padding+mStyles.textSize+(i*(mStyles.textSize+mStyles.spacing)), mPaint); 230 | } 231 | i++; 232 | } 233 | } 234 | 235 | /** 236 | * @return the flag whether the legend will be drawn 237 | */ 238 | public boolean isVisible() { 239 | return mIsVisible; 240 | } 241 | 242 | /** 243 | * set the flag whether the legend will be drawn 244 | * 245 | * @param mIsVisible visible flag 246 | */ 247 | public void setVisible(boolean mIsVisible) { 248 | this.mIsVisible = mIsVisible; 249 | } 250 | 251 | /** 252 | * @return font size 253 | */ 254 | public float getTextSize() { 255 | return mStyles.textSize; 256 | } 257 | 258 | /** 259 | * sets the font size. this will clear 260 | * the internal legend width cache 261 | * 262 | * @param textSize font size 263 | */ 264 | public void setTextSize(float textSize) { 265 | mStyles.textSize = textSize; 266 | cachedLegendWidth = 0; 267 | } 268 | 269 | /** 270 | * @return the spacing between the text lines 271 | */ 272 | public int getSpacing() { 273 | return mStyles.spacing; 274 | } 275 | 276 | /** 277 | * set the spacing between the text lines 278 | * 279 | * @param spacing the spacing between the text lines 280 | */ 281 | public void setSpacing(int spacing) { 282 | mStyles.spacing = spacing; 283 | } 284 | 285 | /** 286 | * padding is the space between the edge of the box 287 | * and the beginning of the text 288 | * 289 | * @return padding from edge to text 290 | */ 291 | public int getPadding() { 292 | return mStyles.padding; 293 | } 294 | 295 | /** 296 | * padding is the space between the edge of the box 297 | * and the beginning of the text 298 | * 299 | * @param padding padding from edge to text 300 | */ 301 | public void setPadding(int padding) { 302 | mStyles.padding = padding; 303 | } 304 | 305 | /** 306 | * the width of the box exclusive padding 307 | * 308 | * @return the width of the box 309 | * 0 => auto 310 | */ 311 | public int getWidth() { 312 | return mStyles.width; 313 | } 314 | 315 | /** 316 | * the width of the box exclusive padding 317 | * @param width the width of the box exclusive padding 318 | * 0 => auto 319 | */ 320 | public void setWidth(int width) { 321 | mStyles.width = width; 322 | } 323 | 324 | /** 325 | * @return background color of the box 326 | * it is recommended to use semi-transparent 327 | * color. 328 | */ 329 | public int getBackgroundColor() { 330 | return mStyles.backgroundColor; 331 | } 332 | 333 | /** 334 | * @param backgroundColor background color of the box 335 | * it is recommended to use semi-transparent 336 | * color. 337 | */ 338 | public void setBackgroundColor(int backgroundColor) { 339 | mStyles.backgroundColor = backgroundColor; 340 | } 341 | 342 | /** 343 | * @return margin from the edge of the box 344 | * to the corner of the graphview 345 | */ 346 | public int getMargin() { 347 | return mStyles.margin; 348 | } 349 | 350 | /** 351 | * @param margin margin from the edge of the box 352 | * to the corner of the graphview 353 | */ 354 | public void setMargin(int margin) { 355 | mStyles.margin = margin; 356 | } 357 | 358 | /** 359 | * @return the vertical alignment of the box 360 | */ 361 | public LegendAlign getAlign() { 362 | return mStyles.align; 363 | } 364 | 365 | /** 366 | * @param align the vertical alignment of the box 367 | */ 368 | public void setAlign(LegendAlign align) { 369 | mStyles.align = align; 370 | } 371 | 372 | /** 373 | * @return font color 374 | */ 375 | public int getTextColor() { 376 | return mStyles.textColor; 377 | } 378 | 379 | /** 380 | * @param textColor font color 381 | */ 382 | public void setTextColor(int textColor) { 383 | mStyles.textColor = textColor; 384 | } 385 | 386 | /** 387 | * Use fixed coordinates to position the legend. 388 | * This will override the align setting. 389 | * 390 | * @param x x coordinates in pixel 391 | * @param y y coordinates in pixel 392 | */ 393 | public void setFixedPosition(int x, int y) { 394 | mStyles.fixedPosition = new Point(x, y); 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/RectD.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview; 18 | 19 | import android.graphics.RectF; 20 | 21 | /** 22 | * Created by jonas on 05.06.16. 23 | */ 24 | public class RectD { 25 | public double left; 26 | public double right; 27 | public double top; 28 | public double bottom; 29 | 30 | public RectD() { 31 | } 32 | 33 | public RectD(double lLeft, double lTop, double lRight, double lBottom) { 34 | set(lLeft, lTop, lRight, lBottom); 35 | } 36 | 37 | public double width() { 38 | return right-left; 39 | } 40 | 41 | public double height() { 42 | return bottom-top; 43 | } 44 | 45 | public void set(double lLeft, double lTop, double lRight, double lBottom) { 46 | left = lLeft; 47 | right = lRight; 48 | top = lTop; 49 | bottom = lBottom; 50 | } 51 | 52 | public RectF toRectF() { 53 | return new RectF((float) left, (float) top, (float) right, (float) bottom); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/SecondScale.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview; 18 | 19 | import android.graphics.Canvas; 20 | import android.graphics.Paint; 21 | 22 | import com.jjoe64.graphview.series.Series; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | /** 28 | * To be used to plot a second scale 29 | * on the graph. 30 | * The second scale has always to have 31 | * manual bounds. 32 | * Use {@link #setMinY(double)} and {@link #setMaxY(double)} 33 | * to set them. 34 | * The second scale has it's own array of series. 35 | * 36 | * @author jjoe64 37 | */ 38 | public class SecondScale { 39 | /** 40 | * reference to the graph 41 | */ 42 | protected final GraphView mGraph; 43 | 44 | /** 45 | * array of series for the second 46 | * scale 47 | */ 48 | protected List mSeries; 49 | 50 | /** 51 | * flag whether the y axis bounds 52 | * are manual. 53 | * For the current version this is always 54 | * true. 55 | */ 56 | private boolean mYAxisBoundsManual = true; 57 | 58 | /** 59 | * 60 | */ 61 | protected RectD mCompleteRange = new RectD(); 62 | 63 | protected RectD mCurrentViewport = new RectD(); 64 | 65 | /** 66 | * label formatter for the y labels 67 | * on the right side 68 | */ 69 | protected LabelFormatter mLabelFormatter; 70 | 71 | protected double mReferenceY = Double.NaN; 72 | 73 | /** 74 | * the paint to draw axis titles 75 | */ 76 | private Paint mPaintAxisTitle; 77 | 78 | /** 79 | * the title of the vertical axis 80 | */ 81 | private String mVerticalAxisTitle; 82 | 83 | /** 84 | * font size of the vertical axis title 85 | */ 86 | public float mVerticalAxisTitleTextSize; 87 | 88 | /** 89 | * font color of the vertical axis title 90 | */ 91 | public int mVerticalAxisTitleColor; 92 | 93 | /** 94 | * creates the second scale. 95 | * normally you do not call this contructor. 96 | * Use {@link com.jjoe64.graphview.GraphView#getSecondScale()} 97 | * in order to get the instance. 98 | */ 99 | SecondScale(GraphView graph) { 100 | mGraph = graph; 101 | mSeries = new ArrayList(); 102 | mLabelFormatter = new DefaultLabelFormatter(); 103 | mLabelFormatter.setViewport(mGraph.getViewport()); 104 | } 105 | 106 | /** 107 | * add a series to the second scale. 108 | * Don't add this series also to the GraphView 109 | * object. 110 | * 111 | * @param s the series 112 | */ 113 | public void addSeries(Series s) { 114 | s.onGraphViewAttached(mGraph); 115 | mSeries.add(s); 116 | mGraph.onDataChanged(false, false); 117 | } 118 | 119 | //public void setYAxisBoundsManual(boolean mYAxisBoundsManual) { 120 | // this.mYAxisBoundsManual = mYAxisBoundsManual; 121 | //} 122 | 123 | /** 124 | * set the min y bounds 125 | * 126 | * @param d min y value 127 | */ 128 | public void setMinY(double d) { 129 | mReferenceY = d; 130 | mCurrentViewport.bottom = d; 131 | } 132 | 133 | /** 134 | * set the max y bounds 135 | * 136 | * @param d max y value 137 | */ 138 | public void setMaxY(double d) { 139 | mCurrentViewport.top = d; 140 | } 141 | 142 | /** 143 | * @return the series of the second scale 144 | */ 145 | public List getSeries() { 146 | return mSeries; 147 | } 148 | 149 | /** 150 | * @return min y bound 151 | */ 152 | public double getMinY(boolean completeRange) { 153 | return completeRange ? mCompleteRange.bottom : mCurrentViewport.bottom; 154 | } 155 | 156 | /** 157 | * @return max y bound 158 | */ 159 | public double getMaxY(boolean completeRange) { 160 | return completeRange ? mCompleteRange.top : mCurrentViewport.top; 161 | } 162 | 163 | /** 164 | * @return always true for the current implementation 165 | */ 166 | public boolean isYAxisBoundsManual() { 167 | return mYAxisBoundsManual; 168 | } 169 | 170 | /** 171 | * @return label formatter for the y labels on the right side 172 | */ 173 | public LabelFormatter getLabelFormatter() { 174 | return mLabelFormatter; 175 | } 176 | 177 | /** 178 | * Set a custom label formatter that is used 179 | * for the y labels on the right side. 180 | * 181 | * @param formatter label formatter for the y labels 182 | */ 183 | public void setLabelFormatter(LabelFormatter formatter) { 184 | mLabelFormatter = formatter; 185 | mLabelFormatter.setViewport(mGraph.getViewport()); 186 | } 187 | 188 | /** 189 | * Removes all series of the graph. 190 | */ 191 | public void removeAllSeries() { 192 | mSeries.clear(); 193 | mGraph.onDataChanged(false, false); 194 | } 195 | 196 | /** 197 | * Remove a specific series of the 198 | * second scale. 199 | * 200 | * @param series 201 | */ 202 | public void removeSeries(Series series) { 203 | mSeries.remove(series); 204 | mGraph.onDataChanged(false, false); 205 | } 206 | 207 | /** 208 | * caches the complete range (minX, maxX, minY, maxY) 209 | * by iterating all series and all datapoints and 210 | * stores it into #mCompleteRange 211 | * 212 | * for the x-range it will respect the series on the 213 | * second scale - not for y-values 214 | */ 215 | public void calcCompleteRange() { 216 | List series = getSeries(); 217 | mCompleteRange.set(0d, 0d, 0d, 0d); 218 | if (!series.isEmpty() && !series.get(0).isEmpty()) { 219 | double d = series.get(0).getLowestValueX(); 220 | for (Series s : series) { 221 | if (!s.isEmpty() && d > s.getLowestValueX()) { 222 | d = s.getLowestValueX(); 223 | } 224 | } 225 | mCompleteRange.left = d; 226 | 227 | d = series.get(0).getHighestValueX(); 228 | for (Series s : series) { 229 | if (!s.isEmpty() && d < s.getHighestValueX()) { 230 | d = s.getHighestValueX(); 231 | } 232 | } 233 | mCompleteRange.right = d; 234 | 235 | if (!series.isEmpty() && !series.get(0).isEmpty()) { 236 | d = series.get(0).getLowestValueY(); 237 | for (Series s : series) { 238 | if (!s.isEmpty() && d > s.getLowestValueY()) { 239 | d = s.getLowestValueY(); 240 | } 241 | } 242 | mCompleteRange.bottom = d; 243 | 244 | d = series.get(0).getHighestValueY(); 245 | for (Series s : series) { 246 | if (!s.isEmpty() && d < s.getHighestValueY()) { 247 | d = s.getHighestValueY(); 248 | } 249 | } 250 | mCompleteRange.top = d; 251 | } 252 | } 253 | } 254 | 255 | /** 256 | * @return the title of the vertical axis 257 | */ 258 | public String getVerticalAxisTitle() { 259 | return mVerticalAxisTitle; 260 | } 261 | 262 | /** 263 | * @param mVerticalAxisTitle the title of the vertical axis 264 | */ 265 | public void setVerticalAxisTitle(String mVerticalAxisTitle) { 266 | if(mPaintAxisTitle==null) { 267 | mPaintAxisTitle = new Paint(); 268 | mPaintAxisTitle.setTextSize(getVerticalAxisTitleTextSize()); 269 | mPaintAxisTitle.setTextAlign(Paint.Align.CENTER); 270 | } 271 | this.mVerticalAxisTitle = mVerticalAxisTitle; 272 | } 273 | 274 | /** 275 | * @return font size of the vertical axis title 276 | */ 277 | public float getVerticalAxisTitleTextSize() { 278 | if (getVerticalAxisTitle() == null || getVerticalAxisTitle().length() == 0) { 279 | return 0; 280 | } 281 | return mVerticalAxisTitleTextSize; 282 | } 283 | 284 | /** 285 | * @param verticalAxisTitleTextSize font size of the vertical axis title 286 | */ 287 | public void setVerticalAxisTitleTextSize(float verticalAxisTitleTextSize) { 288 | mVerticalAxisTitleTextSize = verticalAxisTitleTextSize; 289 | } 290 | 291 | /** 292 | * @return font color of the vertical axis title 293 | */ 294 | public int getVerticalAxisTitleColor() { 295 | return mVerticalAxisTitleColor; 296 | } 297 | 298 | /** 299 | * @param verticalAxisTitleColor font color of the vertical axis title 300 | */ 301 | public void setVerticalAxisTitleColor(int verticalAxisTitleColor) { 302 | mVerticalAxisTitleColor = verticalAxisTitleColor; 303 | } 304 | 305 | /** 306 | * draws the vertical axis title if 307 | * it is set 308 | * @param canvas canvas 309 | */ 310 | protected void drawVerticalAxisTitle(Canvas canvas) { 311 | if (mVerticalAxisTitle != null && mVerticalAxisTitle.length() > 0) { 312 | mPaintAxisTitle.setColor(getVerticalAxisTitleColor()); 313 | mPaintAxisTitle.setTextSize(getVerticalAxisTitleTextSize()); 314 | float x = canvas.getWidth() - getVerticalAxisTitleTextSize()/2; 315 | float y = canvas.getHeight() / 2; 316 | canvas.save(); 317 | canvas.rotate(-90, x, y); 318 | canvas.drawText(mVerticalAxisTitle, x, y, mPaintAxisTitle); 319 | canvas.restore(); 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/UniqueLegendRenderer.java: -------------------------------------------------------------------------------- 1 | package com.jjoe64.graphview; 2 | 3 | import android.util.Pair; 4 | 5 | import com.jjoe64.graphview.series.Series; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | /** 13 | * A LegendRenderer that renders items with the same name and color only once in the legend 14 | * Created by poseidon on 27.02.18. 15 | */ 16 | public class UniqueLegendRenderer extends LegendRenderer { 17 | /** 18 | * creates legend renderer 19 | * 20 | * @param graphView regarding graphview 21 | */ 22 | public UniqueLegendRenderer(GraphView graphView) { 23 | super(graphView); 24 | } 25 | 26 | @Override 27 | protected List getAllSeries() { 28 | List originalSeries = super.getAllSeries(); 29 | List distinctSeries = new ArrayList(); 30 | Set> uniqueSeriesKeys = new HashSet>(); 31 | for(Series series : originalSeries) 32 | if(uniqueSeriesKeys.add(new Pair(series.getColor(), series.getTitle()))) 33 | distinctSeries.add(series); 34 | return distinctSeries; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/ValueDependentColor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview; 18 | 19 | import com.jjoe64.graphview.series.DataPointInterface; 20 | 21 | /** 22 | * you can change the color depending on the value. 23 | * takes only effect for BarGraphSeries. 24 | * 25 | * @see com.jjoe64.graphview.series.BarGraphSeries#setValueDependentColor(ValueDependentColor) 26 | */ 27 | public interface ValueDependentColor { 28 | /** 29 | * this is called when a bar is about to draw 30 | * and the color is be loaded. 31 | * 32 | * @param data the current input value 33 | * @return the color that the bar should be drawn with 34 | * Generate the int via the android.graphics.Color class. 35 | */ 36 | public int get(T data); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/compat/OverScrollerCompat.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.compat; 18 | 19 | import android.annotation.TargetApi; 20 | import android.os.Build; 21 | import android.widget.OverScroller; 22 | 23 | /** 24 | * A utility class for using {@link android.widget.OverScroller} in a backward-compatible fashion. 25 | */ 26 | public class OverScrollerCompat { 27 | /** 28 | * Disallow instantiation. 29 | */ 30 | private OverScrollerCompat() { 31 | } 32 | /** 33 | * @see android.view.ScaleGestureDetector#getCurrentSpanY() 34 | */ 35 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 36 | public static float getCurrVelocity(OverScroller overScroller) { 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 38 | return overScroller.getCurrVelocity(); 39 | } else { 40 | return 0; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/helper/DateAsXAxisLabelFormatter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.helper; 18 | 19 | import android.content.Context; 20 | 21 | import com.jjoe64.graphview.DefaultLabelFormatter; 22 | 23 | import java.text.DateFormat; 24 | import java.util.Calendar; 25 | import java.util.Date; 26 | 27 | /** 28 | * Helper class to use date objects as x-values. 29 | * This will use your own Date Format or by default 30 | * the Android default date format to convert 31 | * the x-values (that has to be millis from 32 | * 01-01-1970) into a formatted date string. 33 | * 34 | * See the DateAsXAxis example in the GraphView-Demos project 35 | * to see a working example. 36 | * 37 | * @author jjoe64 38 | */ 39 | public class DateAsXAxisLabelFormatter extends DefaultLabelFormatter { 40 | /** 41 | * the date format that will convert 42 | * the unix timestamp to string 43 | */ 44 | protected final DateFormat mDateFormat; 45 | 46 | /** 47 | * calendar to avoid creating new date objects 48 | */ 49 | protected final Calendar mCalendar; 50 | 51 | /** 52 | * create the formatter with the Android default date format to convert 53 | * the x-values. 54 | * 55 | * @param context the application context 56 | */ 57 | public DateAsXAxisLabelFormatter(Context context) { 58 | mDateFormat = android.text.format.DateFormat.getDateFormat(context); 59 | mCalendar = Calendar.getInstance(); 60 | } 61 | 62 | /** 63 | * create the formatter with your own custom 64 | * date format to convert the x-values. 65 | * 66 | * @param context the application context 67 | * @param dateFormat custom date format 68 | */ 69 | public DateAsXAxisLabelFormatter(Context context, DateFormat dateFormat) { 70 | mDateFormat = dateFormat; 71 | mCalendar = Calendar.getInstance(); 72 | } 73 | 74 | /** 75 | * formats the x-values as date string. 76 | * 77 | * @param value raw value 78 | * @param isValueX true if it's a x value, otherwise false 79 | * @return value converted to string 80 | */ 81 | @Override 82 | public String formatLabel(double value, boolean isValueX) { 83 | if (isValueX) { 84 | // format as date 85 | mCalendar.setTimeInMillis((long) value); 86 | return mDateFormat.format(mCalendar.getTimeInMillis()); 87 | } else { 88 | return super.formatLabel(value, isValueX); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/helper/GraphViewXML.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.helper; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Color; 22 | import android.util.AttributeSet; 23 | import android.util.Log; 24 | 25 | import com.jjoe64.graphview.GraphView; 26 | import com.jjoe64.graphview.R; 27 | import com.jjoe64.graphview.series.BarGraphSeries; 28 | import com.jjoe64.graphview.series.BaseSeries; 29 | import com.jjoe64.graphview.series.DataPoint; 30 | import com.jjoe64.graphview.series.DataPointInterface; 31 | import com.jjoe64.graphview.series.LineGraphSeries; 32 | import com.jjoe64.graphview.series.PointsGraphSeries; 33 | import com.jjoe64.graphview.series.Series; 34 | 35 | /** 36 | * helper class to use GraphView directly 37 | * in a XML layout file. 38 | * 39 | * You can set the data via attribute app:seriesData 40 | * in the format: "X=Y;X=Y;..." e.g. "0=5.0;1=5;2=4;3=9" 41 | * 42 | * Other styling options: 43 | *
  • app:seriesType="line|bar|points"
  • 44 | *
  • app:seriesColor="#ff0000"
  • 45 | *
  • app:seriesTitle="foobar" - if this is set, the legend will be drawn
  • 46 | *
  • android:title="foobar"
  • 47 | * 48 | * Example: 49 | *
     50 |  * {@code
     51 |  *  
     57 |  * }
     58 |  * 
    59 | * 60 | * @author jjoe64 61 | */ 62 | public class GraphViewXML extends GraphView { 63 | /** 64 | * creates the graphview object with data and 65 | * other options from xml attributes. 66 | * 67 | * @param context 68 | * @param attrs 69 | */ 70 | public GraphViewXML(Context context, AttributeSet attrs) { 71 | super(context, attrs); 72 | 73 | // get attributes 74 | TypedArray a=context.obtainStyledAttributes( 75 | attrs, 76 | R.styleable.GraphViewXML); 77 | 78 | String dataStr = a.getString(R.styleable.GraphViewXML_seriesData); 79 | int color = a.getColor(R.styleable.GraphViewXML_seriesColor, Color.TRANSPARENT); 80 | String type = a.getString(R.styleable.GraphViewXML_seriesType); 81 | String seriesTitle = a.getString(R.styleable.GraphViewXML_seriesTitle); 82 | String title = a.getString(R.styleable.GraphViewXML_android_title); 83 | 84 | a.recycle(); 85 | 86 | // decode data 87 | DataPoint[] data; 88 | if (dataStr == null || dataStr.isEmpty()) { 89 | throw new IllegalArgumentException("Attribute seriesData is required in the format: 0=5.0;1=5;2=4;3=9"); 90 | } else { 91 | String[] d = dataStr.split(";"); 92 | try { 93 | data = new DataPoint[d.length]; 94 | int i = 0; 95 | for (String dd : d) { 96 | String[] xy = dd.split("="); 97 | data[i] = new DataPoint(Double.parseDouble(xy[0]), Double.parseDouble(xy[1])); 98 | i++; 99 | } 100 | } catch (Exception e) { 101 | Log.e("GraphViewXML", e.toString()); 102 | throw new IllegalArgumentException("Attribute seriesData is broken. Use this format: 0=5.0;1=5;2=4;3=9"); 103 | } 104 | } 105 | 106 | // create series 107 | BaseSeries series; 108 | if (type == null || type.isEmpty()) { 109 | type = "line"; 110 | } 111 | if (type.equals("line")) { 112 | series = new LineGraphSeries(data); 113 | } else if (type.equals("bar")) { 114 | series = new BarGraphSeries(data); 115 | } else if (type.equals("points")) { 116 | series = new PointsGraphSeries(data); 117 | } else { 118 | throw new IllegalArgumentException("unknown graph type: "+type+". Possible is line|bar|points"); 119 | } 120 | if (color != Color.TRANSPARENT) { 121 | series.setColor(color); 122 | } 123 | addSeries(series); 124 | 125 | if (seriesTitle != null && !seriesTitle.isEmpty()) { 126 | series.setTitle(seriesTitle); 127 | getLegendRenderer().setVisible(true); 128 | } 129 | 130 | if (title != null && !title.isEmpty()) { 131 | setTitle(title); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.helper; 18 | 19 | import com.jjoe64.graphview.DefaultLabelFormatter; 20 | import com.jjoe64.graphview.GraphView; 21 | import com.jjoe64.graphview.LabelFormatter; 22 | import com.jjoe64.graphview.Viewport; 23 | 24 | /** 25 | * Use this label formatter to show static labels. 26 | * Static labels are not bound to the data. It is typical used 27 | * for show text like "low", "middle", "high". 28 | * 29 | * You can set the static labels for vertical or horizontal 30 | * individually and you can define a label formatter that 31 | * is to be used if you don't define static labels. 32 | * 33 | * For example if you only use static labels for horizontal labels, 34 | * graphview will use the dynamicLabelFormatter for the vertical labels. 35 | */ 36 | public class StaticLabelsFormatter implements LabelFormatter { 37 | /** 38 | * reference to the viewport 39 | */ 40 | protected Viewport mViewport; 41 | 42 | /** 43 | * the vertical labels, ordered from bottom to the top 44 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 45 | */ 46 | protected String[] mVerticalLabels; 47 | 48 | /** 49 | * the horizontal labels, ordered form the left to the right 50 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 51 | */ 52 | protected String[] mHorizontalLabels; 53 | 54 | /** 55 | * the label formatter that will format the labels 56 | * for that there are no static labels defined. 57 | */ 58 | protected LabelFormatter mDynamicLabelFormatter; 59 | 60 | /** 61 | * reference to the graphview 62 | */ 63 | protected final GraphView mGraphView; 64 | 65 | /** 66 | * creates the formatter without any static labels 67 | * define your static labels via {@link #setHorizontalLabels(String[])} and {@link #setVerticalLabels(String[])} 68 | * 69 | * @param graphView reference to the graphview 70 | */ 71 | public StaticLabelsFormatter(GraphView graphView) { 72 | mGraphView = graphView; 73 | init(null, null, null); 74 | } 75 | 76 | /** 77 | * creates the formatter without any static labels. 78 | * define your static labels via {@link #setHorizontalLabels(String[])} and {@link #setVerticalLabels(String[])} 79 | * 80 | * @param graphView reference to the graphview 81 | * @param dynamicLabelFormatter the label formatter that will format the labels 82 | * for that there are no static labels defined. 83 | */ 84 | public StaticLabelsFormatter(GraphView graphView, LabelFormatter dynamicLabelFormatter) { 85 | mGraphView = graphView; 86 | init(null, null, dynamicLabelFormatter); 87 | } 88 | 89 | /** 90 | * creates the formatter with static labels defined. 91 | * 92 | * @param graphView reference to the graphview 93 | * @param horizontalLabels the horizontal labels, ordered form the left to the right 94 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 95 | * @param verticalLabels the vertical labels, ordered from bottom to the top 96 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 97 | */ 98 | public StaticLabelsFormatter(GraphView graphView, String[] horizontalLabels, String[] verticalLabels) { 99 | mGraphView = graphView; 100 | init(horizontalLabels, verticalLabels, null); 101 | } 102 | 103 | /** 104 | * creates the formatter with static labels defined. 105 | * 106 | * @param graphView reference to the graphview 107 | * @param horizontalLabels the horizontal labels, ordered form the left to the right 108 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 109 | * @param verticalLabels the vertical labels, ordered from bottom to the top 110 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 111 | * @param dynamicLabelFormatter the label formatter that will format the labels 112 | * for that there are no static labels defined. 113 | */ 114 | public StaticLabelsFormatter(GraphView graphView, String[] horizontalLabels, String[] verticalLabels, LabelFormatter dynamicLabelFormatter) { 115 | mGraphView = graphView; 116 | init(horizontalLabels, verticalLabels, dynamicLabelFormatter); 117 | } 118 | 119 | /** 120 | * @param horizontalLabels the horizontal labels, ordered form the left to the right 121 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 122 | * @param verticalLabels the vertical labels, ordered from bottom to the top 123 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 124 | * @param dynamicLabelFormatter the label formatter that will format the labels 125 | * for that there are no static labels defined. 126 | */ 127 | protected void init(String[] horizontalLabels, String[] verticalLabels, LabelFormatter dynamicLabelFormatter) { 128 | mDynamicLabelFormatter = dynamicLabelFormatter; 129 | if (mDynamicLabelFormatter == null) { 130 | mDynamicLabelFormatter = new DefaultLabelFormatter(); 131 | } 132 | 133 | mHorizontalLabels = horizontalLabels; 134 | mVerticalLabels = verticalLabels; 135 | } 136 | 137 | /** 138 | * Set a label formatter that will be used for the labels 139 | * that don't have static labels. 140 | * 141 | * For example if you only use static labels for horizontal labels, 142 | * graphview will use the dynamicLabelFormatter for the vertical labels. 143 | * 144 | * @param dynamicLabelFormatter the label formatter that will format the labels 145 | * for that there are no static labels defined. 146 | */ 147 | public void setDynamicLabelFormatter(LabelFormatter dynamicLabelFormatter) { 148 | this.mDynamicLabelFormatter = dynamicLabelFormatter; 149 | adjust(); 150 | } 151 | 152 | /** 153 | * @param horizontalLabels the horizontal labels, ordered form the left to the right 154 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 155 | */ 156 | public void setHorizontalLabels(String[] horizontalLabels) { 157 | this.mHorizontalLabels = horizontalLabels; 158 | adjust(); 159 | } 160 | 161 | /** 162 | * @param verticalLabels the vertical labels, ordered from bottom to the top 163 | * if it is null, the labels will be generated via the #dynamicLabelFormatter 164 | */ 165 | public void setVerticalLabels(String[] verticalLabels) { 166 | this.mVerticalLabels = verticalLabels; 167 | adjust(); 168 | } 169 | 170 | /** 171 | * 172 | * @param value raw input number 173 | * @param isValueX true if it is a value for the x axis 174 | * false if it is a value for the y axis 175 | * @return 176 | */ 177 | @Override 178 | public String formatLabel(double value, boolean isValueX) { 179 | if (isValueX && mHorizontalLabels != null) { 180 | double minX = mViewport.getMinX(false); 181 | double maxX = mViewport.getMaxX(false); 182 | double range = maxX - minX; 183 | value = value-minX; 184 | int idx = (int)((value/range) * (mHorizontalLabels.length-1)); 185 | return mHorizontalLabels[idx]; 186 | } else if (!isValueX && mVerticalLabels != null) { 187 | double minY = mViewport.getMinY(false); 188 | double maxY = mViewport.getMaxY(false); 189 | double range = maxY - minY; 190 | value = value-minY; 191 | int idx = (int)((value/range) * (mVerticalLabels.length-1)); 192 | return mVerticalLabels[idx]; 193 | } else { 194 | return mDynamicLabelFormatter.formatLabel(value, isValueX); 195 | } 196 | } 197 | 198 | /** 199 | * @param viewport the used viewport 200 | */ 201 | @Override 202 | public void setViewport(Viewport viewport) { 203 | mViewport = viewport; 204 | adjust(); 205 | } 206 | 207 | /** 208 | * adjusts the number of vertical/horizontal labels 209 | */ 210 | protected void adjust() { 211 | mDynamicLabelFormatter.setViewport(mViewport); 212 | if (mVerticalLabels != null) { 213 | if (mVerticalLabels.length < 2) { 214 | throw new IllegalStateException("You need at least 2 vertical labels if you use static label formatter."); 215 | } 216 | mGraphView.getGridLabelRenderer().setNumVerticalLabels(mVerticalLabels.length); 217 | } 218 | if (mHorizontalLabels != null) { 219 | if (mHorizontalLabels.length < 2) { 220 | throw new IllegalStateException("You need at least 2 horizontal labels if you use static label formatter."); 221 | } 222 | mGraphView.getGridLabelRenderer().setNumHorizontalLabels(mHorizontalLabels.length); 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.series; 18 | 19 | import android.graphics.Canvas; 20 | import android.graphics.Paint; 21 | import android.graphics.RectF; 22 | import androidx.core.view.ViewCompat; 23 | import android.util.Log; 24 | import android.view.animation.AccelerateInterpolator; 25 | 26 | import com.jjoe64.graphview.GraphView; 27 | import com.jjoe64.graphview.RectD; 28 | import com.jjoe64.graphview.ValueDependentColor; 29 | 30 | import java.util.HashMap; 31 | import java.util.Iterator; 32 | import java.util.Map; 33 | import java.util.SortedSet; 34 | import java.util.TreeSet; 35 | 36 | /** 37 | * Series with Bars to visualize the data. 38 | * The Bars are always vertical. 39 | * 40 | * @author jjoe64 41 | */ 42 | public class BarGraphSeries extends BaseSeries { 43 | private static final long ANIMATION_DURATION = 333; 44 | 45 | /** 46 | * paint to do drawing on canvas 47 | */ 48 | private Paint mPaint; 49 | 50 | /** 51 | * custom paint that can be used. 52 | * this will ignore the value dependent color. 53 | */ 54 | private Paint mCustomPaint; 55 | 56 | /** 57 | * spacing between the bars in percentage. 58 | * 0 => no spacing 59 | * 100 => the space between the bars is as big as the bars itself 60 | */ 61 | private int mSpacing; 62 | 63 | /** 64 | * width of a data point 65 | * 0 => no prior knowledge of sampling period, interval between bars will be calculated automatically 66 | * >0 => value is the total distance from one bar to another 67 | */ 68 | private double mDataWidth; 69 | 70 | /** 71 | * callback to generate value-dependent colors 72 | * of the bars 73 | */ 74 | private ValueDependentColor mValueDependentColor; 75 | 76 | /** 77 | * flag whether the values should drawn 78 | * above the bars as text 79 | */ 80 | private boolean mDrawValuesOnTop; 81 | 82 | /** 83 | * color of the text above the bars. 84 | * 85 | * @see #mDrawValuesOnTop 86 | */ 87 | private int mValuesOnTopColor; 88 | 89 | /** 90 | * font size of the text above the bars. 91 | * 92 | * @see #mDrawValuesOnTop 93 | */ 94 | private float mValuesOnTopSize; 95 | 96 | /** 97 | * stores the coordinates of the bars to 98 | * trigger tap on series events. 99 | */ 100 | private Map mDataPoints = new HashMap(); 101 | 102 | /** 103 | * flag for animated rendering 104 | */ 105 | private boolean mAnimated; 106 | 107 | /** 108 | * store the last value that was animated 109 | */ 110 | private double mLastAnimatedValue = Double.NaN; 111 | 112 | /** 113 | * time of start animation 114 | */ 115 | private long mAnimationStart; 116 | 117 | /** 118 | * animation interpolator 119 | */ 120 | private AccelerateInterpolator mAnimationInterpolator; 121 | 122 | /** 123 | * frame number of animation to avoid lagging 124 | */ 125 | private int mAnimationStartFrameNo; 126 | 127 | 128 | /** 129 | * creates bar series without any data 130 | */ 131 | public BarGraphSeries() { 132 | mPaint = new Paint(); 133 | } 134 | 135 | /** 136 | * creates bar series with data 137 | * 138 | * @param data data points 139 | * important: array has to be sorted from lowest x-value to the highest 140 | */ 141 | public BarGraphSeries(E[] data) { 142 | super(data); 143 | mPaint = new Paint(); 144 | mAnimationInterpolator = new AccelerateInterpolator(2f); 145 | } 146 | 147 | /** 148 | * draws the bars on the canvas 149 | * 150 | * @param graphView corresponding graphview 151 | * @param canvas canvas 152 | * @param isSecondScale whether we are plotting the second scale or not 153 | */ 154 | @Override 155 | public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { 156 | mPaint.setTextAlign(Paint.Align.CENTER); 157 | if (mValuesOnTopSize == 0) { 158 | mValuesOnTopSize = graphView.getGridLabelRenderer().getTextSize(); 159 | } 160 | mPaint.setTextSize(mValuesOnTopSize); 161 | 162 | resetDataPoints(); 163 | 164 | // get data 165 | double maxX = graphView.getViewport().getMaxX(false); 166 | double minX = graphView.getViewport().getMinX(false); 167 | 168 | double maxY; 169 | double minY; 170 | if (isSecondScale) { 171 | maxY = graphView.getSecondScale().getMaxY(false); 172 | minY = graphView.getSecondScale().getMinY(false); 173 | } else { 174 | maxY = graphView.getViewport().getMaxY(false); 175 | minY = graphView.getViewport().getMinY(false); 176 | } 177 | 178 | // Iterate through all bar graph series 179 | // so we know how wide to make our bar, 180 | // and in what position to put it in 181 | int numBarSeries = 0; 182 | int currentSeriesOrder = 0; 183 | int numValues = 0; 184 | boolean isCurrentSeries; 185 | SortedSet xVals = new TreeSet(); 186 | for(Series inspectedSeries: graphView.getSeries()) { 187 | if(inspectedSeries instanceof BarGraphSeries) { 188 | isCurrentSeries = (inspectedSeries == this); 189 | if(isCurrentSeries) { 190 | currentSeriesOrder = numBarSeries; 191 | } 192 | numBarSeries++; 193 | 194 | // calculate the number of slots for bars based on the minimum distance between 195 | // x coordinates in the series. This is divided into the range to find 196 | // the placement and width of bar slots 197 | // (sections of the x axis for each bar or set of bars) 198 | // TODO: Move this somewhere more general and cache it, so we don't recalculate it for each series 199 | Iterator curValues = inspectedSeries.getValues(minX, maxX); 200 | if (curValues.hasNext()) { 201 | xVals.add(curValues.next().getX()); 202 | if(isCurrentSeries) { numValues++; } 203 | while (curValues.hasNext()) { 204 | xVals.add(curValues.next().getX()); 205 | if(isCurrentSeries) { numValues++; } 206 | } 207 | } 208 | } 209 | } 210 | if (numValues == 0) { 211 | return; 212 | } 213 | 214 | double minGap = 0; 215 | 216 | if(mDataWidth > 0.0) { 217 | minGap = mDataWidth; 218 | } else { 219 | Double lastVal = null; 220 | 221 | for(Double curVal: xVals) { 222 | if(lastVal != null) { 223 | double curGap = Math.abs(curVal - lastVal); 224 | if (minGap == 0 || (curGap > 0 && curGap < minGap)) { 225 | minGap = curGap; 226 | } 227 | } 228 | lastVal = curVal; 229 | } 230 | } 231 | 232 | int numBarSlots = (minGap == 0) ? 1 : (int)Math.round((maxX - minX)/minGap) + 1; 233 | 234 | Iterator values = getValues(minX, maxX); 235 | 236 | // Calculate the overall bar slot width - this includes all bars across 237 | // all series, and any spacing between sets of bars 238 | int barSlotWidth = numBarSlots == 1 239 | ? graphView.getGraphContentWidth() 240 | : graphView.getGraphContentWidth() / (numBarSlots-1); 241 | 242 | // Total spacing (both sides) between sets of bars 243 | double spacing = Math.min(barSlotWidth*mSpacing/100, barSlotWidth*0.98f); 244 | // Width of an individual bar 245 | double barWidth = (barSlotWidth - spacing) / numBarSeries; 246 | // Offset from the center of a given bar to start drawing 247 | double offset = barSlotWidth/2; 248 | 249 | double diffY = maxY - minY; 250 | double diffX = maxX - minX; 251 | double contentHeight = graphView.getGraphContentHeight(); 252 | double contentWidth = graphView.getGraphContentWidth(); 253 | double contentLeft = graphView.getGraphContentLeft(); 254 | double contentTop = graphView.getGraphContentTop(); 255 | 256 | // draw data 257 | int i=0; 258 | while (values.hasNext()) { 259 | E value = values.next(); 260 | 261 | double valY = value.getY() - minY; 262 | double ratY = valY / diffY; 263 | double y = contentHeight * ratY; 264 | 265 | double valY0 = 0 - minY; 266 | double ratY0 = valY0 / diffY; 267 | double y0 = contentHeight * ratY0; 268 | 269 | double valueX = value.getX(); 270 | double valX = valueX - minX; 271 | double ratX = valX / diffX; 272 | double x = contentWidth * ratX; 273 | 274 | // hook for value dependent color 275 | if (getValueDependentColor() != null) { 276 | mPaint.setColor(getValueDependentColor().get(value)); 277 | } else { 278 | mPaint.setColor(getColor()); 279 | } 280 | 281 | double left = x + contentLeft - offset + spacing/2 + currentSeriesOrder*barWidth; 282 | double right = left + barWidth; 283 | if (left > contentLeft + contentWidth || right < contentLeft) { 284 | continue; 285 | } 286 | double top = (contentTop - y) + contentHeight; 287 | double bottom = (contentTop - y0) + contentHeight - (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1); 288 | 289 | boolean reverse = top > bottom; 290 | 291 | if (mAnimated) { 292 | if ((Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { 293 | long currentTime = System.currentTimeMillis(); 294 | if (mAnimationStart == 0) { 295 | // start animation 296 | mAnimationStart = currentTime; 297 | mAnimationStartFrameNo = 0; 298 | } else { 299 | // anti-lag: wait a few frames 300 | if (mAnimationStartFrameNo < 15) { 301 | // second time 302 | mAnimationStart = currentTime; 303 | mAnimationStartFrameNo++; 304 | } 305 | } 306 | float timeFactor = (float) (currentTime-mAnimationStart) / ANIMATION_DURATION; 307 | float factor = mAnimationInterpolator.getInterpolation(timeFactor); 308 | if (timeFactor <= 1.0) { 309 | double barHeight = bottom - top; 310 | barHeight = barHeight * factor; 311 | top = bottom-barHeight; 312 | ViewCompat.postInvalidateOnAnimation(graphView); 313 | } else { 314 | // animation finished 315 | mLastAnimatedValue = valueX; 316 | } 317 | } 318 | } 319 | 320 | if (reverse) { 321 | double tmp = top; 322 | top = bottom + (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1); 323 | bottom = tmp; 324 | } 325 | 326 | // overdraw 327 | left = Math.max(left, contentLeft); 328 | right = Math.min(right, contentLeft+contentWidth); 329 | bottom = Math.min(bottom, contentTop+contentHeight); 330 | top = Math.max(top, contentTop); 331 | 332 | mDataPoints.put(new RectD(left, top, right, bottom), value); 333 | 334 | Paint p; 335 | if (mCustomPaint != null) { 336 | p = mCustomPaint; 337 | } else { 338 | p = mPaint; 339 | } 340 | canvas.drawRect((float)left, (float)top, (float)right, (float)bottom, p); 341 | 342 | // set values on top of graph 343 | if (mDrawValuesOnTop) { 344 | if (reverse) { 345 | top = bottom + mValuesOnTopSize + 4; 346 | if (top > contentTop+contentHeight) top = contentTop + contentHeight; 347 | } else { 348 | top -= 4; 349 | if (top<=contentTop) top+=contentTop+4; 350 | } 351 | 352 | mPaint.setColor(mValuesOnTopColor); 353 | canvas.drawText( 354 | graphView.getGridLabelRenderer().getLabelFormatter().formatLabel(value.getY(), false) 355 | , (float) (left+right)/2, (float) top, mPaint); 356 | } 357 | 358 | i++; 359 | } 360 | } 361 | 362 | /** 363 | * @return the hook to generate value-dependent color. default null 364 | */ 365 | public ValueDependentColor getValueDependentColor() { 366 | return mValueDependentColor; 367 | } 368 | 369 | /** 370 | * set a hook to make the color of the bars depending 371 | * on the actually value/data. 372 | * 373 | * @param mValueDependentColor hook 374 | * null to disable 375 | */ 376 | public void setValueDependentColor(ValueDependentColor mValueDependentColor) { 377 | this.mValueDependentColor = mValueDependentColor; 378 | } 379 | 380 | /** 381 | * @return the spacing between the bars in percentage 382 | */ 383 | public int getSpacing() { 384 | return mSpacing; 385 | } 386 | 387 | /** 388 | * @param mSpacing spacing between the bars in percentage. 389 | * 0 => no spacing 390 | * 100 => the space between the bars is as big as the bars itself 391 | */ 392 | public void setSpacing(int mSpacing) { 393 | this.mSpacing = mSpacing; 394 | } 395 | 396 | /** 397 | * @return the interval between data points 398 | */ 399 | public double getDataWidth() { 400 | return mDataWidth; 401 | } 402 | 403 | /** 404 | * @param mDataWidth width of a data point (sampling period) 405 | * 0 => no prior knowledge of sampling period, interval between bars will be calculated automatically 406 | * >0 => value is the total distance from one bar to another 407 | */ 408 | public void setDataWidth(double mDataWidth) { 409 | this.mDataWidth = mDataWidth; 410 | } 411 | 412 | /** 413 | * @return whether the values should be drawn above the bars 414 | */ 415 | public boolean isDrawValuesOnTop() { 416 | return mDrawValuesOnTop; 417 | } 418 | 419 | /** 420 | * @param mDrawValuesOnTop flag whether the values should drawn 421 | * above the bars as text 422 | */ 423 | public void setDrawValuesOnTop(boolean mDrawValuesOnTop) { 424 | this.mDrawValuesOnTop = mDrawValuesOnTop; 425 | } 426 | 427 | /** 428 | * @return font color of the values on top of the bars 429 | * @see #setDrawValuesOnTop(boolean) 430 | */ 431 | public int getValuesOnTopColor() { 432 | return mValuesOnTopColor; 433 | } 434 | 435 | /** 436 | * @param mValuesOnTopColor the font color of the values on top of the bars 437 | * @see #setDrawValuesOnTop(boolean) 438 | */ 439 | public void setValuesOnTopColor(int mValuesOnTopColor) { 440 | this.mValuesOnTopColor = mValuesOnTopColor; 441 | } 442 | 443 | /** 444 | * @return font size of the values above the bars 445 | * @see #setDrawValuesOnTop(boolean) 446 | */ 447 | public float getValuesOnTopSize() { 448 | return mValuesOnTopSize; 449 | } 450 | 451 | /** 452 | * @param mValuesOnTopSize font size of the values above the bars 453 | * @see #setDrawValuesOnTop(boolean) 454 | */ 455 | public void setValuesOnTopSize(float mValuesOnTopSize) { 456 | this.mValuesOnTopSize = mValuesOnTopSize; 457 | } 458 | 459 | /** 460 | * resets the cached coordinates of the bars 461 | */ 462 | @Override 463 | protected void resetDataPoints() { 464 | mDataPoints.clear(); 465 | } 466 | 467 | /** 468 | * find the corresponding data point by 469 | * coordinates. 470 | * 471 | * @param x pixels 472 | * @param y pixels 473 | * @return datapoint or null 474 | */ 475 | @Override 476 | protected E findDataPoint(float x, float y) { 477 | for (Map.Entry entry : mDataPoints.entrySet()) { 478 | if (x >= entry.getKey().left && x <= entry.getKey().right 479 | && y >= entry.getKey().top && y <= entry.getKey().bottom) { 480 | return entry.getValue(); 481 | } 482 | } 483 | return null; 484 | } 485 | 486 | /** 487 | * custom paint that can be used. 488 | * this will ignore the value dependent color. 489 | * 490 | * @return custom paint or null 491 | */ 492 | public Paint getCustomPaint() { 493 | return mCustomPaint; 494 | } 495 | 496 | /** 497 | * custom paint that can be used. 498 | * this will ignore the value dependent color. 499 | * 500 | * @param mCustomPaint custom paint to use or null 501 | */ 502 | public void setCustomPaint(Paint mCustomPaint) { 503 | this.mCustomPaint = mCustomPaint; 504 | } 505 | 506 | /** 507 | * draw the series with an animation 508 | * 509 | * @param animated animation activated or not 510 | */ 511 | public void setAnimated(boolean animated) { 512 | this.mAnimated = animated; 513 | } 514 | 515 | /** 516 | * @return rendering is animated or not 517 | */ 518 | public boolean isAnimated() { 519 | return mAnimated; 520 | } 521 | 522 | @Override 523 | public void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value) { 524 | // TODO 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/series/BaseSeries.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.series; 18 | 19 | import android.graphics.Canvas; 20 | import android.graphics.PointF; 21 | import android.util.Log; 22 | 23 | import com.jjoe64.graphview.GraphView; 24 | 25 | import java.lang.ref.WeakReference; 26 | import java.util.ArrayList; 27 | import java.util.HashMap; 28 | import java.util.Iterator; 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.NoSuchElementException; 32 | import java.util.Set; 33 | 34 | /** 35 | * Basis implementation for series. 36 | * Used for series that are plotted on 37 | * a default x/y 2d viewport. 38 | * 39 | * Extend this class to implement your own custom 40 | * graph type. 41 | * 42 | * This implementation uses a internal Array to store 43 | * the data. If you want to implement a custom data provider 44 | * you may want to implement {@link com.jjoe64.graphview.series.Series}. 45 | * 46 | * @author jjoe64 47 | */ 48 | public abstract class BaseSeries implements Series { 49 | /** 50 | * holds the data 51 | */ 52 | final private List mData = new ArrayList(); 53 | 54 | /** 55 | * stores the used coordinates to find the 56 | * corresponding data point on a tap 57 | * 58 | * Key => x/y pixel 59 | * Value => Plotted Datapoint 60 | * 61 | * will be filled while drawing via {@link #registerDataPoint(float, float, DataPointInterface)} 62 | */ 63 | private Map mDataPoints = new HashMap(); 64 | 65 | /** 66 | * title for this series that can be displayed 67 | * in the legend. 68 | */ 69 | private String mTitle; 70 | 71 | /** 72 | * base color for this series. will be used also in 73 | * the legend 74 | */ 75 | private int mColor = 0xff0077cc; 76 | 77 | /** 78 | * cache for lowest y value 79 | */ 80 | private double mLowestYCache = Double.NaN; 81 | 82 | /** 83 | * cahce for highest y value 84 | */ 85 | private double mHighestYCache = Double.NaN; 86 | 87 | /** 88 | * listener to handle tap events on a data point 89 | */ 90 | protected OnDataPointTapListener mOnDataPointTapListener; 91 | 92 | /** 93 | * stores the graphviews where this series is used. 94 | * Can be more than one. 95 | */ 96 | private List> mGraphViews; 97 | private Boolean mIsCursorModeCache; 98 | 99 | /** 100 | * creates series without data 101 | */ 102 | public BaseSeries() { 103 | mGraphViews = new ArrayList<>(); 104 | } 105 | 106 | /** 107 | * creates series with data 108 | * 109 | * @param data data points 110 | * important: array has to be sorted from lowest x-value to the highest 111 | */ 112 | public BaseSeries(E[] data) { 113 | mGraphViews = new ArrayList<>(); 114 | for (E d : data) { 115 | mData.add(d); 116 | } 117 | checkValueOrder(null); 118 | } 119 | 120 | /** 121 | * @return the lowest x value, or 0 if there is no data 122 | */ 123 | public double getLowestValueX() { 124 | if (mData.isEmpty()) return 0d; 125 | return mData.get(0).getX(); 126 | } 127 | 128 | /** 129 | * @return the highest x value, or 0 if there is no data 130 | */ 131 | public double getHighestValueX() { 132 | if (mData.isEmpty()) return 0d; 133 | return mData.get(mData.size()-1).getX(); 134 | } 135 | 136 | /** 137 | * @return the lowest y value, or 0 if there is no data 138 | */ 139 | public double getLowestValueY() { 140 | if (mData.isEmpty()) return 0d; 141 | if (!Double.isNaN(mLowestYCache)) { 142 | return mLowestYCache; 143 | } 144 | double l = mData.get(0).getY(); 145 | for (int i = 1; i < mData.size(); i++) { 146 | double c = mData.get(i).getY(); 147 | if (l > c) { 148 | l = c; 149 | } 150 | } 151 | return mLowestYCache = l; 152 | } 153 | 154 | /** 155 | * @return the highest y value, or 0 if there is no data 156 | */ 157 | public double getHighestValueY() { 158 | if (mData.isEmpty()) return 0d; 159 | if (!Double.isNaN(mHighestYCache)) { 160 | return mHighestYCache; 161 | } 162 | double h = mData.get(0).getY(); 163 | for (int i = 1; i < mData.size(); i++) { 164 | double c = mData.get(i).getY(); 165 | if (h < c) { 166 | h = c; 167 | } 168 | } 169 | return mHighestYCache = h; 170 | } 171 | 172 | /** 173 | * get the values for a given x range. if from and until are bigger or equal than 174 | * all the data, the original data is returned. 175 | * If it is only a part of the data, the range is returned plus one datapoint 176 | * before and after to get a nice scrolling. 177 | * 178 | * @param from minimal x-value 179 | * @param until maximal x-value 180 | * @return data for the range +/- 1 datapoint 181 | */ 182 | @Override 183 | public Iterator getValues(final double from, final double until) { 184 | if (from <= getLowestValueX() && until >= getHighestValueX()) { 185 | return mData.iterator(); 186 | } else { 187 | return new Iterator() { 188 | Iterator org = mData.iterator(); 189 | E nextValue = null; 190 | E nextNextValue = null; 191 | boolean plusOne = true; 192 | 193 | { 194 | // go to first 195 | boolean found = false; 196 | E prevValue = null; 197 | if (org.hasNext()) { 198 | prevValue = org.next(); 199 | } 200 | if (prevValue != null) { 201 | if (prevValue.getX() >= from) { 202 | nextValue = prevValue; 203 | found = true; 204 | } else { 205 | while (org.hasNext()) { 206 | nextValue = org.next(); 207 | if (nextValue.getX() >= from) { 208 | found = true; 209 | nextNextValue = nextValue; 210 | nextValue = prevValue; 211 | break; 212 | } 213 | prevValue = nextValue; 214 | } 215 | } 216 | } 217 | if (!found) { 218 | nextValue = null; 219 | } 220 | } 221 | 222 | @Override 223 | public void remove() { 224 | throw new UnsupportedOperationException(); 225 | } 226 | 227 | @Override 228 | public E next() { 229 | if (hasNext()) { 230 | E r = nextValue; 231 | if (r.getX() > until) { 232 | plusOne = false; 233 | } 234 | if (nextNextValue != null) { 235 | nextValue = nextNextValue; 236 | nextNextValue = null; 237 | } else if (org.hasNext()) nextValue = org.next(); 238 | else nextValue = null; 239 | return r; 240 | } else { 241 | throw new NoSuchElementException(); 242 | } 243 | } 244 | 245 | @Override 246 | public boolean hasNext() { 247 | return nextValue != null && (nextValue.getX() <= until || plusOne); 248 | } 249 | }; 250 | } 251 | } 252 | 253 | /** 254 | * @return the title of the series 255 | */ 256 | public String getTitle() { 257 | return mTitle; 258 | } 259 | 260 | /** 261 | * set the title of the series. This will be used in 262 | * the legend. 263 | * 264 | * @param mTitle title of the series 265 | */ 266 | public void setTitle(String mTitle) { 267 | this.mTitle = mTitle; 268 | } 269 | 270 | /** 271 | * @return color of the series 272 | */ 273 | public int getColor() { 274 | return mColor; 275 | } 276 | 277 | /** 278 | * set the color of the series. This will be used in 279 | * plotting (depends on the series implementation) and 280 | * is used in the legend. 281 | * 282 | * @param mColor 283 | */ 284 | public void setColor(int mColor) { 285 | this.mColor = mColor; 286 | } 287 | 288 | /** 289 | * set a listener for tap on a data point. 290 | * 291 | * @param l listener 292 | */ 293 | public void setOnDataPointTapListener(OnDataPointTapListener l) { 294 | this.mOnDataPointTapListener = l; 295 | } 296 | 297 | /** 298 | * called by the tap detector in order to trigger 299 | * the on tap on datapoint event. 300 | * 301 | * @param x pixel 302 | * @param y pixel 303 | */ 304 | @Override 305 | public void onTap(float x, float y) { 306 | if (mOnDataPointTapListener != null) { 307 | E p = findDataPoint(x, y); 308 | if (p != null) { 309 | mOnDataPointTapListener.onTap(this, p); 310 | } 311 | } 312 | } 313 | 314 | /** 315 | * find the data point which is next to the 316 | * coordinates 317 | * 318 | * @param x pixel 319 | * @param y pixel 320 | * @return the data point or null if nothing was found 321 | */ 322 | protected E findDataPoint(float x, float y) { 323 | float shortestDistance = Float.NaN; 324 | E shortest = null; 325 | for (Map.Entry entry : mDataPoints.entrySet()) { 326 | float x1 = entry.getKey().x; 327 | float y1 = entry.getKey().y; 328 | float x2 = x; 329 | float y2 = y; 330 | 331 | float distance = (float) Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)); 332 | if (shortest == null || distance < shortestDistance) { 333 | shortestDistance = distance; 334 | shortest = entry.getValue(); 335 | } 336 | } 337 | if (shortest != null) { 338 | if (shortestDistance < 120) { 339 | return shortest; 340 | } 341 | } 342 | return null; 343 | } 344 | 345 | public E findDataPointAtX(float x) { 346 | float shortestDistance = Float.NaN; 347 | E shortest = null; 348 | for (Map.Entry entry : mDataPoints.entrySet()) { 349 | float x1 = entry.getKey().x; 350 | float x2 = x; 351 | 352 | float distance = Math.abs(x1 - x2); 353 | if (shortest == null || distance < shortestDistance) { 354 | shortestDistance = distance; 355 | shortest = entry.getValue(); 356 | } 357 | } 358 | if (shortest != null) { 359 | if (shortestDistance < 200) { 360 | return shortest; 361 | } 362 | } 363 | return null; 364 | } 365 | 366 | /** 367 | * register the datapoint to find it at a tap 368 | * 369 | * @param x pixel 370 | * @param y pixel 371 | * @param dp the data point to save 372 | */ 373 | protected void registerDataPoint(float x, float y, E dp) { 374 | // performance 375 | // TODO maybe invalidate after setting the listener 376 | if (mOnDataPointTapListener != null || isCursorMode()) { 377 | mDataPoints.put(new PointF(x, y), dp); 378 | } 379 | } 380 | 381 | private boolean isCursorMode() { 382 | if (mIsCursorModeCache != null) { 383 | return mIsCursorModeCache; 384 | } 385 | for (WeakReference graphView : mGraphViews) { 386 | if (graphView != null && graphView.get() != null && graphView.get().isCursorMode()) { 387 | return mIsCursorModeCache = true; 388 | } 389 | } 390 | return mIsCursorModeCache = false; 391 | } 392 | 393 | /** 394 | * clears the cached data point coordinates 395 | */ 396 | protected void resetDataPoints() { 397 | mDataPoints.clear(); 398 | } 399 | 400 | /** 401 | * clears the data of this series and sets new. 402 | * will redraw the graph 403 | * 404 | * @param data the values must be in the correct order! 405 | * x-value has to be ASC. First the lowest x value and at least the highest x value. 406 | */ 407 | public void resetData(E[] data) { 408 | mData.clear(); 409 | for (E d : data) { 410 | mData.add(d); 411 | } 412 | checkValueOrder(null); 413 | 414 | mHighestYCache = mLowestYCache = Double.NaN; 415 | 416 | // update graphview 417 | for (WeakReference gv : mGraphViews) { 418 | if (gv != null && gv.get() != null) { 419 | gv.get().onDataChanged(true, false); 420 | } 421 | } 422 | } 423 | 424 | /** 425 | * stores the reference of the used graph 426 | * 427 | * @param graphView graphview 428 | */ 429 | @Override 430 | public void onGraphViewAttached(GraphView graphView) { 431 | mGraphViews.add(new WeakReference<>(graphView)); 432 | } 433 | 434 | /** 435 | * 436 | * @param dataPoint values the values must be in the correct order! 437 | * x-value has to be ASC. First the lowest x value and at least the highest x value. 438 | * @param scrollToEnd true => graphview will scroll to the end (maxX) 439 | * @param maxDataPoints if max data count is reached, the oldest data 440 | * value will be lost to avoid memory leaks 441 | * @param silent set true to avoid rerender the graph 442 | */ 443 | public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, boolean silent) { 444 | checkValueOrder(dataPoint); 445 | 446 | if (!mData.isEmpty() && dataPoint.getX() < mData.get(mData.size()-1).getX()) { 447 | throw new IllegalArgumentException("new x-value must be greater then the last value. x-values has to be ordered in ASC."); 448 | } 449 | synchronized (mData) { 450 | int curDataCount = mData.size(); 451 | if (curDataCount < maxDataPoints) { 452 | // enough space 453 | mData.add(dataPoint); 454 | } else { 455 | // we have to trim one data 456 | mData.remove(0); 457 | mData.add(dataPoint); 458 | } 459 | 460 | // update lowest/highest cache 461 | double dataPointY = dataPoint.getY(); 462 | if (!Double.isNaN(mHighestYCache)) { 463 | if (dataPointY > mHighestYCache) { 464 | mHighestYCache = dataPointY; 465 | } 466 | } 467 | if (!Double.isNaN(mLowestYCache)) { 468 | if (dataPointY < mLowestYCache) { 469 | mLowestYCache = dataPointY; 470 | } 471 | } 472 | 473 | } 474 | 475 | if (!silent) { 476 | // recalc the labels when it was the first data 477 | boolean keepLabels = mData.size() != 1; 478 | 479 | // update linked graph views 480 | // update graphview 481 | for (WeakReference gv : mGraphViews) { 482 | if (gv != null && gv.get() != null) { 483 | if (scrollToEnd) { 484 | gv.get().getViewport().scrollToEnd(); 485 | } else { 486 | gv.get().onDataChanged(keepLabels, scrollToEnd); 487 | } 488 | } 489 | } 490 | } 491 | } 492 | 493 | /** 494 | * 495 | * @param dataPoint values the values must be in the correct order! 496 | * x-value has to be ASC. First the lowest x value and at least the highest x value. 497 | * @param scrollToEnd true => graphview will scroll to the end (maxX) 498 | * @param maxDataPoints if max data count is reached, the oldest data 499 | * value will be lost to avoid memory leaks 500 | */ 501 | public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints) { 502 | appendData(dataPoint, scrollToEnd, maxDataPoints, false); 503 | } 504 | 505 | /** 506 | * @return whether there are data points 507 | */ 508 | @Override 509 | public boolean isEmpty() { 510 | return mData.isEmpty(); 511 | } 512 | 513 | /** 514 | * checks that the data is in the correct order 515 | * 516 | * @param onlyLast if not null, it will only check that this 517 | * datapoint is after the last point. 518 | */ 519 | protected void checkValueOrder(DataPointInterface onlyLast) { 520 | if (mData.size()>1) { 521 | if (onlyLast != null) { 522 | // only check last 523 | if (onlyLast.getX() < mData.get(mData.size()-1).getX()) { 524 | throw new IllegalArgumentException("new x-value must be greater then the last value. x-values has to be ordered in ASC."); 525 | } 526 | } else { 527 | double lx = mData.get(0).getX(); 528 | 529 | for (int i = 1; i < mData.size(); i++) { 530 | if (mData.get(i).getX() != Double.NaN) { 531 | if (lx > mData.get(i).getX()) { 532 | 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."); 533 | } 534 | lx = mData.get(i).getX(); 535 | } 536 | } 537 | } 538 | } 539 | } 540 | 541 | public abstract void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value); 542 | 543 | public void clearCursorModeCache() { 544 | mIsCursorModeCache = null; 545 | } 546 | 547 | @Override 548 | public void clearReference(GraphView graphView) { 549 | // find and remove 550 | for (WeakReference view : mGraphViews) { 551 | if (view != null && view.get() != null && view.get() == graphView) { 552 | mGraphViews.remove(view); 553 | break; 554 | } 555 | } 556 | } 557 | } 558 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/series/DataPoint.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.series; 18 | 19 | import android.provider.ContactsContract; 20 | 21 | import java.io.Serializable; 22 | import java.util.Date; 23 | 24 | /** 25 | * default data point implementation. 26 | * This stores the x and y values. 27 | * 28 | * @author jjoe64 29 | */ 30 | public class DataPoint implements DataPointInterface, Serializable { 31 | private static final long serialVersionUID=1428263322645L; 32 | 33 | private double x; 34 | private double y; 35 | 36 | public DataPoint(double x, double y) { 37 | this.x=x; 38 | this.y=y; 39 | } 40 | 41 | public DataPoint(Date x, double y) { 42 | this.x = x.getTime(); 43 | this.y = y; 44 | } 45 | 46 | @Override 47 | public double getX() { 48 | return x; 49 | } 50 | 51 | @Override 52 | public double getY() { 53 | return y; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return "["+x+"/"+y+"]"; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/series/DataPointInterface.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.series; 18 | 19 | /** 20 | * interface of data points. Implement this in order 21 | * to use your class in {@link com.jjoe64.graphview.series.Series}. 22 | * 23 | * You can also use the default implementation {@link com.jjoe64.graphview.series.DataPoint} so 24 | * you do not have to implement it for yourself. 25 | * 26 | * @author jjoe64 27 | */ 28 | public interface DataPointInterface { 29 | /** 30 | * @return the x value 31 | */ 32 | public double getX(); 33 | 34 | /** 35 | * @return the y value 36 | */ 37 | public double getY(); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | *

    5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | *

    9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | *

    11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.series; 18 | 19 | import android.graphics.Canvas; 20 | import android.graphics.Color; 21 | import android.graphics.Paint; 22 | import android.graphics.Path; 23 | import androidx.core.view.ViewCompat; 24 | import android.view.animation.AccelerateInterpolator; 25 | 26 | import com.jjoe64.graphview.GraphView; 27 | 28 | import java.util.Iterator; 29 | 30 | /** 31 | * Series to plot the data as line. 32 | * The line can be styled with many options. 33 | * 34 | * @author jjoe64 35 | */ 36 | public class LineGraphSeries extends BaseSeries { 37 | private static final long ANIMATION_DURATION = 333; 38 | 39 | /** 40 | * wrapped styles regarding the line 41 | */ 42 | private final class Styles { 43 | /** 44 | * the thickness of the line. 45 | * This option will be ignored if you are 46 | * using a custom paint via {@link #setCustomPaint(android.graphics.Paint)} 47 | */ 48 | private int thickness = 5; 49 | 50 | /** 51 | * flag whether the area under the line to the bottom 52 | * of the viewport will be filled with a 53 | * specific background color. 54 | * 55 | * @see #backgroundColor 56 | */ 57 | private boolean drawBackground = false; 58 | 59 | /** 60 | * flag whether the data points are highlighted as 61 | * a visible point. 62 | * 63 | * @see #dataPointsRadius 64 | */ 65 | private boolean drawDataPoints = false; 66 | 67 | /** 68 | * the radius for the data points. 69 | * 70 | * @see #drawDataPoints 71 | */ 72 | private float dataPointsRadius = 10f; 73 | 74 | /** 75 | * the background color for the filling under 76 | * the line. 77 | * 78 | * @see #drawBackground 79 | */ 80 | private int backgroundColor = Color.argb(100, 172, 218, 255); 81 | } 82 | 83 | /** 84 | * wrapped styles 85 | */ 86 | private Styles mStyles; 87 | 88 | private Paint mSelectionPaint; 89 | 90 | /** 91 | * internal paint object 92 | */ 93 | private Paint mPaint; 94 | 95 | /** 96 | * paint for the background 97 | */ 98 | private Paint mPaintBackground; 99 | 100 | /** 101 | * path for the background filling 102 | */ 103 | private Path mPathBackground; 104 | 105 | /** 106 | * path to the line 107 | */ 108 | private Path mPath; 109 | 110 | /** 111 | * custom paint that can be used. 112 | * this will ignore the thickness and color styles. 113 | */ 114 | private Paint mCustomPaint; 115 | 116 | /** 117 | * rendering is animated 118 | */ 119 | private boolean mAnimated; 120 | 121 | /** 122 | * last animated value 123 | */ 124 | private double mLastAnimatedValue = Double.NaN; 125 | 126 | /** 127 | * time of animation start 128 | */ 129 | private long mAnimationStart; 130 | 131 | /** 132 | * animation interpolator 133 | */ 134 | private AccelerateInterpolator mAnimationInterpolator; 135 | 136 | /** 137 | * number of animation frame to avoid lagging 138 | */ 139 | private int mAnimationStartFrameNo; 140 | 141 | /** 142 | * flag whether the line should be drawn as a path 143 | * or with single drawLine commands (more performance) 144 | * By default we use drawLine because it has much more peformance. 145 | * For some styling reasons it can make sense to draw as path. 146 | */ 147 | private boolean mDrawAsPath = false; 148 | 149 | /** 150 | * creates a series without data 151 | */ 152 | public LineGraphSeries() { 153 | init(); 154 | } 155 | 156 | /** 157 | * creates a series with data 158 | * 159 | * @param data data points 160 | * important: array has to be sorted from lowest x-value to the highest 161 | */ 162 | public LineGraphSeries(E[] data) { 163 | super(data); 164 | init(); 165 | } 166 | 167 | /** 168 | * do the initialization 169 | * creates internal objects 170 | */ 171 | protected void init() { 172 | mStyles = new Styles(); 173 | mPaint = new Paint(); 174 | mPaint.setStrokeCap(Paint.Cap.ROUND); 175 | mPaint.setStyle(Paint.Style.STROKE); 176 | mPaintBackground = new Paint(); 177 | 178 | mSelectionPaint = new Paint(); 179 | mSelectionPaint.setColor(Color.argb(80, 0, 0, 0)); 180 | mSelectionPaint.setStyle(Paint.Style.FILL); 181 | 182 | mPathBackground = new Path(); 183 | mPath = new Path(); 184 | 185 | mAnimationInterpolator = new AccelerateInterpolator(2f); 186 | } 187 | 188 | /** 189 | * plots the series 190 | * draws the line and the background 191 | * 192 | * @param graphView graphview 193 | * @param canvas canvas 194 | * @param isSecondScale flag if it is the second scale 195 | */ 196 | @Override 197 | public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { 198 | resetDataPoints(); 199 | 200 | // get data 201 | double maxX = graphView.getViewport().getMaxX(false); 202 | double minX = graphView.getViewport().getMinX(false); 203 | 204 | double maxY; 205 | double minY; 206 | if (isSecondScale) { 207 | maxY = graphView.getSecondScale().getMaxY(false); 208 | minY = graphView.getSecondScale().getMinY(false); 209 | } else { 210 | maxY = graphView.getViewport().getMaxY(false); 211 | minY = graphView.getViewport().getMinY(false); 212 | } 213 | 214 | Iterator values = getValues(minX, maxX); 215 | 216 | // draw background 217 | double lastEndY = 0; 218 | double lastEndX = 0; 219 | 220 | // draw data 221 | mPaint.setStrokeWidth(mStyles.thickness); 222 | mPaint.setColor(getColor()); 223 | mPaintBackground.setColor(mStyles.backgroundColor); 224 | 225 | Paint paint; 226 | if (mCustomPaint != null) { 227 | paint = mCustomPaint; 228 | } else { 229 | paint = mPaint; 230 | } 231 | 232 | mPath.reset(); 233 | 234 | if (mStyles.drawBackground) { 235 | mPathBackground.reset(); 236 | } 237 | 238 | double diffY = maxY - minY; 239 | double diffX = maxX - minX; 240 | 241 | float graphHeight = graphView.getGraphContentHeight(); 242 | float graphWidth = graphView.getGraphContentWidth(); 243 | float graphLeft = graphView.getGraphContentLeft(); 244 | float graphTop = graphView.getGraphContentTop(); 245 | 246 | lastEndY = 0; 247 | lastEndX = 0; 248 | 249 | // needed to end the path for background 250 | double lastUsedEndX = 0; 251 | double lastUsedEndY = 0; 252 | float firstX = -1; 253 | float firstY = -1; 254 | float lastRenderedX = Float.NaN; 255 | int i = 0; 256 | float lastAnimationReferenceX = graphLeft; 257 | 258 | boolean sameXSkip = false; 259 | float minYOnSameX = 0f; 260 | float maxYOnSameX = 0f; 261 | 262 | while (values.hasNext()) { 263 | E value = values.next(); 264 | 265 | double valY = value.getY() - minY; 266 | double ratY = valY / diffY; 267 | double y = graphHeight * ratY; 268 | 269 | double valueX = value.getX(); 270 | double valX = valueX - minX; 271 | double ratX = valX / diffX; 272 | double x = graphWidth * ratX; 273 | 274 | double orgX = x; 275 | double orgY = y; 276 | 277 | if (i > 0) { 278 | // overdraw 279 | boolean isOverdrawY = false; 280 | boolean isOverdrawEndPoint = false; 281 | boolean skipDraw = false; 282 | 283 | if (x > graphWidth) { // end right 284 | double b = ((graphWidth - lastEndX) * (y - lastEndY) / (x - lastEndX)); 285 | y = lastEndY + b; 286 | x = graphWidth; 287 | isOverdrawEndPoint = true; 288 | } 289 | if (y < 0) { // end bottom 290 | // skip when previous and this point is out of bound 291 | if (lastEndY < 0) { 292 | skipDraw = true; 293 | } else { 294 | double b = ((0 - lastEndY) * (x - lastEndX) / (y - lastEndY)); 295 | x = lastEndX + b; 296 | } 297 | y = 0; 298 | isOverdrawY = isOverdrawEndPoint = true; 299 | } 300 | if (y > graphHeight) { // end top 301 | // skip when previous and this point is out of bound 302 | if (lastEndY > graphHeight) { 303 | skipDraw = true; 304 | } else { 305 | double b = ((graphHeight - lastEndY) * (x - lastEndX) / (y - lastEndY)); 306 | x = lastEndX + b; 307 | } 308 | y = graphHeight; 309 | isOverdrawY = isOverdrawEndPoint = true; 310 | } 311 | if (lastEndX < 0) { // start left 312 | double b = ((0 - x) * (y - lastEndY) / (lastEndX - x)); 313 | lastEndY = y - b; 314 | lastEndX = 0; 315 | } 316 | 317 | // we need to save the X before it will be corrected when overdraw y 318 | float orgStartX = (float) lastEndX + (graphLeft + 1); 319 | 320 | if (lastEndY < 0) { // start bottom 321 | if (!skipDraw) { 322 | double b = ((0 - y) * (x - lastEndX) / (lastEndY - y)); 323 | lastEndX = x - b; 324 | } 325 | lastEndY = 0; 326 | isOverdrawY = true; 327 | } 328 | if (lastEndY > graphHeight) { // start top 329 | // skip when previous and this point is out of bound 330 | if (!skipDraw) { 331 | double b = ((graphHeight - y) * (x - lastEndX) / (lastEndY - y)); 332 | lastEndX = x - b; 333 | } 334 | lastEndY = graphHeight; 335 | isOverdrawY = true; 336 | } 337 | 338 | float startX = (float) lastEndX + (graphLeft + 1); 339 | float startY = (float) (graphTop - lastEndY) + graphHeight; 340 | float endX = (float) x + (graphLeft + 1); 341 | float endY = (float) (graphTop - y) + graphHeight; 342 | float startXAnimated = startX; 343 | float endXAnimated = endX; 344 | 345 | if (endX < startX) { 346 | // dont draw from right to left 347 | skipDraw = true; 348 | } 349 | 350 | // NaN can happen when previous and current value is out of y bounds 351 | if (!skipDraw && !Float.isNaN(startY) && !Float.isNaN(endY)) { 352 | // animation 353 | if (mAnimated) { 354 | if ((Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { 355 | long currentTime = System.currentTimeMillis(); 356 | if (mAnimationStart == 0) { 357 | // start animation 358 | mAnimationStart = currentTime; 359 | mAnimationStartFrameNo = 0; 360 | } else { 361 | // anti-lag: wait a few frames 362 | if (mAnimationStartFrameNo < 15) { 363 | // second time 364 | mAnimationStart = currentTime; 365 | mAnimationStartFrameNo++; 366 | } 367 | } 368 | float timeFactor = (float) (currentTime - mAnimationStart) / ANIMATION_DURATION; 369 | float factor = mAnimationInterpolator.getInterpolation(timeFactor); 370 | if (timeFactor <= 1.0) { 371 | startXAnimated = (startX - lastAnimationReferenceX) * factor + lastAnimationReferenceX; 372 | startXAnimated = Math.max(startXAnimated, lastAnimationReferenceX); 373 | endXAnimated = (endX - lastAnimationReferenceX) * factor + lastAnimationReferenceX; 374 | ViewCompat.postInvalidateOnAnimation(graphView); 375 | } else { 376 | // animation finished 377 | mLastAnimatedValue = valueX; 378 | } 379 | } else { 380 | lastAnimationReferenceX = endX; 381 | } 382 | } 383 | 384 | // draw data point 385 | if (!isOverdrawEndPoint) { 386 | if (mStyles.drawDataPoints) { 387 | // draw first datapoint 388 | Paint.Style prevStyle = paint.getStyle(); 389 | paint.setStyle(Paint.Style.FILL); 390 | canvas.drawCircle(endXAnimated, endY, mStyles.dataPointsRadius, paint); 391 | paint.setStyle(prevStyle); 392 | } 393 | registerDataPoint(endX, endY, value); 394 | } 395 | 396 | if (mDrawAsPath) { 397 | mPath.moveTo(startXAnimated, startY); 398 | } 399 | // performance opt. 400 | if (Float.isNaN(lastRenderedX) || Math.abs(endX - lastRenderedX) > .3f) { 401 | if (mDrawAsPath) { 402 | mPath.lineTo(endXAnimated, endY); 403 | } else { 404 | // draw vertical lines that were skipped 405 | if (sameXSkip) { 406 | sameXSkip = false; 407 | renderLine(canvas, new float[]{lastRenderedX, minYOnSameX, lastRenderedX, maxYOnSameX}, paint); 408 | } 409 | renderLine(canvas, new float[]{startXAnimated, startY, endXAnimated, endY}, paint); 410 | } 411 | lastRenderedX = endX; 412 | } else { 413 | // rendering on same x position 414 | // save min+max y position and draw it as line 415 | if (sameXSkip) { 416 | minYOnSameX = Math.min(minYOnSameX, endY); 417 | maxYOnSameX = Math.max(maxYOnSameX, endY); 418 | } else { 419 | // first 420 | sameXSkip = true; 421 | minYOnSameX = Math.min(startY, endY); 422 | maxYOnSameX = Math.max(startY, endY); 423 | } 424 | } 425 | 426 | } 427 | 428 | if (mStyles.drawBackground) { 429 | if (isOverdrawY) { 430 | // start draw original x 431 | if (firstX == -1) { 432 | firstX = orgStartX; 433 | firstY = startY; 434 | mPathBackground.moveTo(orgStartX, startY); 435 | } 436 | // from original start to new start 437 | mPathBackground.lineTo(startXAnimated, startY); 438 | } 439 | if (firstX == -1) { 440 | firstX = startXAnimated; 441 | firstY = startY; 442 | mPathBackground.moveTo(startXAnimated, startY); 443 | } 444 | mPathBackground.lineTo(startXAnimated, startY); 445 | mPathBackground.lineTo(endXAnimated, endY); 446 | } 447 | 448 | lastUsedEndX = endXAnimated; 449 | lastUsedEndY = endY; 450 | } else if (mStyles.drawDataPoints) { 451 | //fix: last value not drawn as datapoint. Draw first point here, and then on every step the end values (above) 452 | float first_X = (float) x + (graphLeft + 1); 453 | float first_Y = (float) (graphTop - y) + graphHeight; 454 | 455 | if (first_X >= graphLeft && first_Y <= (graphTop + graphHeight)) { 456 | if (mAnimated && (Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { 457 | long currentTime = System.currentTimeMillis(); 458 | if (mAnimationStart == 0) { 459 | // start animation 460 | mAnimationStart = currentTime; 461 | } 462 | float timeFactor = (float) (currentTime - mAnimationStart) / ANIMATION_DURATION; 463 | float factor = mAnimationInterpolator.getInterpolation(timeFactor); 464 | if (timeFactor <= 1.0) { 465 | first_X = (first_X - lastAnimationReferenceX) * factor + lastAnimationReferenceX; 466 | ViewCompat.postInvalidateOnAnimation(graphView); 467 | } else { 468 | // animation finished 469 | mLastAnimatedValue = valueX; 470 | } 471 | } 472 | 473 | 474 | Paint.Style prevStyle = paint.getStyle(); 475 | paint.setStyle(Paint.Style.FILL); 476 | canvas.drawCircle(first_X, first_Y, mStyles.dataPointsRadius, paint); 477 | paint.setStyle(prevStyle); 478 | registerDataPoint(first_X, first_Y, value); 479 | } 480 | } 481 | lastEndY = orgY; 482 | lastEndX = orgX; 483 | i++; 484 | } 485 | 486 | if (mDrawAsPath) { 487 | // draw at the end 488 | canvas.drawPath(mPath, paint); 489 | } 490 | 491 | if (mStyles.drawBackground && firstX != -1) { 492 | // end / close path 493 | if (lastUsedEndY != graphHeight + graphTop) { 494 | // dont draw line to same point, otherwise the path is completely broken 495 | mPathBackground.lineTo((float) lastUsedEndX, graphHeight + graphTop); 496 | } 497 | mPathBackground.lineTo(firstX, graphHeight + graphTop); 498 | if (firstY != graphHeight + graphTop) { 499 | // dont draw line to same point, otherwise the path is completely broken 500 | mPathBackground.lineTo(firstX, firstY); 501 | } 502 | //mPathBackground.close(); 503 | canvas.drawPath(mPathBackground, mPaintBackground); 504 | } 505 | } 506 | 507 | /** 508 | * just a wrapper to draw lines on canvas 509 | * 510 | * @param canvas 511 | * @param pts 512 | * @param paint 513 | */ 514 | private void renderLine(Canvas canvas, float[] pts, Paint paint) { 515 | if (pts.length == 4 && pts[0] == pts[2] && pts[1] == pts[3]) { 516 | // avoid zero length lines, to makes troubles on some devices 517 | // see https://github.com/appsthatmatter/GraphView/issues/499 518 | return; 519 | } 520 | canvas.drawLines(pts, paint); 521 | } 522 | 523 | /** 524 | * the thickness of the line. 525 | * This option will be ignored if you are 526 | * using a custom paint via {@link #setCustomPaint(android.graphics.Paint)} 527 | * 528 | * @return the thickness of the line 529 | */ 530 | public int getThickness() { 531 | return mStyles.thickness; 532 | } 533 | 534 | /** 535 | * the thickness of the line. 536 | * This option will be ignored if you are 537 | * using a custom paint via {@link #setCustomPaint(android.graphics.Paint)} 538 | * 539 | * @param thickness thickness of the line 540 | */ 541 | public void setThickness(int thickness) { 542 | mStyles.thickness = thickness; 543 | } 544 | 545 | /** 546 | * flag whether the area under the line to the bottom 547 | * of the viewport will be filled with a 548 | * specific background color. 549 | * 550 | * @return whether the background will be drawn 551 | * @see #getBackgroundColor() 552 | */ 553 | public boolean isDrawBackground() { 554 | return mStyles.drawBackground; 555 | } 556 | 557 | /** 558 | * flag whether the area under the line to the bottom 559 | * of the viewport will be filled with a 560 | * specific background color. 561 | * 562 | * @param drawBackground whether the background will be drawn 563 | * @see #setBackgroundColor(int) 564 | */ 565 | public void setDrawBackground(boolean drawBackground) { 566 | mStyles.drawBackground = drawBackground; 567 | } 568 | 569 | /** 570 | * flag whether the data points are highlighted as 571 | * a visible point. 572 | * 573 | * @return flag whether the data points are highlighted 574 | * @see #setDataPointsRadius(float) 575 | */ 576 | public boolean isDrawDataPoints() { 577 | return mStyles.drawDataPoints; 578 | } 579 | 580 | /** 581 | * flag whether the data points are highlighted as 582 | * a visible point. 583 | * 584 | * @param drawDataPoints flag whether the data points are highlighted 585 | * @see #setDataPointsRadius(float) 586 | */ 587 | public void setDrawDataPoints(boolean drawDataPoints) { 588 | mStyles.drawDataPoints = drawDataPoints; 589 | } 590 | 591 | /** 592 | * @return the radius for the data points. 593 | * @see #setDrawDataPoints(boolean) 594 | */ 595 | public float getDataPointsRadius() { 596 | return mStyles.dataPointsRadius; 597 | } 598 | 599 | /** 600 | * @param dataPointsRadius the radius for the data points. 601 | * @see #setDrawDataPoints(boolean) 602 | */ 603 | public void setDataPointsRadius(float dataPointsRadius) { 604 | mStyles.dataPointsRadius = dataPointsRadius; 605 | } 606 | 607 | /** 608 | * @return the background color for the filling under 609 | * the line. 610 | * @see #setDrawBackground(boolean) 611 | */ 612 | public int getBackgroundColor() { 613 | return mStyles.backgroundColor; 614 | } 615 | 616 | /** 617 | * @param backgroundColor the background color for the filling under 618 | * the line. 619 | * @see #setDrawBackground(boolean) 620 | */ 621 | public void setBackgroundColor(int backgroundColor) { 622 | mStyles.backgroundColor = backgroundColor; 623 | } 624 | 625 | /** 626 | * custom paint that can be used. 627 | * this will ignore the thickness and color styles. 628 | * 629 | * @param customPaint the custom paint to be used for rendering the line 630 | */ 631 | public void setCustomPaint(Paint customPaint) { 632 | this.mCustomPaint = customPaint; 633 | } 634 | 635 | /** 636 | * @param animated activate the animated rendering 637 | */ 638 | public void setAnimated(boolean animated) { 639 | this.mAnimated = animated; 640 | } 641 | 642 | /** 643 | * flag whether the line should be drawn as a path 644 | * or with single drawLine commands (more performance) 645 | * By default we use drawLine because it has much more peformance. 646 | * For some styling reasons it can make sense to draw as path. 647 | */ 648 | public boolean isDrawAsPath() { 649 | return mDrawAsPath; 650 | } 651 | 652 | /** 653 | * flag whether the line should be drawn as a path 654 | * or with single drawLine commands (more performance) 655 | * By default we use drawLine because it has much more peformance. 656 | * For some styling reasons it can make sense to draw as path. 657 | * 658 | * @param mDrawAsPath true to draw as path 659 | */ 660 | public void setDrawAsPath(boolean mDrawAsPath) { 661 | this.mDrawAsPath = mDrawAsPath; 662 | } 663 | 664 | /** 665 | * 666 | * @param dataPoint values the values must be in the correct order! 667 | * x-value has to be ASC. First the lowest x value and at least the highest x value. 668 | * @param scrollToEnd true => graphview will scroll to the end (maxX) 669 | * @param maxDataPoints if max data count is reached, the oldest data 670 | * value will be lost to avoid memory leaks 671 | * @param silent set true to avoid rerender the graph 672 | */ 673 | public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, boolean silent) { 674 | if (!isAnimationActive()) { 675 | mAnimationStart = 0; 676 | } 677 | super.appendData(dataPoint, scrollToEnd, maxDataPoints, silent); 678 | } 679 | 680 | /** 681 | * @return currently animation is active 682 | */ 683 | private boolean isAnimationActive() { 684 | if (mAnimated) { 685 | long curr = System.currentTimeMillis(); 686 | return curr - mAnimationStart <= ANIMATION_DURATION; 687 | } 688 | return false; 689 | } 690 | 691 | @Override 692 | public void drawSelection(GraphView graphView, Canvas canvas, boolean b, DataPointInterface value) { 693 | double spanX = graphView.getViewport().getMaxX(false) - graphView.getViewport().getMinX(false); 694 | double spanXPixel = graphView.getGraphContentWidth(); 695 | 696 | double spanY = graphView.getViewport().getMaxY(false) - graphView.getViewport().getMinY(false); 697 | double spanYPixel = graphView.getGraphContentHeight(); 698 | 699 | double pointX = (value.getX() - graphView.getViewport().getMinX(false)) * spanXPixel / spanX; 700 | pointX += graphView.getGraphContentLeft(); 701 | 702 | double pointY = (value.getY() - graphView.getViewport().getMinY(false)) * spanYPixel / spanY; 703 | pointY = graphView.getGraphContentTop() + spanYPixel - pointY; 704 | 705 | // border 706 | canvas.drawCircle((float) pointX, (float) pointY, 30f, mSelectionPaint); 707 | 708 | // fill 709 | Paint.Style prevStyle = mPaint.getStyle(); 710 | mPaint.setStyle(Paint.Style.FILL); 711 | canvas.drawCircle((float) pointX, (float) pointY, 23f, mPaint); 712 | mPaint.setStyle(prevStyle); 713 | } 714 | } 715 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.series; 18 | 19 | /** 20 | * Listener for the tap event which will be 21 | * triggered when the user touches on a datapoint. 22 | * 23 | * Use this in {@link com.jjoe64.graphview.series.BaseSeries#setOnDataPointTapListener(OnDataPointTapListener)} 24 | * 25 | * @author jjoe64 26 | */ 27 | public interface OnDataPointTapListener { 28 | /** 29 | * gets called when the user touches on a datapoint. 30 | * 31 | * @param series the corresponding series 32 | * @param dataPoint the data point that was tapped on 33 | */ 34 | void onTap(Series series, DataPointInterface dataPoint); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.series; 18 | 19 | import android.graphics.Canvas; 20 | import android.graphics.Color; 21 | import android.graphics.Paint; 22 | import android.graphics.Path; 23 | import android.graphics.Point; 24 | 25 | import com.jjoe64.graphview.GraphView; 26 | 27 | import java.util.Iterator; 28 | 29 | /** 30 | * Series that plots the data as points. 31 | * The points can be different shapes or a 32 | * complete custom drawing. 33 | * 34 | * @author jjoe64 35 | */ 36 | public class PointsGraphSeries extends BaseSeries { 37 | /** 38 | * interface to implement a custom 39 | * drawing for the data points. 40 | */ 41 | public static interface CustomShape { 42 | /** 43 | * called when drawing a single data point. 44 | * use the x and y coordinates to draw your 45 | * drawing at this point. 46 | * 47 | * @param canvas canvas to draw on 48 | * @param paint internal paint object. this has the correct color. 49 | * But you can use your own paint. 50 | * @param x x-coordinate the point has to be drawn to 51 | * @param y y-coordinate the point has to be drawn to 52 | * @param dataPoint the related data point 53 | */ 54 | void draw(Canvas canvas, Paint paint, float x, float y, DataPointInterface dataPoint); 55 | } 56 | 57 | /** 58 | * choose a predefined shape to draw for 59 | * each data point. 60 | * You can also draw a custom drawing via {@link com.jjoe64.graphview.series.PointsGraphSeries.CustomShape} 61 | */ 62 | public enum Shape { 63 | /** 64 | * draws a point / circle 65 | */ 66 | POINT, 67 | 68 | /** 69 | * draws a triangle 70 | */ 71 | TRIANGLE, 72 | 73 | /** 74 | * draws a rectangle 75 | */ 76 | RECTANGLE 77 | } 78 | 79 | /** 80 | * wrapped styles for this series 81 | */ 82 | private final class Styles { 83 | /** 84 | * this is used for the size of the shape that 85 | * will be drawn. 86 | * This is useless if you are using a custom shape. 87 | */ 88 | float size; 89 | 90 | /** 91 | * the shape that will be drawn for each point. 92 | */ 93 | Shape shape; 94 | } 95 | 96 | /** 97 | * wrapped styles 98 | */ 99 | private Styles mStyles; 100 | 101 | /** 102 | * internal paint object 103 | */ 104 | private Paint mPaint; 105 | 106 | /** 107 | * handler to use a custom drawing 108 | */ 109 | private CustomShape mCustomShape; 110 | 111 | /** 112 | * creates the series without data 113 | */ 114 | public PointsGraphSeries() { 115 | init(); 116 | } 117 | 118 | /** 119 | * creates the series with data 120 | * 121 | * @param data datapoints 122 | */ 123 | public PointsGraphSeries(E[] data) { 124 | super(data); 125 | init(); 126 | } 127 | 128 | /** 129 | * inits the internal objects 130 | * set the defaults 131 | */ 132 | protected void init() { 133 | mStyles = new Styles(); 134 | mStyles.size = 20f; 135 | mPaint = new Paint(); 136 | mPaint.setStrokeCap(Paint.Cap.ROUND); 137 | setShape(Shape.POINT); 138 | } 139 | 140 | /** 141 | * plot the data to the viewport 142 | * 143 | * @param graphView graphview 144 | * @param canvas canvas to draw on 145 | * @param isSecondScale whether it is the second scale 146 | */ 147 | @Override 148 | public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { 149 | resetDataPoints(); 150 | 151 | // get data 152 | double maxX = graphView.getViewport().getMaxX(false); 153 | double minX = graphView.getViewport().getMinX(false); 154 | 155 | double maxY; 156 | double minY; 157 | if (isSecondScale) { 158 | maxY = graphView.getSecondScale().getMaxY(false); 159 | minY = graphView.getSecondScale().getMinY(false); 160 | } else { 161 | maxY = graphView.getViewport().getMaxY(false); 162 | minY = graphView.getViewport().getMinY(false); 163 | } 164 | 165 | Iterator values = getValues(minX, maxX); 166 | 167 | // draw background 168 | double lastEndY = 0; 169 | double lastEndX = 0; 170 | 171 | // draw data 172 | mPaint.setColor(getColor()); 173 | 174 | double diffY = maxY - minY; 175 | double diffX = maxX - minX; 176 | 177 | float graphHeight = graphView.getGraphContentHeight(); 178 | float graphWidth = graphView.getGraphContentWidth(); 179 | float graphLeft = graphView.getGraphContentLeft(); 180 | float graphTop = graphView.getGraphContentTop(); 181 | 182 | lastEndY = 0; 183 | lastEndX = 0; 184 | float firstX = 0; 185 | int i=0; 186 | while (values.hasNext()) { 187 | E value = values.next(); 188 | 189 | double valY = value.getY() - minY; 190 | double ratY = valY / diffY; 191 | double y = graphHeight * ratY; 192 | 193 | double valX = value.getX() - minX; 194 | double ratX = valX / diffX; 195 | double x = graphWidth * ratX; 196 | 197 | double orgX = x; 198 | double orgY = y; 199 | 200 | // overdraw 201 | boolean overdraw = false; 202 | if (x > graphWidth) { // end right 203 | overdraw = true; 204 | } 205 | if (y < 0) { // end bottom 206 | overdraw = true; 207 | } 208 | if (y > graphHeight) { // end top 209 | overdraw = true; 210 | } 211 | /* Fix a bug that continue to show the DOT after Y axis */ 212 | if(x < 0) { 213 | overdraw = true; 214 | } 215 | 216 | float endX = (float) x + (graphLeft + 1); 217 | float endY = (float) (graphTop - y) + graphHeight; 218 | registerDataPoint(endX, endY, value); 219 | 220 | // draw data point 221 | if (!overdraw) { 222 | if (mCustomShape != null) { 223 | mCustomShape.draw(canvas, mPaint, endX, endY, value); 224 | } else if (mStyles.shape == Shape.POINT) { 225 | canvas.drawCircle(endX, endY, mStyles.size, mPaint); 226 | } else if (mStyles.shape == Shape.RECTANGLE) { 227 | canvas.drawRect(endX-mStyles.size, endY-mStyles.size, endX+mStyles.size, endY+mStyles.size, mPaint); 228 | } else if (mStyles.shape == Shape.TRIANGLE) { 229 | Point[] points = new Point[3]; 230 | points[0] = new Point((int)endX, (int)(endY-getSize())); 231 | points[1] = new Point((int)(endX+getSize()), (int)(endY+getSize()*0.67)); 232 | points[2] = new Point((int)(endX-getSize()), (int)(endY+getSize()*0.67)); 233 | drawArrows(points, canvas, mPaint); 234 | } 235 | } 236 | 237 | i++; 238 | } 239 | 240 | } 241 | 242 | /** 243 | * helper to draw triangle 244 | * 245 | * @param point array with 3 coordinates 246 | * @param canvas canvas to draw on 247 | * @param paint paint object 248 | */ 249 | private void drawArrows(Point[] point, Canvas canvas, Paint paint) { 250 | float [] points = new float[8]; 251 | points[0] = point[0].x; 252 | points[1] = point[0].y; 253 | points[2] = point[1].x; 254 | points[3] = point[1].y; 255 | points[4] = point[2].x; 256 | points[5] = point[2].y; 257 | points[6] = point[0].x; 258 | points[7] = point[0].y; 259 | 260 | canvas.drawVertices(Canvas.VertexMode.TRIANGLES, 8, points, 0, null, 0, null, 0, null, 0, 0, paint); 261 | Path path = new Path(); 262 | path.moveTo(point[0].x , point[0].y); 263 | path.lineTo(point[1].x,point[1].y); 264 | path.lineTo(point[2].x,point[2].y); 265 | canvas.drawPath(path,paint); 266 | } 267 | 268 | /** 269 | * This is used for the size of the shape that 270 | * will be drawn. 271 | * This is useless if you are using a custom shape. 272 | * 273 | * @return the size of the shape 274 | */ 275 | public float getSize() { 276 | return mStyles.size; 277 | } 278 | 279 | /** 280 | * This is used for the size of the shape that 281 | * will be drawn. 282 | * This is useless if you are using a custom shape. 283 | * 284 | * @param radius the size of the shape 285 | */ 286 | public void setSize(float radius) { 287 | mStyles.size = radius; 288 | } 289 | 290 | /** 291 | * @return the shape that will be drawn for each point 292 | */ 293 | public Shape getShape() { 294 | return mStyles.shape; 295 | } 296 | 297 | /** 298 | * @param s the shape that will be drawn for each point 299 | */ 300 | public void setShape(Shape s) { 301 | mStyles.shape = s; 302 | } 303 | 304 | /** 305 | * Use a custom handler to draw your own 306 | * drawing for each data point. 307 | * 308 | * @param shape handler to use a custom drawing 309 | */ 310 | public void setCustomShape(CustomShape shape) { 311 | mCustomShape = shape; 312 | } 313 | 314 | @Override 315 | public void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value) { 316 | // TODO 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/main/java/com/jjoe64/graphview/series/Series.java: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphView 3 | * Copyright 2016 Jonas Gehring 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.jjoe64.graphview.series; 18 | 19 | import android.graphics.Canvas; 20 | 21 | import com.jjoe64.graphview.GraphView; 22 | 23 | import java.util.Iterator; 24 | 25 | /** 26 | * Basis interface for series that can be plotted 27 | * on the graph. 28 | * You can implement this in order to create a completely 29 | * custom series type. 30 | * But it is recommended to extend {@link com.jjoe64.graphview.series.BaseSeries} or another 31 | * implemented Series class to save time. 32 | * Anyway this interface can make sense if you want to implement 33 | * a custom data provider, because BaseSeries uses a internal Array to store 34 | * the data. 35 | * 36 | * @author jjoe64 37 | */ 38 | public interface Series { 39 | /** 40 | * @return the lowest x-value of the data 41 | */ 42 | public double getLowestValueX(); 43 | 44 | /** 45 | * @return the highest x-value of the data 46 | */ 47 | public double getHighestValueX(); 48 | 49 | /** 50 | * @return the lowest y-value of the data 51 | */ 52 | public double getLowestValueY(); 53 | 54 | /** 55 | * @return the highest y-value of the data 56 | */ 57 | public double getHighestValueY(); 58 | 59 | /** 60 | * get the values for a specific range. It is 61 | * important that the data comes in the sorted order 62 | * (from lowest to highest x-value). 63 | * 64 | * @param from the minimal x-value 65 | * @param until the maximal x-value 66 | * @return all datapoints between the from and until x-value 67 | * including the from and until data points. 68 | */ 69 | public Iterator getValues(double from, double until); 70 | 71 | /** 72 | * Plots the series to the viewport. 73 | * You have to care about overdrawing. 74 | * This method may be called 2 times: one for 75 | * the default scale and one time for the 76 | * second scale. 77 | * 78 | * @param graphView corresponding graphview 79 | * @param canvas canvas to draw on 80 | * @param isSecondScale true if the drawing is for the second scale 81 | */ 82 | public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale); 83 | 84 | /** 85 | * @return the title of the series. Used in the legend 86 | */ 87 | public String getTitle(); 88 | 89 | /** 90 | * @return the color of the series. Used in the legend and should 91 | * be used for the plotted points or lines. 92 | */ 93 | public int getColor(); 94 | 95 | /** 96 | * set a listener for tap on a data point. 97 | * 98 | * @param l listener 99 | */ 100 | public void setOnDataPointTapListener(OnDataPointTapListener l); 101 | 102 | /** 103 | * called by the tap detector in order to trigger 104 | * the on tap on datapoint event. 105 | * 106 | * @param x pixel 107 | * @param y pixel 108 | */ 109 | void onTap(float x, float y); 110 | 111 | /** 112 | * called when the series was added to a graph 113 | * 114 | * @param graphView graphview 115 | */ 116 | void onGraphViewAttached(GraphView graphView); 117 | 118 | /** 119 | * @return whether there are data points 120 | */ 121 | boolean isEmpty(); 122 | 123 | /** 124 | * clear reference to view and activity 125 | * 126 | * @param graphView 127 | */ 128 | void clearReference(GraphView graphView); 129 | } 130 | -------------------------------------------------------------------------------- /src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /zooming.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjoe64/GraphView/047769ac3fbf06055fef3ab49ae515a0ec8b5322/zooming.gif --------------------------------------------------------------------------------