diff --git a/GVBar.png b/GVBar.png deleted file mode 100644 index 7eabad12c..000000000 Binary files a/GVBar.png and /dev/null differ diff --git a/GVLine.jpg b/GVLine.jpg deleted file mode 100644 index cb93a390a..000000000 Binary files a/GVLine.jpg and /dev/null differ diff --git a/README.markdown b/README.markdown index 58f473201..7732c6a8b 100644 --- a/README.markdown +++ b/README.markdown @@ -1,89 +1,86 @@ -Chart and Graph Library for Android -==================================== - -

What is GraphView

-GraphView is a library for Android to programmatically create flexible and nice-looking diagramms. It is easy to understand, to integrate and to customize it. -At the moment there are two different types: - - -Tested on Android 1.6, 2.2, 2.3 and 3.0 (honeycomb, tablet), 4.0. - - - - - -

Features

- -* Two chart types -Line Chart and Bar Chart. +# Chart and Graph Library for Android + +## 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 + +## What is GraphView + +GraphView is a library for Android to programmatically create +flexible and nice-looking diagrams. +It is **easy** to understand, to integrate and to customize. + +Supported graph types: +* Line Graphs +* Bar Graphs +* Point Graphs +* or implement your own custom types. + + + + + + + +## Top Features + +* Line Chart, Bar Chart, Points +* Combination of different graph types +* Scrolling vertical and horizontal +. You can scroll with a finger touch move gesture. +* Scaling / Zooming vertical and horizontal +. With two-fingers touch scale gesture (Multi-touch), the viewport can be changed. +* Realtime Graph (Live change of data) +* Second scale axis * Draw multiple series of data -Let the diagram show more that one series in a graph. You can set a color and a description for every series. +. Let the diagram show more that one series in a graph. You can set a color and a description for every series. * Show legend -A legend can be displayed inline the chart. You can set the width and the vertical align (top, middle, bottom). +. A legend can be displayed inline the chart. You can set the width and the vertical align (top, middle, bottom). * Custom labels -The labels for the x- and y-axis are generated automatically. But you can set your own labels, Strings are possible. +. The labels for the x- and y-axis are generated automatically. But you can set your own labels, Strings are possible. * Handle incomplete data -It's possible to give the data in different frequency. +. It's possible to give the data in different frequency. * Viewport -You can limit the viewport so that only a part of the data will be displayed. -* Scrolling -You can scroll with a finger touch move gesture. -* Scaling / Zooming -Since Android 2.3! With two-fingers touch scale gesture (Multi-touch), the viewport can be changed. -* Background (line graph) -Optionally draws a light background under the diagram stroke. +. You can limit the viewport so that only a part of the data will be displayed. * Manual Y axis limits -* Realtime Graph (Live) -* And more - -

How to use

-View GraphView page
http://android-graphview.org
- -

Important

-To show you how to integrate the library into an existing project see the GraphView-Demos project! -See GraphView-Demos for examples. -https://github.com/jjoe64/GraphView-Demos
-
View GraphView page http://android-graphview.org +* And much more... Check out the project page and/or the demo app -How to create a new version for maven repo --------------------------------------------- -create sources.jar -- $ jar cvf sources.jar src +## How to use -create java doc jar -- $ mkdir javadoc -- $ javadoc -d javadoc -sourcepath src/main/java/ -subpackages com.jjoe64 -- $ jar cvf javadoc.jar javadoc +1) Add gradle dependency: +``` +implementation 'com.jjoe64:graphview:4.2.2' +``` -change version in gradle.properties +2) Add view to layout: +```xml + +``` -uncomment part for publishing in build.gradle +3) Add some data: +```java +GraphView graph = (GraphView) findViewById(R.id.graph); +LineGraphSeries series = new LineGraphSeries(new DataPoint[] { + new DataPoint(0, 1), + new DataPoint(1, 5), + new DataPoint(2, 3), + new DataPoint(3, 2), + new DataPoint(4, 6) +}); +graph.addSeries(series); +``` -run gradle task uploadArchives -- ./gradlew --rerun-tasks uploadArchives +## Download Demo project at Google Play Store -open https://oss.sonatype.org +
+Showcase GraphView Demo App -login +## More examples and documentation -Staging Repositiories - -search: jjoe64 - -Close entry - -Refresh/Wait - -Release entry - -Wait some days - -How to create a new .jar file --------------------------------- -run this gradle task -- ./gradlew --rerun-tasks clearJar makeJar -copy myCompiledLibrary.jar from build/libs/ to public/GraphView-x.x.x.jar +Get started at project wiki homepage +To show you how to integrate the library into an existing project see the GraphView-Demos project! +See GraphView-Demos for examples. +https://github.com/jjoe64/GraphView-Demos
+
View GraphView wiki page https://github.com/jjoe64/GraphView/wiki diff --git a/README.new-version.md b/README.new-version.md new file mode 100644 index 000000000..8bd815f25 --- /dev/null +++ b/README.new-version.md @@ -0,0 +1,59 @@ +How to create a new version for maven repo +-------------------------------------------- +create sources.jar +- $ jar cvf sources.jar src + +create java doc jar +- $ mkdir javadoc +- $ javadoc -d javadoc -sourcepath src/main/java/ -subpackages com.jjoe64 +- $ jar cvf javadoc.jar javadoc + +change version in gradle.properties + +uncomment part for publishing in build.gradle + +(once) create a gpg file +- gpg --gen-key + +(once) publish key +- gpg --send-keys D8C3B041 +and/or here as ascii +- gpg --export -a D8C3B041 +- http://keyserver.ubuntu.com:11371/ + +=> needs some time + +hardcode gpg key password in maven_push.gradle + +hardcode user/pwd of nexus account in maven_push.gradle + +success gradle task uploadArchives +- ./gradlew --rerun-tasks uploadArchives +- enter gpg info (id:D8C3B041 / path: /Users/jonas/.gnupg/secring.gpg / PWD) + +open https://oss.sonatype.org + +login + +Staging Repositiories + +search: jjoe64 + +Close entry + +Refresh/Wait + +Release entry + +Wait some days + +## update java doc + +$ javadoc -d javadoc -sourcepath src/main/java/ -subpackages com.jjoe64 +$ mv javadoc/ .. +$ git checkout gh-pages +$ rm -rf javadoc +$ mv ../javadoc/ . +$ git add javadoc +$ git commit -a + diff --git a/anim.gif b/anim.gif new file mode 100644 index 000000000..43e2d90f7 Binary files /dev/null and b/anim.gif differ diff --git a/build.gradle b/build.gradle index 022154336..ddd7534ac 100644 --- a/build.gradle +++ b/build.gradle @@ -1,25 +1,28 @@ buildscript { repositories { mavenCentral() + google() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.android.tools.build:gradle:3.5.3' } } -task wrapper(type: Wrapper) { - gradleVersion = '2.2.1' +wrapper { + gradleVersion = '5.6' } apply plugin: 'com.android.library' + android { - compileSdkVersion 22 - buildToolsVersion "21.1.2" + compileSdkVersion 27 + buildToolsVersion '28.0.3' defaultConfig { minSdkVersion 9 - targetSdkVersion 22 + targetSdkVersion 27 versionCode 1 versionName "1.0" } @@ -28,28 +31,37 @@ android { minifyEnabled false } } + lintOptions { + abortOnError false + } + } dependencies { - compile 'com.android.support:support-v4:22.1.1' + implementation 'androidx.core:core:1.0.0-beta01' } + //this is used to generate .jar files and push to maven repo -/* // This is the actual solution, as in http://stackoverflow.com/a/19037807/1002054 task clearJar(type: Delete) { - delete 'build/libs/myCompiledLibrary.jar' + delete 'build/outputs/myCompiledLibrary.jar' } task makeJar(type: Copy) { from('build/intermediates/bundles/release/') - into('build/libs/') + into('build/outputs/') include('classes.jar') rename ('classes.jar', 'myCompiledLibrary.jar') } makeJar.dependsOn(clearJar, build) + apply from: './maven_push.gradle' -*/ +repositories { + google() + mavenCentral() + jcenter() +} \ No newline at end of file diff --git a/doc-assets/1059439_1.png b/doc-assets/1059439_1.png new file mode 100644 index 000000000..8be6dde8b Binary files /dev/null and b/doc-assets/1059439_1.png differ diff --git a/doc-assets/4000611_1.png b/doc-assets/4000611_1.png new file mode 100644 index 000000000..a373f0487 Binary files /dev/null and b/doc-assets/4000611_1.png differ diff --git a/doc-assets/469160_orig_1.png b/doc-assets/469160_orig_1.png new file mode 100644 index 000000000..7b96d263a Binary files /dev/null and b/doc-assets/469160_orig_1.png differ diff --git a/doc-assets/5901645_1.png b/doc-assets/5901645_1.png new file mode 100644 index 000000000..962804de9 Binary files /dev/null and b/doc-assets/5901645_1.png differ diff --git a/doc-assets/6316193_orig_1.png b/doc-assets/6316193_orig_1.png new file mode 100644 index 000000000..f4f6e462a Binary files /dev/null and b/doc-assets/6316193_orig_1.png differ diff --git a/doc-assets/9303658_1.png b/doc-assets/9303658_1.png new file mode 100644 index 000000000..bbba36477 Binary files /dev/null and b/doc-assets/9303658_1.png differ diff --git a/doc-assets/Screen_Shot_2016_10_08_at_12_19_56_1.png b/doc-assets/Screen_Shot_2016_10_08_at_12_19_56_1.png new file mode 100644 index 000000000..b5d610643 Binary files /dev/null and b/doc-assets/Screen_Shot_2016_10_08_at_12_19_56_1.png differ diff --git a/doc-assets/Screen_Shot_2016_10_08_at_12_20_56_1.png b/doc-assets/Screen_Shot_2016_10_08_at_12_20_56_1.png new file mode 100644 index 000000000..77780bb31 Binary files /dev/null and b/doc-assets/Screen_Shot_2016_10_08_at_12_20_56_1.png differ diff --git a/doc-assets/Screen_Shot_2016_10_08_at_12_23_38_1.png b/doc-assets/Screen_Shot_2016_10_08_at_12_23_38_1.png new file mode 100644 index 000000000..745fc291c Binary files /dev/null and b/doc-assets/Screen_Shot_2016_10_08_at_12_23_38_1.png differ diff --git a/doc-assets/Screen_Shot_2016_10_08_at_12_24_19_1.png b/doc-assets/Screen_Shot_2016_10_08_at_12_24_19_1.png new file mode 100644 index 000000000..6f4433146 Binary files /dev/null and b/doc-assets/Screen_Shot_2016_10_08_at_12_24_19_1.png differ diff --git a/doc-assets/Screenshot_20161008_122642_1_1.png b/doc-assets/Screenshot_20161008_122642_1_1.png new file mode 100644 index 000000000..8aba95d2f Binary files /dev/null and b/doc-assets/Screenshot_20161008_122642_1_1.png differ diff --git a/doc-assets/Screenshot_20161011_210215_1.png b/doc-assets/Screenshot_20161011_210215_1.png new file mode 100644 index 000000000..0ed502672 Binary files /dev/null and b/doc-assets/Screenshot_20161011_210215_1.png differ diff --git a/doc-assets/Screenshot_20161012_180242_1.png b/doc-assets/Screenshot_20161012_180242_1.png new file mode 100644 index 000000000..acb238f26 Binary files /dev/null and b/doc-assets/Screenshot_20161012_180242_1.png differ diff --git a/doc-assets/Screenshot_20161012_180257_1.png b/doc-assets/Screenshot_20161012_180257_1.png new file mode 100644 index 000000000..dca262fe7 Binary files /dev/null and b/doc-assets/Screenshot_20161012_180257_1.png differ diff --git a/doc-assets/Screenshot_20161012_180325_1.png b/doc-assets/Screenshot_20161012_180325_1.png new file mode 100644 index 000000000..f20b1436c Binary files /dev/null and b/doc-assets/Screenshot_20161012_180325_1.png differ diff --git a/doc-assets/Screenshot_20161012_180336_1.png b/doc-assets/Screenshot_20161012_180336_1.png new file mode 100644 index 000000000..ae45497de Binary files /dev/null and b/doc-assets/Screenshot_20161012_180336_1.png differ diff --git a/doc-assets/Screenshot_20161012_180355_1.png b/doc-assets/Screenshot_20161012_180355_1.png new file mode 100644 index 000000000..f9797826c Binary files /dev/null and b/doc-assets/Screenshot_20161012_180355_1.png differ diff --git a/doc-assets/Screenshot_20161012_180404_1.png b/doc-assets/Screenshot_20161012_180404_1.png new file mode 100644 index 000000000..069978e9d Binary files /dev/null and b/doc-assets/Screenshot_20161012_180404_1.png differ diff --git a/doc-assets/snapshotshare_1.png b/doc-assets/snapshotshare_1.png new file mode 100644 index 000000000..82901109b Binary files /dev/null and b/doc-assets/snapshotshare_1.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 000000000..42d355a14 Binary files /dev/null and b/favicon.ico differ diff --git a/gradle.properties b/gradle.properties index 5cb7f87ea..86a27f74c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -VERSION_NAME=4.0.1 -VERSION_CODE=12 +VERSION_NAME=4.2.2 +VERSION_CODE=17 GROUP=com.jjoe64 POM_DESCRIPTION=Android Graph Library for creating zoomable and scrollable charts. @@ -7,8 +7,8 @@ POM_URL=http://android-graphview.org/ POM_SCM_URL=https://github.com/jjoe64/GraphView POM_SCM_CONNECTION=scm:git@github.com:jjoe64/GraphView.git POM_SCM_DEV_CONNECTION=scm:git@github.com:jjoe64/GraphView.git -POM_LICENCE_NAME=GNU GENERAL PUBLIC LICENSE including GPL linking exception -POM_LICENCE_URL=https://github.com/jjoe64/GraphView/blob/master/license.txtl +POM_LICENCE_NAME=Apache License, Version 2.0 +POM_LICENCE_URL=https://github.com/jjoe64/GraphView/blob/master/license.txt POM_LICENCE_DIST=repo POM_DEVELOPER_ID=jjoe64 POM_DEVELOPER_NAME=Jonas Gehring diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c97a8bdb9..01b8bf6b1 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 636eed90f..25f587d12 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Apr 27 20:07:18 CEST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-all.zip diff --git a/gradlew b/gradlew index 91a7e269e..cccdd3d51 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,20 +6,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730b..e95643d6a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/license.txt b/license.txt index a6638b88f..f92a0dc19 100644 --- a/license.txt +++ b/license.txt @@ -1,355 +1,68 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 +Copyright 2016 Jonas Gehring - including "GPL linking exception" +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. + http://www.apache.org/licenses/LICENSE-2.0 - Preamble +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. +Apache License - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. +Version 2.0, January 2004 - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. +http://www.apache.org/licenses/ - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. +1. Definitions. - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - The precise terms and conditions for copying, distribution and -modification follow. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION +"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. - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. +"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. -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. +"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). - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: +"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. - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. +"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." - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. +"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. - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) +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. -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. +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. -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. +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: -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +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 +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. - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: +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. +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. - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, +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. - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, +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. - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) +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. -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. +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. -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - 11. Linking this library statically or dynamically with other modules -is making a combined work based on this library. Thus, the terms and -conditions of the GNU General Public License cover the whole combination. -As a special exception, the copyright holders of this library give you -permission to link this library with independent modules to produce an -executable, regardless of the license terms of these independent -modules, and to copy and distribute the resulting executable under terms -of your choice, provided that you also meet, for each linked independent -module, the terms and conditions of the license of that module. An -independent module is a module which is not derived from or based on this -library. If you modify this library, you may extend this exception to your -version of the library, but you are not obliged to do so. If you do not -wish to do so, delete this exception statement from your version. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. +END OF TERMS AND CONDITIONS diff --git a/maven_push.gradle b/maven_push.gradle index b741e61b1..3d15ef167 100644 --- a/maven_push.gradle +++ b/maven_push.gradle @@ -1,6 +1,28 @@ apply plugin: 'maven' apply plugin: 'signing' +import org.gradle.plugins.signing.Sign + +gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.allTasks.any { it instanceof Sign }) { + // Use Java 6's console to read from the console (no good for + // a CI environment) + Console console = System.console() + console.printf "\n\nWe have to sign some things in this build." + + "\n\nPlease enter your signing details.\n\n" + + def id = "D8C3B041" + def file = "/Users/jonas/.gnupg/secring.gpg" + def password = "" + + allprojects { ext."signing.keyId" = id } + allprojects { ext."signing.secretKeyRingFile" = file } + allprojects { ext."signing.password" = password } + + console.printf "\nThanks.\n\n" + file + } +} + def sonatypeRepositoryUrl if (isReleaseBuild()) { println 'RELEASE BUILD' @@ -13,11 +35,15 @@ if (isReleaseBuild()) { } def getRepositoryUsername() { - return hasProperty('nexusUsername') ? nexusUsername : "" + return "" } def getRepositoryPassword() { - return hasProperty('nexusPassword') ? nexusPassword : "" + return "" +} + +def isReleaseBuild() { + return version.contains("SNAPSHOT") == false } afterEvaluate { project -> @@ -26,7 +52,9 @@ afterEvaluate { project -> mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + pom.groupId = GROUP pom.artifactId = POM_ARTIFACT_ID + pom.version = VERSION_NAME repository(url: sonatypeRepositoryUrl) { authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) @@ -38,6 +66,7 @@ afterEvaluate { project -> description POM_DESCRIPTION url POM_URL + scm { url POM_SCM_URL connection POM_SCM_CONNECTION @@ -89,4 +118,4 @@ afterEvaluate { project -> archives androidSourcesJar archives androidJavadocsJar } -} \ No newline at end of file +} diff --git a/public/GraphView-4.1.0.jar b/public/GraphView-4.1.0.jar new file mode 100644 index 000000000..1f6a7f72d Binary files /dev/null and b/public/GraphView-4.1.0.jar differ diff --git a/public/GraphView-4.1.1.jar b/public/GraphView-4.1.1.jar new file mode 100644 index 000000000..b42e0f33a Binary files /dev/null and b/public/GraphView-4.1.1.jar differ diff --git a/public/GraphView-4.2.0.jar b/public/GraphView-4.2.0.jar new file mode 100644 index 000000000..4da2fc27a Binary files /dev/null and b/public/GraphView-4.2.0.jar differ diff --git a/public/GraphView-4.2.1.jar b/public/GraphView-4.2.1.jar new file mode 100644 index 000000000..c45cdd1b8 Binary files /dev/null and b/public/GraphView-4.2.1.jar differ diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 0ab342f13..120f744d6 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -3,8 +3,6 @@ package="com.jjoe64.graphview" android:versionCode="1" android:versionName="1.0"> - - diff --git a/src/main/java/com/jjoe64/graphview/CursorMode.java b/src/main/java/com/jjoe64/graphview/CursorMode.java new file mode 100644 index 000000000..e42369148 --- /dev/null +++ b/src/main/java/com/jjoe64/graphview/CursorMode.java @@ -0,0 +1,254 @@ +package com.jjoe64.graphview; + +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.Log; +import android.util.TypedValue; +import android.view.MotionEvent; + +import com.jjoe64.graphview.series.BaseSeries; +import com.jjoe64.graphview.series.DataPointInterface; +import com.jjoe64.graphview.series.Series; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by jonas on 22/02/2017. + */ + +public class CursorMode { + private final static class Styles { + public float textSize; + public int spacing; + public int padding; + public int width; + public int backgroundColor; + public int margin; + public int textColor; + } + + protected final Paint mPaintLine; + protected final GraphView mGraphView; + protected float mPosX; + protected float mPosY; + protected boolean mCursorVisible; + protected final Map mCurrentSelection; + protected final Paint mRectPaint; + protected final Paint mTextPaint; + protected double mCurrentSelectionX; + protected Styles mStyles; + protected int cachedLegendWidth; + + public CursorMode(GraphView graphView) { + mStyles = new Styles(); + mGraphView = graphView; + mPaintLine = new Paint(); + mPaintLine.setColor(Color.argb(128, 180, 180, 180)); + mPaintLine.setStrokeWidth(10f); + mCurrentSelection = new HashMap<>(); + mRectPaint = new Paint(); + mTextPaint = new Paint(); + resetStyles(); + } + + /** + * resets the styles to the defaults + * and clears the legend width cache + */ + public void resetStyles() { + mStyles.textSize = mGraphView.getGridLabelRenderer().getTextSize(); + mStyles.spacing = (int) (mStyles.textSize / 5); + mStyles.padding = (int) (mStyles.textSize / 2); + mStyles.width = 0; + mStyles.backgroundColor = Color.argb(180, 100, 100, 100); + mStyles.margin = (int) (mStyles.textSize); + + // get matching styles from theme + TypedValue typedValue = new TypedValue(); + mGraphView.getContext().getTheme().resolveAttribute(android.R.attr.textAppearanceSmall, typedValue, true); + + int color1; + + try { + TypedArray array = mGraphView.getContext().obtainStyledAttributes(typedValue.data, new int[]{ + android.R.attr.textColorPrimary}); + color1 = array.getColor(0, Color.BLACK); + array.recycle(); + } catch (Exception e) { + color1 = Color.BLACK; + } + + mStyles.textColor = color1; + + cachedLegendWidth = 0; + } + + + public void onDown(MotionEvent e) { + mPosX = Math.max(e.getX(), mGraphView.getGraphContentLeft()); + mPosX = Math.min(mPosX, mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth()); + mPosY = e.getY(); + mCursorVisible = true; + findCurrentDataPoint(); + mGraphView.invalidate(); + } + + public void onMove(MotionEvent e) { + if (mCursorVisible) { + mPosX = Math.max(e.getX(), mGraphView.getGraphContentLeft()); + mPosX = Math.min(mPosX, mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth()); + mPosY = e.getY(); + findCurrentDataPoint(); + mGraphView.invalidate(); + } + } + + public void draw(Canvas canvas) { + if (mCursorVisible) { + canvas.drawLine(mPosX, 0, mPosX, canvas.getHeight(), mPaintLine); + } + + // selection + for (Map.Entry entry : mCurrentSelection.entrySet()) { + entry.getKey().drawSelection(mGraphView, canvas, false, entry.getValue()); + } + + if (!mCurrentSelection.isEmpty()) { + drawLegend(canvas); + } + } + + protected String getTextForSeries(Series s, DataPointInterface value) { + StringBuffer txt = new StringBuffer(); + if (s.getTitle() != null) { + txt.append(s.getTitle()); + txt.append(": "); + } + txt.append(mGraphView.getGridLabelRenderer().getLabelFormatter().formatLabel(value.getY(), false)); + return txt.toString(); + } + + protected void drawLegend(Canvas canvas) { + mTextPaint.setTextSize(mStyles.textSize); + mTextPaint.setColor(mStyles.textColor); + + int shapeSize = (int) (mStyles.textSize*0.8d); + + // width + int legendWidth = mStyles.width; + if (legendWidth == 0) { + // auto + legendWidth = cachedLegendWidth; + + if (legendWidth == 0) { + Rect textBounds = new Rect(); + for (Map.Entry entry : mCurrentSelection.entrySet()) { + String txt = getTextForSeries(entry.getKey(), entry.getValue()); + mTextPaint.getTextBounds(txt, 0, txt.length(), textBounds); + legendWidth = Math.max(legendWidth, textBounds.width()); + } + if (legendWidth == 0) legendWidth = 1; + + // add shape size + legendWidth += shapeSize+mStyles.padding*2 + mStyles.spacing; + cachedLegendWidth = legendWidth; + } + } + + float legendPosX = mPosX - mStyles.margin - legendWidth; + if (legendPosX < 0) { + legendPosX = 0; + } + + // rect + float legendHeight = (mStyles.textSize+mStyles.spacing) * (mCurrentSelection.size() + 1) -mStyles.spacing; + + float legendPosY = mPosY - legendHeight - 4.5f * mStyles.textSize; + if (legendPosY < 0) { + legendPosY = 0; + } + + float lLeft; + float lTop; + lLeft = legendPosX; + lTop = legendPosY; + + float lRight = lLeft+legendWidth; + float lBottom = lTop+legendHeight+2*mStyles.padding; + mRectPaint.setColor(mStyles.backgroundColor); + canvas.drawRoundRect(new RectF(lLeft, lTop, lRight, lBottom), 8, 8, mRectPaint); + + mTextPaint.setFakeBoldText(true); + canvas.drawText(mGraphView.getGridLabelRenderer().getLabelFormatter().formatLabel(mCurrentSelectionX, true), lLeft+mStyles.padding, lTop+mStyles.padding/2+mStyles.textSize, mTextPaint); + + mTextPaint.setFakeBoldText(false); + + int i=1; + for (Map.Entry entry : mCurrentSelection.entrySet()) { + mRectPaint.setColor(entry.getKey().getColor()); + canvas.drawRect(new RectF(lLeft+mStyles.padding, lTop+mStyles.padding+(i*(mStyles.textSize+mStyles.spacing)), lLeft+mStyles.padding+shapeSize, lTop+mStyles.padding+(i*(mStyles.textSize+mStyles.spacing))+shapeSize), mRectPaint); + 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); + i++; + } + } + + public boolean onUp(MotionEvent event) { + mCursorVisible = false; + findCurrentDataPoint(); + mGraphView.invalidate(); + return true; + } + + private void findCurrentDataPoint() { + double selX = 0; + mCurrentSelection.clear(); + for (Series series : mGraphView.getSeries()) { + if (series instanceof BaseSeries) { + DataPointInterface p = ((BaseSeries) series).findDataPointAtX(mPosX); + if (p != null) { + selX = p.getX(); + mCurrentSelection.put((BaseSeries) series, p); + } + } + } + + if (!mCurrentSelection.isEmpty()) { + mCurrentSelectionX = selX; + } + } + + public void setTextSize(float t) { + mStyles.textSize = t; + } + + public void setTextColor(int color) { + mStyles.textColor = color; + } + + public void setBackgroundColor(int color) { + mStyles.backgroundColor = color; + } + + public void setSpacing(int s) { + mStyles.spacing = s; + } + + public void setPadding(int s) { + mStyles.padding = s; + } + + public void setMargin(int s) { + mStyles.margin = s; + } + + public void setWidth(int s) { + mStyles.width = s; + } +} diff --git a/src/main/java/com/jjoe64/graphview/DefaultLabelFormatter.java b/src/main/java/com/jjoe64/graphview/DefaultLabelFormatter.java index 0a761e2d7..c22c44626 100644 --- a/src/main/java/com/jjoe64/graphview/DefaultLabelFormatter.java +++ b/src/main/java/com/jjoe64/graphview/DefaultLabelFormatter.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview; diff --git a/src/main/java/com/jjoe64/graphview/GraphView.java b/src/main/java/com/jjoe64/graphview/GraphView.java index 51debe1ee..a01da0fbf 100644 --- a/src/main/java/com/jjoe64/graphview/GraphView.java +++ b/src/main/java/com/jjoe64/graphview/GraphView.java @@ -1,43 +1,44 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview; import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.Point; import android.graphics.PointF; +import android.net.Uri; +import android.provider.MediaStore; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; +import com.jjoe64.graphview.series.BaseSeries; import com.jjoe64.graphview.series.Series; +import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.List; /** * @author jjoe64 - * @version 4.0.0 */ public class GraphView extends View { /** @@ -155,11 +156,15 @@ public boolean onTouchEvent(MotionEvent event) { */ private Paint mPaintTitle; + private boolean mIsCursorMode; + /** * paint for the preview (in the SDK) */ private Paint mPreviewPaint; + private CursorMode mCursorMode; + /** * Initialize the GraphView view * @param context @@ -274,10 +279,47 @@ public List getSeries() { * performance. */ public void onDataChanged(boolean keepLabelsSize, boolean keepViewport) { - // adjust grid system + // adjustSteps grid system mViewport.calcCompleteRange(); + if (mSecondScale != null) { + mSecondScale.calcCompleteRange(); + } mGridLabelRenderer.invalidate(keepLabelsSize, keepViewport); - invalidate(); + postInvalidate(); + } + + /** + * draw all the stuff on canvas + * + * @param canvas + */ + protected void drawGraphElements(Canvas canvas) { + // must be in hardware accelerated mode + if (android.os.Build.VERSION.SDK_INT >= 11 && !canvas.isHardwareAccelerated()) { + // just warn about it, because it is ok when making a snapshot + Log.w("GraphView", "GraphView should be used in hardware accelerated mode." + + "You can use android:hardwareAccelerated=\"true\" on your activity. Read this for more info:" + + "https://developer.android.com/guide/topics/graphics/hardware-accel.html"); + } + + drawTitle(canvas); + mViewport.drawFirst(canvas); + mGridLabelRenderer.draw(canvas); + for (Series s : mSeries) { + s.draw(this, canvas, false); + } + if (mSecondScale != null) { + for (Series s : mSecondScale.getSeries()) { + s.draw(this, canvas, true); + } + } + + if (mCursorMode != null) { + mCursorMode.draw(canvas); + } + + mViewport.draw(canvas); + mLegendRenderer.draw(canvas); } /** @@ -291,19 +333,7 @@ protected void onDraw(Canvas canvas) { canvas.drawColor(Color.rgb(200, 200, 200)); canvas.drawText("GraphView: No Preview available", canvas.getWidth()/2, canvas.getHeight()/2, mPreviewPaint); } else { - drawTitle(canvas); - mViewport.drawFirst(canvas); - mGridLabelRenderer.draw(canvas); - for (Series s : mSeries) { - s.draw(this, canvas, false); - } - if (mSecondScale != null) { - for (Series s : mSecondScale.getSeries()) { - s.draw(this, canvas, true); - } - } - mViewport.draw(canvas); - mLegendRenderer.draw(canvas); + drawGraphElements(canvas); } } @@ -402,6 +432,7 @@ public int getGraphContentWidth() { int graphwidth = getWidth() - (2 * border) - getGridLabelRenderer().getLabelVerticalWidth(); if (mSecondScale != null) { graphwidth -= getGridLabelRenderer().getLabelVerticalSecondScaleWidth(); + graphwidth -= mSecondScale.getVerticalAxisTitleTextSize(); } return graphwidth; } @@ -513,16 +544,29 @@ public void setTitleColor(int titleColor) { } /** + * creates the second scale logic and returns it * - * @return + * @return second scale object */ public SecondScale getSecondScale() { if (mSecondScale == null) { - mSecondScale = new SecondScale(mViewport); + // this creates the second scale + mSecondScale = new SecondScale(this); + mSecondScale.setVerticalAxisTitleTextSize(mGridLabelRenderer.mStyles.textSize); } return mSecondScale; } + /** + * clears the second scale + */ + public void clearSecondScale() { + if (mSecondScale != null) { + mSecondScale.removeAllSeries(); + mSecondScale = null; + } + } + /** * Removes all series of the graph. */ @@ -533,7 +577,7 @@ public void removeAllSeries() { /** * Remove a specific series of the graph. - * This will also re-render the graph, but + * This will also re-draw the graph, but * without recalculating the viewport and * label sizes. * If you want this, you have to call {@link #onDataChanged(boolean, boolean)} @@ -545,4 +589,70 @@ public void removeSeries(Series series) { mSeries.remove(series); onDataChanged(false, false); } + + /** + * takes a snapshot and return it as bitmap + * + * @return snapshot of graph + */ + public Bitmap takeSnapshot() { + Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + draw(canvas); + return bitmap; + } + + /** + * takes a snapshot, stores it and open the share dialog. + * Notice that you need the permission android.permission.WRITE_EXTERNAL_STORAGE + * + * @param context + * @param imageName + * @param title + */ + public void takeSnapshotAndShare(Context context, String imageName, String title) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + Bitmap inImage = takeSnapshot(); + inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes); + + String path = MediaStore.Images.Media.insertImage(context.getContentResolver(), inImage, imageName, null); + if (path == null) { + // most likely a security problem + throw new SecurityException("Could not get path from MediaStore. Please check permissions."); + } + + Intent i = new Intent(Intent.ACTION_SEND); + i.setType("image/*"); + i.putExtra(Intent.EXTRA_STREAM, Uri.parse(path)); + try { + context.startActivity(Intent.createChooser(i, title)); + } catch (android.content.ActivityNotFoundException ex) { + ex.printStackTrace(); + } + } + + public void setCursorMode(boolean b) { + mIsCursorMode = b; + if (mIsCursorMode) { + if (mCursorMode == null) { + mCursorMode = new CursorMode(this); + } + } else { + mCursorMode = null; + invalidate(); + } + for (Series series : mSeries) { + if (series instanceof BaseSeries) { + ((BaseSeries) series).clearCursorModeCache(); + } + } + } + + public CursorMode getCursorMode() { + return mCursorMode; + } + + public boolean isCursorMode() { + return mIsCursorMode; + } } diff --git a/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java b/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java index d9244b068..37210b4e7 100644 --- a/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java +++ b/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview; @@ -24,8 +21,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.support.v4.view.ViewCompat; -import android.util.Log; import android.util.TypedValue; import java.util.LinkedHashMap; @@ -38,6 +33,26 @@ * @author jjoe64 */ public class GridLabelRenderer { + + /** + * Hoziontal label alignment + */ + public enum VerticalLabelsVAlign { + /** + * Above vertical line + */ + ABOVE, + /** + * Mid vertical line + */ + MID, + /** + * Below vertical line + */ + BELOW + } + + /** * wrapper for the styles regarding * to the grid and the labels @@ -110,6 +125,12 @@ public final class Styles { * font color of the horizontal axis title */ public int horizontalAxisTitleColor; + + /** + * angle of the horizontal axis label in + * degrees between 0 and 180 + */ + public float horizontalLabelsAngle; /** * flag whether the horizontal labels are @@ -132,13 +153,37 @@ public final class Styles { * the space between the labels text and the graph content */ int labelsSpace; + + /** + * vertical labels vertical align (above, below, mid of the grid line) + */ + VerticalLabelsVAlign verticalLabelsVAlign = VerticalLabelsVAlign.MID; } /** * Definition which lines will be drawn in the background */ public enum GridStyle { - BOTH, VERTICAL, HORIZONTAL, NONE; + /** + * show vertical and horizonal lines + * this is the default + */ + BOTH, + + /** + * show only vertical lines + */ + VERTICAL, + + /** + * show only horizontal lines + */ + HORIZONTAL, + + /** + * dont draw any lines + */ + NONE; public boolean drawVertical() { return this == BOTH || this == VERTICAL && this != NONE; } public boolean drawHorizontal() { return this == BOTH || this == HORIZONTAL && this != NONE; } @@ -175,7 +220,6 @@ public enum GridStyle { /** * cache of the horizontal steps * (vertical lines and horizontal labels) - * Key = Pixel (x) * Value = x-value */ private Map mStepsHorizontal; @@ -199,7 +243,7 @@ public enum GridStyle { * flag whether is bounds are automatically * adjusted for nice human-readable numbers */ - private boolean mIsAdjusted; + protected boolean mIsAdjusted; /** * the width of the vertical labels @@ -271,6 +315,32 @@ public enum GridStyle { */ private int mNumHorizontalLabels; + /** + * sets the space for the vertical labels on the right side + * + * @param newWidth set fixed width. set null to calculate it automatically + */ + public void setSecondScaleLabelVerticalWidth(Integer newWidth) { + mLabelVerticalSecondScaleWidth = newWidth; + } + + /** + * activate or deactivate human rounding of the + * horizontal axis. GraphView tries to fit the labels + * to display numbers that can be divided by 1, 2, or 5. + */ + private boolean mHumanRoundingY; + + /** + * activate or deactivate human rounding of the + * horizontal axis. GraphView tries to fit the labels + * to display numbers that can be divided by 1, 2, or 5. + * + * By default this is enabled. It makes sense to deactivate it + * when using Dates on the x axis. + */ + private boolean mHumanRoundingX; + /** * create the default grid label renderer. * @@ -283,6 +353,8 @@ public GridLabelRenderer(GraphView graphView) { resetStyles(); mNumVerticalLabels = 5; mNumHorizontalLabels = 5; + mHumanRoundingX = true; + mHumanRoundingY = true; } /** @@ -338,9 +410,10 @@ public void resetStyles() { mStyles.horizontalLabelsVisible = true; mStyles.verticalLabelsVisible = true; + + mStyles.horizontalLabelsAngle = 0f; mStyles.gridStyle = GridStyle.BOTH; - reloadStyles(); } @@ -362,6 +435,59 @@ public void reloadStyles() { mPaintAxisTitle.setTextAlign(Paint.Align.CENTER); } + /** + * GraphView tries to fit the labels + * to display numbers that can be divided by 1, 2, or 5. + * + * By default this is enabled. It makes sense to deactivate it + * when using Dates on the x axis. + + * @return if human rounding is enabled + */ + public boolean isHumanRoundingX() { + return mHumanRoundingX; + } + + /** + * GraphView tries to fit the labels + * to display numbers that can be divided by 1, 2, or 5. + * + * @return if human rounding is enabled + */ + public boolean isHumanRoundingY() { + return mHumanRoundingY; + } + + /** + * activate or deactivate human rounding of the + * horizontal axis. GraphView tries to fit the labels + * to display numbers that can be divided by 1, 2, or 5. + * + * By default this is enabled. It makes sense to deactivate it + * when using Dates on the x axis. + * + * @param humanRoundingX false to deactivate + * @param humanRoundingY false to deactivate + */ + public void setHumanRounding(boolean humanRoundingX, boolean humanRoundingY) { + this.mHumanRoundingX = humanRoundingX; + this.mHumanRoundingY = humanRoundingY; + } + + /** + * activate or deactivate human rounding of the + * horizontal axis. GraphView tries to fit the labels + * to display numbers that can be divided by 1, 2, or 5. + * + * By default this is enabled. + * + * @param humanRoundingBoth false to deactivate on both axises + */ + public void setHumanRounding(boolean humanRoundingBoth) { + this.mHumanRoundingX = humanRoundingBoth; + this.mHumanRoundingY = humanRoundingBoth; + } + /** * @return the general text size for the axis titles */ @@ -390,6 +516,13 @@ public Paint.Align getVerticalLabelsAlign() { public int getHorizontalLabelsColor() { return mStyles.horizontalLabelsColor; } + + /** + * @return the angle of the horizontal labels + */ + public float getHorizontalLabelsAngle() { + return mStyles.horizontalLabelsAngle; + } /** * clears the internal cache and forces @@ -440,8 +573,8 @@ protected boolean adjustVerticalSecondScale() { return true; } - double minY = mGraphView.mSecondScale.getMinY(); - double maxY = mGraphView.mSecondScale.getMaxY(); + double minY = mGraphView.mSecondScale.getMinY(false); + double maxY = mGraphView.mSecondScale.getMaxY(false); // TODO find the number of labels int numVerticalLabels = mNumVerticalLabels; @@ -450,37 +583,110 @@ protected boolean adjustVerticalSecondScale() { double exactSteps; if (mGraphView.mSecondScale.isYAxisBoundsManual()) { - newMinY = minY; - double rangeY = maxY - newMinY; - exactSteps = rangeY / (numVerticalLabels - 1); + // split range into equal steps + exactSteps = (maxY - minY) / (numVerticalLabels - 1); + + // round because of floating error + exactSteps = Math.round(exactSteps * 1000000d) / 1000000d; } else { // TODO auto adjusting throw new IllegalStateException("Not yet implemented"); } - double newMaxY = newMinY + (numVerticalLabels - 1) * exactSteps; + if (mStepsVerticalSecondScale != null && mStepsVerticalSecondScale.size() > 1) { + // else choose other nice steps that previous + // steps are included (divide to have more, or multiplicate to have less) - // TODO auto adjusting - //mGraphView.getViewport().setMinY(newMinY); - //mGraphView.getViewport().setMaxY(newMaxY); + double d1 = 0, d2 = 0; + int i = 0; + for (Double v : mStepsVerticalSecondScale.values()) { + if (i == 0) { + d1 = v; + } else { + d2 = v; + break; + } + i++; + } + double oldSteps = d2 - d1; + if (oldSteps > 0) { + double newSteps = Double.NaN; + + if (oldSteps > exactSteps) { + newSteps = oldSteps / 2; + } else if (oldSteps < exactSteps) { + newSteps = oldSteps * 2; + } + + // only if there wont be more than numLabels + // and newSteps will be better than oldSteps + int numStepsOld = (int) ((maxY - minY) / oldSteps); + int numStepsNew = (int) ((maxY - minY) / newSteps); + + boolean shouldChange; + + // avoid switching between 2 steps + if (numStepsOld <= numVerticalLabels && numStepsNew <= numVerticalLabels) { + // both are possible + // only the new if it hows more labels + shouldChange = numStepsNew > numStepsOld; + } else { + shouldChange = true; + } + + if (newSteps != Double.NaN && shouldChange && numStepsNew <= numVerticalLabels) { + exactSteps = newSteps; + } else { + // try to stay to the old steps + exactSteps = oldSteps; + } + } + } else { + // first time + } - //if (!mGraphView.getViewport().isYAxisBoundsManual()) { - // mGraphView.getViewport().setYAxisBoundsStatus(Viewport.AxisBoundsStatus.AUTO_ADJUSTED); - //} + // find the first data point that is relevant to display + // starting from 1st datapoint so that the steps have nice numbers + // goal is to start with the minY or 1 step before + newMinY = mGraphView.getSecondScale().mReferenceY; + // must be down-rounded + double count = Math.floor((minY-newMinY)/exactSteps); + newMinY = count*exactSteps + newMinY; + + // it can happen that we need to add some more labels to fill the complete screen + numVerticalLabels = (int) ((mGraphView.getSecondScale().mCurrentViewport.height()*-1 / exactSteps)) + 2; + + // ensure that the value is valid (minimum 2) + // see https://github.com/appsthatmatter/GraphView/issues/520 + numVerticalLabels = Math.max(numVerticalLabels, 2); if (mStepsVerticalSecondScale != null) { mStepsVerticalSecondScale.clear(); } else { - mStepsVerticalSecondScale = new LinkedHashMap(numVerticalLabels); + mStepsVerticalSecondScale = new LinkedHashMap<>(numVerticalLabels); } + int height = mGraphView.getGraphContentHeight(); - double v = newMaxY; - int p = mGraphView.getGraphContentTop(); // start - int pixelStep = height / (numVerticalLabels - 1); + // convert data-y to pixel-y in current viewport + double pixelPerData = height / mGraphView.getSecondScale().mCurrentViewport.height()*-1; + for (int i = 0; i < numVerticalLabels; i++) { - mStepsVerticalSecondScale.put(p, v); - p += pixelStep; - v -= exactSteps; + // dont draw if it is top of visible screen + if (newMinY + (i * exactSteps) > mGraphView.getSecondScale().mCurrentViewport.top) { + continue; + } + // dont draw if it is below of visible screen + if (newMinY + (i * exactSteps) < mGraphView.getSecondScale().mCurrentViewport.bottom) { + continue; + } + + + // where is the data point on the current screen + double dataPointPos = newMinY + (i * exactSteps); + double relativeToCurrentViewport = dataPointPos - mGraphView.getSecondScale().mCurrentViewport.bottom; + + double pixelPos = relativeToCurrentViewport * pixelPerData; + mStepsVerticalSecondScale.put((int) pixelPos, dataPointPos); } return true; @@ -493,7 +699,7 @@ protected boolean adjustVerticalSecondScale() { * * @return true if it is ready */ - protected boolean adjustVertical() { + protected boolean adjustVertical(boolean changeBounds) { if (mLabelHorizontalHeight == null) { return false; } @@ -511,84 +717,131 @@ protected boolean adjustVertical() { double newMinY; double exactSteps; - if (mGraphView.getViewport().isYAxisBoundsManual()) { - newMinY = minY; - double rangeY = maxY - newMinY; - exactSteps = rangeY / (numVerticalLabels - 1); - } else { - // find good steps - boolean adjusting = true; - newMinY = minY; - exactSteps = 0d; - while (adjusting) { - double rangeY = maxY - newMinY; - exactSteps = rangeY / (numVerticalLabels - 1); - exactSteps = humanRound(exactSteps, true); - - // adjust viewport - // wie oft passt STEP in minY rein? - int count = 0; - if (newMinY >= 0d) { - // positive number - while (newMinY - exactSteps >= 0) { - newMinY -= exactSteps; - count++; - } - newMinY = exactSteps * count; + // split range into equal steps + exactSteps = (maxY - minY) / (numVerticalLabels - 1); + + // round because of floating error + exactSteps = Math.round(exactSteps * 1000000d) / 1000000d; + + // smallest viewport + if (exactSteps == 0d) { + exactSteps = 0.0000001d; + maxY = minY + exactSteps * (numVerticalLabels - 1); + } + + // human rounding to have nice numbers (1, 2, 5, ...) + if (isHumanRoundingY()) { + exactSteps = humanRound(exactSteps, changeBounds); + } else if (mStepsVertical != null && mStepsVertical.size() > 1) { + // else choose other nice steps that previous + // steps are included (divide to have more, or multiplicate to have less) + + double d1 = 0, d2 = 0; + int i = 0; + for (Double v : mStepsVertical.values()) { + if (i == 0) { + d1 = v; } else { - // negative number - count++; - while (newMinY + exactSteps < 0) { - newMinY += exactSteps; - count++; - } - newMinY = exactSteps * count * -1; + d2 = v; + break; + } + i++; + } + double oldSteps = d2 - d1; + if (oldSteps > 0) { + double newSteps = Double.NaN; + + if (oldSteps > exactSteps) { + newSteps = oldSteps / 2; + } else if (oldSteps < exactSteps) { + newSteps = oldSteps * 2; + } + + // only if there wont be more than numLabels + // and newSteps will be better than oldSteps + int numStepsOld = (int) ((maxY - minY) / oldSteps); + int numStepsNew = (int) ((maxY - minY) / newSteps); + + boolean shouldChange; + + // avoid switching between 2 steps + if (numStepsOld <= numVerticalLabels && numStepsNew <= numVerticalLabels) { + // both are possible + // only the new if it hows more labels + shouldChange = numStepsNew > numStepsOld; + } else { + shouldChange = true; } - // wenn minY sich geändert hat, steps nochmal berechnen - // wenn nicht, fertig - if (newMinY == minY) { - adjusting = false; + if (newSteps != Double.NaN && shouldChange && numStepsNew <= numVerticalLabels) { + exactSteps = newSteps; } else { - minY = newMinY; + // try to stay to the old steps + exactSteps = oldSteps; } } + } else { + // first time } - double newMaxY = newMinY + (numVerticalLabels - 1) * exactSteps; - mGraphView.getViewport().setMinY(newMinY); - mGraphView.getViewport().setMaxY(newMaxY); - - if (!mGraphView.getViewport().isYAxisBoundsManual()) { - mGraphView.getViewport().setYAxisBoundsStatus(Viewport.AxisBoundsStatus.AUTO_ADJUSTED); + // find the first data point that is relevant to display + // starting from 1st datapoint so that the steps have nice numbers + // goal is to start with the minX or 1 step before + newMinY = mGraphView.getViewport().getReferenceY(); + // must be down-rounded + double count = Math.floor((minY-newMinY)/exactSteps); + newMinY = count*exactSteps + newMinY; + + // now we have our labels bounds + if (changeBounds) { + mGraphView.getViewport().setMinY(newMinY); + mGraphView.getViewport().setMaxY(Math.max(maxY, newMinY + (numVerticalLabels - 1) * exactSteps)); + mGraphView.getViewport().mYAxisBoundsStatus = Viewport.AxisBoundsStatus.AUTO_ADJUSTED; } + // it can happen that we need to add some more labels to fill the complete screen + numVerticalLabels = (int) ((mGraphView.getViewport().mCurrentViewport.height()*-1 / exactSteps)) + 2; + if (mStepsVertical != null) { mStepsVertical.clear(); } else { - mStepsVertical = new LinkedHashMap(numVerticalLabels); + mStepsVertical = new LinkedHashMap<>((int) numVerticalLabels); } + int height = mGraphView.getGraphContentHeight(); - double v = newMaxY; - int p = mGraphView.getGraphContentTop(); // start - int pixelStep = height / (numVerticalLabels - 1); + // convert data-y to pixel-y in current viewport + double pixelPerData = height / mGraphView.getViewport().mCurrentViewport.height()*-1; + for (int i = 0; i < numVerticalLabels; i++) { - mStepsVertical.put(p, v); - p += pixelStep; - v -= exactSteps; + // dont draw if it is top of visible screen + if (newMinY + (i * exactSteps) > mGraphView.getViewport().mCurrentViewport.top) { + continue; + } + // dont draw if it is below of visible screen + if (newMinY + (i * exactSteps) < mGraphView.getViewport().mCurrentViewport.bottom) { + continue; + } + + + // where is the data point on the current screen + double dataPointPos = newMinY + (i * exactSteps); + double relativeToCurrentViewport = dataPointPos - mGraphView.getViewport().mCurrentViewport.bottom; + + double pixelPos = relativeToCurrentViewport * pixelPerData; + mStepsVertical.put((int) pixelPos, dataPointPos); } return true; } /** - * calculates the horizontal steps. This will - * automatically change the bounds to nice - * human-readable min/max. + * calculates the horizontal steps. * + * @param changeBounds This will automatically change the + * bounds to nice human-readable min/max. * @return true if it is ready */ - protected boolean adjustHorizontal() { + protected boolean adjustHorizontal(boolean changeBounds) { if (mLabelVerticalWidth == null) { return false; } @@ -603,126 +856,113 @@ protected boolean adjustHorizontal() { double newMinX; double exactSteps; - float scalingOffset = 0f; - if (mGraphView.getViewport().isXAxisBoundsManual() && mGraphView.getViewport().getXAxisBoundsStatus() != Viewport.AxisBoundsStatus.READJUST_AFTER_SCALE) { - // scaling - if (mGraphView.getViewport().mScalingActive) { - minX = mGraphView.getViewport().mScalingBeginLeft; - maxX = minX + mGraphView.getViewport().mScalingBeginWidth; + // split range into equal steps + exactSteps = (maxX - minX) / (numHorizontalLabels - 1); - //numHorizontalLabels *= (mGraphView.getViewport().mCurrentViewport.width()+oldStep)/(mGraphView.getViewport().mScalingBeginWidth+oldStep); - //numHorizontalLabels = (float) Math.ceil(numHorizontalLabels); - } + // round because of floating error + exactSteps = Math.round(exactSteps * 1000000d) / 1000000d; - newMinX = minX; - double rangeX = maxX - newMinX; - exactSteps = rangeX / (numHorizontalLabels - 1); - } else { - // find good steps - boolean adjusting = true; - newMinX = minX; - exactSteps = 0d; - while (adjusting) { - double rangeX = maxX - newMinX; - exactSteps = rangeX / (numHorizontalLabels - 1); - - boolean roundAlwaysUp = true; - if (mGraphView.getViewport().getXAxisBoundsStatus() == Viewport.AxisBoundsStatus.READJUST_AFTER_SCALE) { - // if viewports gets smaller, round down - if (mGraphView.getViewport().mCurrentViewport.width() < mGraphView.getViewport().mScalingBeginWidth) { - roundAlwaysUp = false; - } - } - exactSteps = humanRound(exactSteps, roundAlwaysUp); - - // adjust viewport - // wie oft passt STEP in minX rein? - int count = 0; - if (newMinX >= 0d) { - // positive number - while (newMinX - exactSteps >= 0) { - newMinX -= exactSteps; - count++; - } - newMinX = exactSteps * count; + // smallest viewport + if (exactSteps == 0d) { + exactSteps = 0.0000001d; + maxX = minX + exactSteps * (numHorizontalLabels - 1); + } + + // human rounding to have nice numbers (1, 2, 5, ...) + if (isHumanRoundingX()) { + exactSteps = humanRound(exactSteps, false); + } else if (mStepsHorizontal != null && mStepsHorizontal.size() > 1) { + // else choose other nice steps that previous + // steps are included (divide to have more, or multiplicate to have less) + + double d1 = 0, d2 = 0; + int i = 0; + for (Double v : mStepsHorizontal.values()) { + if (i == 0) { + d1 = v; } else { - // negative number - count++; - while (newMinX + exactSteps < 0) { - newMinX += exactSteps; - count++; - } - newMinX = exactSteps * count * -1; + d2 = v; + break; + } + i++; + } + double oldSteps = d2 - d1; + if (oldSteps > 0) { + double newSteps = Double.NaN; + + if (oldSteps > exactSteps) { + newSteps = oldSteps / 2; + } else if (oldSteps < exactSteps) { + newSteps = oldSteps * 2; } - // wenn minX sich geändert hat, steps nochmal berechnen - // wenn nicht, fertig - if (newMinX == minX) { - adjusting = false; + // only if there wont be more than numLabels + // and newSteps will be better than oldSteps + int numStepsOld = (int) ((maxX - minX) / oldSteps); + int numStepsNew = (int) ((maxX - minX) / newSteps); + + boolean shouldChange; + + // avoid switching between 2 steps + if (numStepsOld <= numHorizontalLabels && numStepsNew <= numHorizontalLabels) { + // both are possible + // only the new if it hows more labels + shouldChange = numStepsNew > numStepsOld; } else { - minX = newMinX; + shouldChange = true; } - } - double newMaxX = newMinX + (numHorizontalLabels - 1) * exactSteps; - mGraphView.getViewport().setMinX(newMinX); - mGraphView.getViewport().setMaxX(newMaxX); - if (mGraphView.getViewport().getXAxisBoundsStatus() == Viewport.AxisBoundsStatus.READJUST_AFTER_SCALE) { - mGraphView.getViewport().setXAxisBoundsStatus(Viewport.AxisBoundsStatus.FIX); - } else { - mGraphView.getViewport().setXAxisBoundsStatus(Viewport.AxisBoundsStatus.AUTO_ADJUSTED); + if (newSteps != Double.NaN && shouldChange && numStepsNew <= numHorizontalLabels) { + exactSteps = newSteps; + } else { + // try to stay to the old steps + exactSteps = oldSteps; + } } - } - - if (mStepsHorizontal != null) { - mStepsHorizontal.clear(); } else { - mStepsHorizontal = new LinkedHashMap((int) numHorizontalLabels); + // first time } - int width = mGraphView.getGraphContentWidth(); - - float scrolled = 0; - float scrolledPixels = 0; - - double v = newMinX; - int p = mGraphView.getGraphContentLeft(); // start - float pixelStep = width / (numHorizontalLabels - 1); - if (mGraphView.getViewport().mScalingActive) { - float oldStep = mGraphView.getViewport().mScalingBeginWidth / (numHorizontalLabels - 1); - float factor = (mGraphView.getViewport().mCurrentViewport.width() + oldStep) / (mGraphView.getViewport().mScalingBeginWidth + oldStep); - pixelStep *= 1f / factor; - //numHorizontalLabels *= (mGraphView.getViewport().mCurrentViewport.width()+oldStep)/(mGraphView.getViewport().mScalingBeginWidth+oldStep); - //numHorizontalLabels = (float) Math.ceil(numHorizontalLabels); - - //scrolled = ((float) mGraphView.getViewport().getMinX(false) - mGraphView.getViewport().mScalingBeginLeft)*2; - float newWidth = width * 1f / factor; - scrolledPixels = (newWidth - width) * -0.5f; + // starting from 1st datapoint + // goal is to start with the minX or 1 step before + newMinX = mGraphView.getViewport().getReferenceX(); + // must be down-rounded + double count = Math.floor((minX-newMinX)/exactSteps); + newMinX = count*exactSteps + newMinX; + // now we have our labels bounds + if (changeBounds) { + mGraphView.getViewport().setMinX(newMinX); + mGraphView.getViewport().setMaxX(newMinX + (numHorizontalLabels - 1) * exactSteps); + mGraphView.getViewport().mXAxisBoundsStatus = Viewport.AxisBoundsStatus.AUTO_ADJUSTED; } - // scrolling - if (!Float.isNaN(mGraphView.getViewport().mScrollingReferenceX)) { - scrolled = mGraphView.getViewport().mScrollingReferenceX - (float) newMinX; - scrolledPixels += scrolled * (pixelStep / (float) exactSteps); + // it can happen that we need to add some more labels to fill the complete screen + numHorizontalLabels = (int) ((mGraphView.getViewport().mCurrentViewport.width() / exactSteps)) + 1; - if (scrolled < 0 - exactSteps) { - mGraphView.getViewport().mScrollingReferenceX += exactSteps; - } else if (scrolled > exactSteps) { - mGraphView.getViewport().mScrollingReferenceX -= exactSteps; - } + if (mStepsHorizontal != null) { + mStepsHorizontal.clear(); + } else { + mStepsHorizontal = new LinkedHashMap<>((int) numHorizontalLabels); } - p += scrolledPixels; - v += scrolled; + + int width = mGraphView.getGraphContentWidth(); + // convert data-x to pixel-x in current viewport + double pixelPerData = width / mGraphView.getViewport().mCurrentViewport.width(); for (int i = 0; i < numHorizontalLabels; i++) { - // don't draw steps before 0 (scrolling) - if (p >= mGraphView.getGraphContentLeft()) { - mStepsHorizontal.put(p, v); + // dont draw if it is left of visible screen + if (newMinX + (i * exactSteps) < mGraphView.getViewport().mCurrentViewport.left) { + continue; } - p += pixelStep; - v += exactSteps; + + // where is the data point on the current screen + double dataPointPos = newMinX + (i * exactSteps); + double relativeToCurrentViewport = dataPointPos - mGraphView.getViewport().mCurrentViewport.left; + + double pixelPos = relativeToCurrentViewport * pixelPerData; + mStepsHorizontal.put((int) pixelPos, dataPointPos); } return true; @@ -734,10 +974,10 @@ protected boolean adjustHorizontal() { * nice human-readable values, except the bounds * are manual. */ - protected void adjust() { - mIsAdjusted = adjustVertical(); + protected void adjustSteps() { + mIsAdjusted = adjustVertical(! Viewport.AxisBoundsStatus.FIX.equals(mGraphView.getViewport().mYAxisBoundsStatus)); mIsAdjusted &= adjustVerticalSecondScale(); - mIsAdjusted &= adjustHorizontal(); + mIsAdjusted &= adjustHorizontal(! Viewport.AxisBoundsStatus.FIX.equals(mGraphView.getViewport().mXAxisBoundsStatus)); } /** @@ -787,7 +1027,7 @@ protected void calcLabelVerticalSecondScaleSize(Canvas canvas) { } // test label - double testY = ((mGraphView.mSecondScale.getMaxY() - mGraphView.mSecondScale.getMinY()) * 0.783) + mGraphView.mSecondScale.getMinY(); + double testY = ((mGraphView.mSecondScale.getMaxY(false) - mGraphView.mSecondScale.getMinY(false)) * 0.783) + mGraphView.mSecondScale.getMinY(false); String testLabel = mGraphView.mSecondScale.getLabelFormatter().formatLabel(testY, false); Rect textBounds = new Rect(); mPaintLabel.getTextBounds(testLabel, 0, testLabel.length(), textBounds); @@ -830,6 +1070,16 @@ protected void calcLabelHorizontalSize(Canvas canvas) { mLabelHorizontalHeight = (int) Math.max(mLabelHorizontalHeight, mStyles.textSize); } + if (mStyles.horizontalLabelsAngle > 0f && mStyles.horizontalLabelsAngle <= 180f) { + int adjHorizontalHeightH = (int) Math.round(Math.abs(mLabelHorizontalHeight*Math.cos(Math.toRadians(mStyles.horizontalLabelsAngle)))); + int adjHorizontalHeightW = (int) Math.round(Math.abs(mLabelHorizontalWidth*Math.sin(Math.toRadians(mStyles.horizontalLabelsAngle)))); + int adjHorizontalWidthH = (int) Math.round(Math.abs(mLabelHorizontalHeight*Math.sin(Math.toRadians(mStyles.horizontalLabelsAngle)))); + int adjHorizontalWidthW = (int) Math.round(Math.abs(mLabelHorizontalWidth*Math.cos(Math.toRadians(mStyles.horizontalLabelsAngle)))); + + mLabelHorizontalHeight = adjHorizontalHeightH + adjHorizontalHeightW; + mLabelHorizontalWidth = adjHorizontalWidthH + adjHorizontalWidthW; + } + // space between text and graph content mLabelHorizontalHeight += mStyles.labelsSpace; } @@ -854,13 +1104,13 @@ public void draw(Canvas canvas) { labelSizeChanged = true; } if (labelSizeChanged) { - // redraw - ViewCompat.postInvalidateOnAnimation(mGraphView); + // redraw directly + mGraphView.drawGraphElements(canvas); return; } if (!mIsAdjusted) { - adjust(); + adjustSteps(); } if (mIsAdjusted) { @@ -874,6 +1124,11 @@ public void draw(Canvas canvas) { drawHorizontalAxisTitle(canvas); drawVerticalAxisTitle(canvas); + + // draw second scale axis title if it exists + if (mGraphView.mSecondScale != null) { + mGraphView.mSecondScale.drawVerticalAxisTitle(canvas); + } } /** @@ -953,16 +1208,27 @@ protected void drawHorizontalSteps(Canvas canvas) { } } if (mStyles.gridStyle.drawVertical()) { - canvas.drawLine(e.getKey(), mGraphView.getGraphContentTop(), e.getKey(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mPaintLine); + // dont draw if it is right of visible screen + if (e.getKey() <= mGraphView.getGraphContentWidth()) { + canvas.drawLine(mGraphView.getGraphContentLeft()+e.getKey(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft()+e.getKey(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mPaintLine); + } } // draw label if (isHorizontalLabelsVisible()) { - mPaintLabel.setTextAlign(Paint.Align.CENTER); - if (i == mStepsHorizontal.size() - 1) - mPaintLabel.setTextAlign(Paint.Align.RIGHT); - if (i == 0) - mPaintLabel.setTextAlign(Paint.Align.LEFT); + if (mStyles.horizontalLabelsAngle > 0f && mStyles.horizontalLabelsAngle <= 180f) { + if (mStyles.horizontalLabelsAngle < 90f) { + mPaintLabel.setTextAlign((Paint.Align.RIGHT)); + } else if (mStyles.horizontalLabelsAngle <= 180f) { + mPaintLabel.setTextAlign((Paint.Align.LEFT)); + } + } else { + mPaintLabel.setTextAlign(Paint.Align.CENTER); + if (i == mStepsHorizontal.size() - 1) + mPaintLabel.setTextAlign(Paint.Align.RIGHT); + if (i == 0) + mPaintLabel.setTextAlign(Paint.Align.LEFT); + } // multiline labels String label = mLabelFormatter.formatLabel(e.getValue(), true); @@ -970,10 +1236,31 @@ protected void drawHorizontalSteps(Canvas canvas) { label = ""; } String[] lines = label.split("\n"); + + // If labels are angled, calculate adjustment to line them up with the grid + int labelWidthAdj = 0; + if (mStyles.horizontalLabelsAngle > 0f && mStyles.horizontalLabelsAngle <= 180f) { + Rect textBounds = new Rect(); + mPaintLabel.getTextBounds(lines[0], 0, lines[0].length(), textBounds); + labelWidthAdj = (int) Math.abs(textBounds.width()*Math.cos(Math.toRadians(mStyles.horizontalLabelsAngle))); + } for (int li = 0; li < lines.length; li++) { // for the last line y = height float y = (canvas.getHeight() - mStyles.padding - getHorizontalAxisTitleHeight()) - (lines.length - li - 1) * getTextSize() * 1.1f + mStyles.labelsSpace; - canvas.drawText(lines[li], e.getKey(), y, mPaintLabel); + float x = mGraphView.getGraphContentLeft()+e.getKey(); + if (mStyles.horizontalLabelsAngle > 0 && mStyles.horizontalLabelsAngle < 90f) { + canvas.save(); + canvas.rotate(mStyles.horizontalLabelsAngle, x + labelWidthAdj, y); + canvas.drawText(lines[li], x + labelWidthAdj, y, mPaintLabel); + canvas.restore(); + } else if (mStyles.horizontalLabelsAngle > 0 && mStyles.horizontalLabelsAngle <= 180f) { + canvas.save(); + canvas.rotate(mStyles.horizontalLabelsAngle - 180f, x - labelWidthAdj, y); + canvas.drawText(lines[li], x - labelWidthAdj, y, mPaintLabel); + canvas.restore(); + } else { + canvas.drawText(lines[li], x, y, mPaintLabel); + } } } i++; @@ -996,6 +1283,8 @@ protected void drawVerticalStepsSecondScale(Canvas canvas) { mPaintLabel.setColor(getVerticalLabelsSecondScaleColor()); mPaintLabel.setTextAlign(getVerticalLabelsSecondScaleAlign()); for (Map.Entry e : mStepsVerticalSecondScale.entrySet()) { + float posY = mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight()-e.getKey(); + // draw label int labelsWidth = mLabelVerticalSecondScaleWidth; int labelsOffset = (int) startLeft; @@ -1005,7 +1294,7 @@ protected void drawVerticalStepsSecondScale(Canvas canvas) { labelsOffset += labelsWidth / 2; } - float y = e.getKey(); + float y = posY; String[] lines = mGraphView.mSecondScale.mLabelFormatter.formatLabel(e.getValue(), false).split("\n"); y += (lines.length * getTextSize() * 1.1f) / 2; // center text vertically @@ -1028,7 +1317,13 @@ protected void drawVerticalSteps(Canvas canvas) { float startLeft = mGraphView.getGraphContentLeft(); mPaintLabel.setColor(getVerticalLabelsColor()); mPaintLabel.setTextAlign(getVerticalLabelsAlign()); + + int numberOfLine = mStepsVertical.size(); + int currentLine = 1; + for (Map.Entry e : mStepsVertical.entrySet()) { + float posY = mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight()-e.getKey(); + // draw line if (mStyles.highlightZeroLines) { if (e.getValue() == 0d) { @@ -1038,11 +1333,18 @@ protected void drawVerticalSteps(Canvas canvas) { } } if (mStyles.gridStyle.drawHorizontal()) { - canvas.drawLine(startLeft, e.getKey(), startLeft + mGraphView.getGraphContentWidth(), e.getKey(), mPaintLine); + canvas.drawLine(startLeft, posY, startLeft + mGraphView.getGraphContentWidth(), posY, mPaintLine); + } + + //if draw the label above or below the line, we mustn't draw the first for last label, for beautiful design. + boolean isDrawLabel = true; + if ((mStyles.verticalLabelsVAlign == VerticalLabelsVAlign.ABOVE && currentLine == 1) + || (mStyles.verticalLabelsVAlign == VerticalLabelsVAlign.BELOW && currentLine == numberOfLine)){ + isDrawLabel = false; } // draw label - if (isVerticalLabelsVisible()) { + if (isVerticalLabelsVisible() && isDrawLabel) { int labelsWidth = mLabelVerticalWidth; int labelsOffset = 0; if (getVerticalLabelsAlign() == Paint.Align.RIGHT) { @@ -1053,20 +1355,33 @@ protected void drawVerticalSteps(Canvas canvas) { } labelsOffset += mStyles.padding + getVerticalAxisTitleWidth(); - float y = e.getKey(); + float y = posY; String label = mLabelFormatter.formatLabel(e.getValue(), false); if (label == null) { label = ""; } String[] lines = label.split("\n"); - y += (lines.length * getTextSize() * 1.1f) / 2; // center text vertically + switch (mStyles.verticalLabelsVAlign){ + case MID: + y += (lines.length * getTextSize() * 1.1f) / 2; // center text vertically + break; + case ABOVE: + y -= 5; + break; + case BELOW: + y += (lines.length * getTextSize() * 1.1f) + 5; + break; + } + for (int li = 0; li < lines.length; li++) { // for the last line y = height float y2 = y - (lines.length - li - 1) * getTextSize() * 1.1f; canvas.drawText(lines[li], labelsOffset, y2, mPaintLabel); } } + + currentLine ++; } } @@ -1081,11 +1396,11 @@ protected void drawVerticalSteps(Canvas canvas) { protected double humanRound(double in, boolean roundAlwaysUp) { // round-up to 1-steps, 2-steps or 5-steps int ten = 0; - while (in >= 10d) { + while (Math.abs(in) >= 10d) { in /= 10d; ten++; } - while (in < 1d) { + while (Math.abs(in) < 1d) { in *= 10d; ten--; } @@ -1123,6 +1438,10 @@ public Styles getStyles() { * 0 if there are no vertical labels */ public int getLabelVerticalWidth() { + if (mStyles.verticalLabelsVAlign == VerticalLabelsVAlign.ABOVE + || mStyles.verticalLabelsVAlign == VerticalLabelsVAlign.BELOW) { + return 0; + } return mLabelVerticalWidth == null || !isVerticalLabelsVisible() ? 0 : mLabelVerticalWidth; } @@ -1188,6 +1507,7 @@ public int getPadding() { */ public void setTextSize(float textSize) { mStyles.textSize = textSize; + reloadStyles(); } /** @@ -1210,12 +1530,20 @@ public void setVerticalLabelsColor(int verticalLabelsColor) { public void setHorizontalLabelsColor(int horizontalLabelsColor) { mStyles.horizontalLabelsColor = horizontalLabelsColor; } + + /** + * @param horizontalLabelsAngle the angle of the horizontal labels in degrees + */ + public void setHorizontalLabelsAngle(int horizontalLabelsAngle) { + mStyles.horizontalLabelsAngle = horizontalLabelsAngle; + } /** * @param gridColor the color of the grid lines */ public void setGridColor(int gridColor) { mStyles.gridColor = gridColor; + reloadStyles(); } /** @@ -1465,4 +1793,21 @@ public int getLabelsSpace() { public void setLabelsSpace(int labelsSpace) { mStyles.labelsSpace = labelsSpace; } + + + /** + * set horizontal label align + * @param align + */ + public void setVerticalLabelsVAlign(VerticalLabelsVAlign align){ + mStyles.verticalLabelsVAlign = align; + } + + /** + * Get horizontal label align + * @return align + */ + public VerticalLabelsVAlign getVerticalLabelsVAlign(){ + return mStyles.verticalLabelsVAlign; + } } diff --git a/src/main/java/com/jjoe64/graphview/LabelFormatter.java b/src/main/java/com/jjoe64/graphview/LabelFormatter.java index 80aad2990..a1f4d9c1e 100644 --- a/src/main/java/com/jjoe64/graphview/LabelFormatter.java +++ b/src/main/java/com/jjoe64/graphview/LabelFormatter.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview; diff --git a/src/main/java/com/jjoe64/graphview/LegendRenderer.java b/src/main/java/com/jjoe64/graphview/LegendRenderer.java index db901577f..61ea019b2 100644 --- a/src/main/java/com/jjoe64/graphview/LegendRenderer.java +++ b/src/main/java/com/jjoe64/graphview/LegendRenderer.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview; @@ -151,6 +148,15 @@ public void resetStyles() { cachedLegendWidth = 0; } + protected List getAllSeries() { + List allSeries = new ArrayList(); + allSeries.addAll(mGraphView.getSeries()); + if (mGraphView.mSecondScale != null) { + allSeries.addAll(mGraphView.getSecondScale().getSeries()); + } + return allSeries; + } + /** * draws the legend if it is visible * @@ -164,11 +170,7 @@ public void draw(Canvas canvas) { int shapeSize = (int) (mStyles.textSize*0.8d); - List allSeries = new ArrayList(); - allSeries.addAll(mGraphView.getSeries()); - if (mGraphView.mSecondScale != null) { - allSeries.addAll(mGraphView.getSecondScale().getSeries()); - } + List allSeries = getAllSeries(); // width int legendWidth = mStyles.width; diff --git a/src/main/java/com/jjoe64/graphview/RectD.java b/src/main/java/com/jjoe64/graphview/RectD.java new file mode 100644 index 000000000..ebd30fb32 --- /dev/null +++ b/src/main/java/com/jjoe64/graphview/RectD.java @@ -0,0 +1,55 @@ +/** + * GraphView + * Copyright 2016 Jonas Gehring + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jjoe64.graphview; + +import android.graphics.RectF; + +/** + * Created by jonas on 05.06.16. + */ +public class RectD { + public double left; + public double right; + public double top; + public double bottom; + + public RectD() { + } + + public RectD(double lLeft, double lTop, double lRight, double lBottom) { + set(lLeft, lTop, lRight, lBottom); + } + + public double width() { + return right-left; + } + + public double height() { + return bottom-top; + } + + public void set(double lLeft, double lTop, double lRight, double lBottom) { + left = lLeft; + right = lRight; + top = lTop; + bottom = lBottom; + } + + public RectF toRectF() { + return new RectF((float) left, (float) top, (float) right, (float) bottom); + } +} diff --git a/src/main/java/com/jjoe64/graphview/SecondScale.java b/src/main/java/com/jjoe64/graphview/SecondScale.java index 1fff8ee8a..6dede5ecf 100644 --- a/src/main/java/com/jjoe64/graphview/SecondScale.java +++ b/src/main/java/com/jjoe64/graphview/SecondScale.java @@ -1,24 +1,24 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview; +import android.graphics.Canvas; +import android.graphics.Paint; + import com.jjoe64.graphview.series.Series; import java.util.ArrayList; @@ -37,9 +37,9 @@ */ public class SecondScale { /** - * reference to the viewport of the graph + * reference to the graph */ - protected final Viewport mViewport; + protected final GraphView mGraph; /** * array of series for the second @@ -56,14 +56,11 @@ public class SecondScale { private boolean mYAxisBoundsManual = true; /** - * min y value for the y axis bounds + * */ - private double mMinY; + protected RectD mCompleteRange = new RectD(); - /** - * max y value for the y axis bounds - */ - private double mMaxY; + protected RectD mCurrentViewport = new RectD(); /** * label formatter for the y labels @@ -71,17 +68,39 @@ public class SecondScale { */ protected LabelFormatter mLabelFormatter; + protected double mReferenceY = Double.NaN; + + /** + * the paint to draw axis titles + */ + private Paint mPaintAxisTitle; + + /** + * the title of the vertical axis + */ + private String mVerticalAxisTitle; + + /** + * font size of the vertical axis title + */ + public float mVerticalAxisTitleTextSize; + + /** + * font color of the vertical axis title + */ + public int mVerticalAxisTitleColor; + /** * creates the second scale. * normally you do not call this contructor. * Use {@link com.jjoe64.graphview.GraphView#getSecondScale()} * in order to get the instance. */ - SecondScale(Viewport viewport) { - mViewport = viewport; + SecondScale(GraphView graph) { + mGraph = graph; mSeries = new ArrayList(); mLabelFormatter = new DefaultLabelFormatter(); - mLabelFormatter.setViewport(mViewport); + mLabelFormatter.setViewport(mGraph.getViewport()); } /** @@ -92,7 +111,9 @@ public class SecondScale { * @param s the series */ public void addSeries(Series s) { + s.onGraphViewAttached(mGraph); mSeries.add(s); + mGraph.onDataChanged(false, false); } //public void setYAxisBoundsManual(boolean mYAxisBoundsManual) { @@ -105,7 +126,8 @@ public void addSeries(Series s) { * @param d min y value */ public void setMinY(double d) { - mMinY = d; + mReferenceY = d; + mCurrentViewport.bottom = d; } /** @@ -114,7 +136,7 @@ public void setMinY(double d) { * @param d max y value */ public void setMaxY(double d) { - mMaxY = d; + mCurrentViewport.top = d; } /** @@ -127,15 +149,15 @@ public List getSeries() { /** * @return min y bound */ - public double getMinY() { - return mMinY; + public double getMinY(boolean completeRange) { + return completeRange ? mCompleteRange.bottom : mCurrentViewport.bottom; } /** * @return max y bound */ - public double getMaxY() { - return mMaxY; + public double getMaxY(boolean completeRange) { + return completeRange ? mCompleteRange.top : mCurrentViewport.top; } /** @@ -160,6 +182,141 @@ public LabelFormatter getLabelFormatter() { */ public void setLabelFormatter(LabelFormatter formatter) { mLabelFormatter = formatter; - mLabelFormatter.setViewport(mViewport); + mLabelFormatter.setViewport(mGraph.getViewport()); + } + + /** + * Removes all series of the graph. + */ + public void removeAllSeries() { + mSeries.clear(); + mGraph.onDataChanged(false, false); + } + + /** + * Remove a specific series of the + * second scale. + * + * @param series + */ + public void removeSeries(Series series) { + mSeries.remove(series); + mGraph.onDataChanged(false, false); + } + + /** + * caches the complete range (minX, maxX, minY, maxY) + * by iterating all series and all datapoints and + * stores it into #mCompleteRange + * + * for the x-range it will respect the series on the + * second scale - not for y-values + */ + public void calcCompleteRange() { + List series = getSeries(); + mCompleteRange.set(0d, 0d, 0d, 0d); + if (!series.isEmpty() && !series.get(0).isEmpty()) { + double d = series.get(0).getLowestValueX(); + for (Series s : series) { + if (!s.isEmpty() && d > s.getLowestValueX()) { + d = s.getLowestValueX(); + } + } + mCompleteRange.left = d; + + d = series.get(0).getHighestValueX(); + for (Series s : series) { + if (!s.isEmpty() && d < s.getHighestValueX()) { + d = s.getHighestValueX(); + } + } + mCompleteRange.right = d; + + if (!series.isEmpty() && !series.get(0).isEmpty()) { + d = series.get(0).getLowestValueY(); + for (Series s : series) { + if (!s.isEmpty() && d > s.getLowestValueY()) { + d = s.getLowestValueY(); + } + } + mCompleteRange.bottom = d; + + d = series.get(0).getHighestValueY(); + for (Series s : series) { + if (!s.isEmpty() && d < s.getHighestValueY()) { + d = s.getHighestValueY(); + } + } + mCompleteRange.top = d; + } + } + } + + /** + * @return the title of the vertical axis + */ + public String getVerticalAxisTitle() { + return mVerticalAxisTitle; + } + + /** + * @param mVerticalAxisTitle the title of the vertical axis + */ + public void setVerticalAxisTitle(String mVerticalAxisTitle) { + if(mPaintAxisTitle==null) { + mPaintAxisTitle = new Paint(); + mPaintAxisTitle.setTextSize(getVerticalAxisTitleTextSize()); + mPaintAxisTitle.setTextAlign(Paint.Align.CENTER); + } + this.mVerticalAxisTitle = mVerticalAxisTitle; + } + + /** + * @return font size of the vertical axis title + */ + public float getVerticalAxisTitleTextSize() { + if (getVerticalAxisTitle() == null || getVerticalAxisTitle().length() == 0) { + return 0; + } + return mVerticalAxisTitleTextSize; + } + + /** + * @param verticalAxisTitleTextSize font size of the vertical axis title + */ + public void setVerticalAxisTitleTextSize(float verticalAxisTitleTextSize) { + mVerticalAxisTitleTextSize = verticalAxisTitleTextSize; + } + + /** + * @return font color of the vertical axis title + */ + public int getVerticalAxisTitleColor() { + return mVerticalAxisTitleColor; + } + + /** + * @param verticalAxisTitleColor font color of the vertical axis title + */ + public void setVerticalAxisTitleColor(int verticalAxisTitleColor) { + mVerticalAxisTitleColor = verticalAxisTitleColor; + } + + /** + * draws the vertical axis title if + * it is set + * @param canvas canvas + */ + protected void drawVerticalAxisTitle(Canvas canvas) { + if (mVerticalAxisTitle != null && mVerticalAxisTitle.length() > 0) { + mPaintAxisTitle.setColor(getVerticalAxisTitleColor()); + mPaintAxisTitle.setTextSize(getVerticalAxisTitleTextSize()); + float x = canvas.getWidth() - getVerticalAxisTitleTextSize()/2; + float y = canvas.getHeight() / 2; + canvas.save(); + canvas.rotate(-90, x, y); + canvas.drawText(mVerticalAxisTitle, x, y, mPaintAxisTitle); + canvas.restore(); + } } } diff --git a/src/main/java/com/jjoe64/graphview/UniqueLegendRenderer.java b/src/main/java/com/jjoe64/graphview/UniqueLegendRenderer.java new file mode 100644 index 000000000..d0289bd78 --- /dev/null +++ b/src/main/java/com/jjoe64/graphview/UniqueLegendRenderer.java @@ -0,0 +1,36 @@ +package com.jjoe64.graphview; + +import android.util.Pair; + +import com.jjoe64.graphview.series.Series; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A LegendRenderer that renders items with the same name and color only once in the legend + * Created by poseidon on 27.02.18. + */ +public class UniqueLegendRenderer extends LegendRenderer { + /** + * creates legend renderer + * + * @param graphView regarding graphview + */ + public UniqueLegendRenderer(GraphView graphView) { + super(graphView); + } + + @Override + protected List getAllSeries() { + List originalSeries = super.getAllSeries(); + List distinctSeries = new ArrayList(); + Set> uniqueSeriesKeys = new HashSet>(); + for(Series series : originalSeries) + if(uniqueSeriesKeys.add(new Pair(series.getColor(), series.getTitle()))) + distinctSeries.add(series); + return distinctSeries; + } +} diff --git a/src/main/java/com/jjoe64/graphview/ValueDependentColor.java b/src/main/java/com/jjoe64/graphview/ValueDependentColor.java index a02f22a21..0d18d5f7e 100644 --- a/src/main/java/com/jjoe64/graphview/ValueDependentColor.java +++ b/src/main/java/com/jjoe64/graphview/ValueDependentColor.java @@ -1,23 +1,19 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ - package com.jjoe64.graphview; import com.jjoe64.graphview.series.DataPointInterface; diff --git a/src/main/java/com/jjoe64/graphview/Viewport.java b/src/main/java/com/jjoe64/graphview/Viewport.java index 9a8ec6c7a..ce674cce8 100644 --- a/src/main/java/com/jjoe64/graphview/Viewport.java +++ b/src/main/java/com/jjoe64/graphview/Viewport.java @@ -1,40 +1,36 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.RectF; -import android.support.v4.view.ViewCompat; -import android.support.v4.widget.EdgeEffectCompat; +import androidx.core.view.ViewCompat; +import androidx.core.widget.EdgeEffectCompat; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.widget.OverScroller; -import com.jjoe64.graphview.compat.OverScrollerCompat; import com.jjoe64.graphview.series.DataPointInterface; import com.jjoe64.graphview.series.Series; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -51,6 +47,72 @@ * @author jjoe64 */ public class Viewport { + /** + * this reference value is used to generate the + * vertical labels. It is used when the y axis bounds + * is set manual and humanRoundingY=false. it will be the minValueY value. + */ + protected double referenceY = Double.NaN; + + /** + * this reference value is used to generate the + * horizontal labels. It is used when the x axis bounds + * is set manual and humanRoundingX=false. it will be the minValueX value. + */ + protected double referenceX = Double.NaN; + + /** + * flag whether the vertical scaling is activated + */ + protected boolean scalableY; + + /** + * minimal viewport used for scaling and scrolling. + * this is used if the data that is available is + * less then the viewport that we want to be able to display. + * + * Double.NaN to disable this value + */ + private RectD mMinimalViewport = new RectD(Double.NaN, Double.NaN, Double.NaN, Double.NaN); + + /** + * the reference number to generate the labels + * @return by default 0, only when manual bounds and no human rounding + * is active, the min x value is returned + */ + protected double getReferenceX() { + // if the bounds is manual then we take the + // original manual min y value as reference + if (isXAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRoundingX()) { + if (Double.isNaN(referenceX)) { + referenceX = getMinX(false); + } + return referenceX; + } else { + // starting from 0 so that the steps have nice numbers + return 0; + } + } + + /** + * listener to notify when x bounds changed after + * scaling or scrolling. + * This can be used to load more detailed data. + */ + public interface OnXAxisBoundsChangedListener { + /** + * Called after scaling or scrolling with + * the new bounds + * @param minX min x value + * @param maxX max x value + */ + void onXAxisBoundsChanged(double minX, double maxX, OnXAxisBoundsChangedListener.Reason reason); + + public enum Reason { + SCROLL, SCALE + } + } + /** * listener for the scale gesture */ @@ -63,21 +125,43 @@ public class Viewport { */ @Override public boolean onScale(ScaleGestureDetector detector) { - float viewportWidth = mCurrentViewport.width(); - float center = mCurrentViewport.left + viewportWidth / 2; - viewportWidth /= detector.getScaleFactor(); + // --- horizontal scaling --- + double viewportWidth = mCurrentViewport.width(); + + if (mMaxXAxisSize != 0) { + if (viewportWidth > mMaxXAxisSize) { + viewportWidth = mMaxXAxisSize; + } + } + + double center = mCurrentViewport.left + viewportWidth / 2; + + float scaleSpanX; + if (android.os.Build.VERSION.SDK_INT >= 11 && scalableY) { + scaleSpanX = detector.getCurrentSpanX()/detector.getPreviousSpanX(); + } else { + scaleSpanX = detector.getScaleFactor(); + } + + viewportWidth /= scaleSpanX; mCurrentViewport.left = center - viewportWidth / 2; mCurrentViewport.right = mCurrentViewport.left+viewportWidth; // viewportStart must not be < minX - float minX = (float) getMinX(true); + double minX = getMinX(true); + if (!Double.isNaN(mMinimalViewport.left)) { + minX = Math.min(minX, mMinimalViewport.left); + } if (mCurrentViewport.left < minX) { mCurrentViewport.left = minX; mCurrentViewport.right = mCurrentViewport.left+viewportWidth; } // viewportStart + viewportSize must not be > maxX - float maxX = (float) getMaxX(true); + double maxX = getMaxX(true); + if (!Double.isNaN(mMinimalViewport.right)) { + maxX = Math.max(maxX, mMinimalViewport.right); + } if (viewportWidth == 0) { mCurrentViewport.right = maxX; } @@ -94,7 +178,68 @@ public boolean onScale(ScaleGestureDetector detector) { } } - // adjust viewport, labels, etc. + + // --- vertical scaling --- + if (scalableY && android.os.Build.VERSION.SDK_INT >= 11 && detector.getCurrentSpanY() != 0f && detector.getPreviousSpanY() != 0f) { + boolean hasSecondScale = mGraphView.mSecondScale != null; + + double viewportHeight = mCurrentViewport.height()*-1; + + if (mMaxYAxisSize != 0) { + if (viewportHeight > mMaxYAxisSize) { + viewportHeight = mMaxYAxisSize; + } + } + + center = mCurrentViewport.bottom + viewportHeight / 2; + + viewportHeight /= detector.getCurrentSpanY()/detector.getPreviousSpanY(); + mCurrentViewport.bottom = center - viewportHeight / 2; + mCurrentViewport.top = mCurrentViewport.bottom+viewportHeight; + + // ignore bounds when second scale + if (!hasSecondScale) { + // viewportStart must not be < minY + double minY = getMinY(true); + if (!Double.isNaN(mMinimalViewport.bottom)) { + minY = Math.min(minY, mMinimalViewport.bottom); + } + if (mCurrentViewport.bottom < minY) { + mCurrentViewport.bottom = minY; + mCurrentViewport.top = mCurrentViewport.bottom+viewportHeight; + } + + // viewportStart + viewportSize must not be > maxY + double maxY = getMaxY(true); + if (!Double.isNaN(mMinimalViewport.top)) { + maxY = Math.max(maxY, mMinimalViewport.top); + } + if (viewportHeight == 0) { + mCurrentViewport.top = maxY; + } + overlap = mCurrentViewport.bottom + viewportHeight - maxY; + if (overlap > 0) { + // scroll left + if (mCurrentViewport.bottom-overlap > minY) { + mCurrentViewport.bottom -= overlap; + mCurrentViewport.top = mCurrentViewport.bottom+viewportHeight; + } else { + // maximal scale + mCurrentViewport.bottom = minY; + mCurrentViewport.top = maxY; + } + } + } else { + // ---- second scale --- + viewportHeight = mGraphView.mSecondScale.mCurrentViewport.height()*-1; + center = mGraphView.mSecondScale.mCurrentViewport.bottom + viewportHeight / 2; + viewportHeight /= detector.getCurrentSpanY()/detector.getPreviousSpanY(); + mGraphView.mSecondScale.mCurrentViewport.bottom = center - viewportHeight / 2; + mGraphView.mSecondScale.mCurrentViewport.top = mGraphView.mSecondScale.mCurrentViewport.bottom+viewportHeight; + } + } + + // adjustSteps viewport, labels, etc. mGraphView.onDataChanged(true, false); ViewCompat.postInvalidateOnAnimation(mGraphView); @@ -110,9 +255,12 @@ public boolean onScale(ScaleGestureDetector detector) { */ @Override public boolean onScaleBegin(ScaleGestureDetector detector) { + // cursor mode + if (mGraphView.isCursorMode()) { + return false; + } + if (mIsScalable) { - mScalingBeginWidth = mCurrentViewport.width(); - mScalingBeginLeft = mCurrentViewport.left; mScalingActive = true; return true; } else { @@ -122,7 +270,7 @@ public boolean onScaleBegin(ScaleGestureDetector detector) { /** * called when sacling ends - * This will re-adjust the viewport. + * This will re-adjustSteps the viewport. * * @param detector detector */ @@ -130,13 +278,10 @@ public boolean onScaleBegin(ScaleGestureDetector detector) { public void onScaleEnd(ScaleGestureDetector detector) { mScalingActive = false; - // re-adjust - mXAxisBoundsStatus = AxisBoundsStatus.READJUST_AFTER_SCALE; - - mScrollingReferenceX = Float.NaN; - - // adjust viewport, labels, etc. - mGraphView.onDataChanged(true, false); + // notify + if (mOnXAxisBoundsChangedListener != null) { + mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCALE); + } ViewCompat.postInvalidateOnAnimation(mGraphView); } @@ -149,11 +294,15 @@ public void onScaleEnd(ScaleGestureDetector detector) { = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { + // cursor mode + if (mGraphView.isCursorMode()) { + return true; + } + if (!mIsScrollable || mScalingActive) return false; // Initiates the decay phase of any active edge effects. releaseEdgeEffects(); - mScrollerStartViewport.set(mCurrentViewport); // Aborts any active scroll animations and invalidates. mScroller.forceFinished(true); ViewCompat.postInvalidateOnAnimation(mGraphView); @@ -162,11 +311,11 @@ public boolean onDown(MotionEvent e) { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - if (!mIsScrollable || mScalingActive) return false; - - if (Float.isNaN(mScrollingReferenceX)) { - mScrollingReferenceX = mCurrentViewport.left; + // cursor mode + if (mGraphView.isCursorMode()) { + return true; } + if (!mIsScrollable || mScalingActive) return false; // Scrolling uses math based on the viewport (as opposed to math using pixels). /** @@ -176,63 +325,120 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d * additional information about the viewport, see the comments for * {@link mCurrentViewport}. */ - float viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth(); - float viewportOffsetY = -distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight(); + double viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth(); + double viewportOffsetY = distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight(); - int completeWidth = (int)((mCompleteRange.width()/mCurrentViewport.width()) * (float) mGraphView.getGraphContentWidth()); - int completeHeight = (int)((mCompleteRange.height()/mCurrentViewport.height()) * (float) mGraphView.getGraphContentHeight()); + // respect minimal viewport + double completeRangeLeft = mCompleteRange.left; + if (!Double.isNaN(mMinimalViewport.left)) { + completeRangeLeft = Math.min(completeRangeLeft, mMinimalViewport.left); + } + double completeRangeRight = mCompleteRange.right; + if (!Double.isNaN(mMinimalViewport.right)) { + completeRangeRight = Math.max(completeRangeRight, mMinimalViewport.right); + } + double completeRangeWidth = completeRangeRight - completeRangeLeft; + + double completeRangeBottom = mCompleteRange.bottom; + if (!Double.isNaN(mMinimalViewport.bottom)) { + completeRangeBottom = Math.min(completeRangeBottom, mMinimalViewport.bottom); + } + double completeRangeTop = mCompleteRange.top; + if (!Double.isNaN(mMinimalViewport.top)) { + completeRangeTop = Math.max(completeRangeTop, mMinimalViewport.top); + } + double completeRangeHeight = completeRangeTop - completeRangeBottom; + + int completeWidth = (int)((completeRangeWidth/mCurrentViewport.width()) * (double) mGraphView.getGraphContentWidth()); + int completeHeight = (int)((completeRangeHeight/mCurrentViewport.height()) * (double) mGraphView.getGraphContentHeight()); int scrolledX = (int) (completeWidth - * (mCurrentViewport.left + viewportOffsetX - mCompleteRange.left) - / mCompleteRange.width()); + * (mCurrentViewport.left + viewportOffsetX - completeRangeLeft) + / completeRangeWidth); + int scrolledY = (int) (completeHeight - * (mCompleteRange.bottom - mCurrentViewport.bottom - viewportOffsetY) - / mCompleteRange.height()); - boolean canScrollX = mCurrentViewport.left > mCompleteRange.left - || mCurrentViewport.right < mCompleteRange.right; - boolean canScrollY = mCurrentViewport.bottom > mCompleteRange.bottom - || mCurrentViewport.top < mCompleteRange.top; + * (mCurrentViewport.bottom + viewportOffsetY - completeRangeBottom) + / completeRangeHeight*-1); + boolean canScrollX = mCurrentViewport.left > completeRangeLeft + || mCurrentViewport.right < completeRangeRight; + boolean canScrollY = mCurrentViewport.bottom > completeRangeBottom + || mCurrentViewport.top < completeRangeTop; + + boolean hasSecondScale = mGraphView.mSecondScale != null; + + // second scale + double viewportOffsetY2 = 0d; + if (hasSecondScale) { + viewportOffsetY2 = distanceY * mGraphView.mSecondScale.mCurrentViewport.height() / mGraphView.getGraphContentHeight(); + canScrollY |= mGraphView.mSecondScale.mCurrentViewport.bottom > mGraphView.mSecondScale.mCompleteRange.bottom + || mGraphView.mSecondScale.mCurrentViewport.top < mGraphView.mSecondScale.mCompleteRange.top; + } + + canScrollY &= scrollableY; if (canScrollX) { if (viewportOffsetX < 0) { - float tooMuch = mCurrentViewport.left+viewportOffsetX - mCompleteRange.left; + double tooMuch = mCurrentViewport.left+viewportOffsetX - completeRangeLeft; if (tooMuch < 0) { viewportOffsetX -= tooMuch; } } else { - float tooMuch = mCurrentViewport.right+viewportOffsetX - mCompleteRange.right; + double tooMuch = mCurrentViewport.right+viewportOffsetX - completeRangeRight; if (tooMuch > 0) { viewportOffsetX -= tooMuch; } } + mCurrentViewport.left += viewportOffsetX; mCurrentViewport.right += viewportOffsetX; + + // notify + if (mOnXAxisBoundsChangedListener != null) { + mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCROLL); + } } if (canScrollY) { - //mCurrentViewport.top += viewportOffsetX; - //mCurrentViewport.bottom -= viewportOffsetX; + // if we have the second axis we ignore the max/min range + if (!hasSecondScale) { + if (viewportOffsetY < 0) { + double tooMuch = mCurrentViewport.bottom+viewportOffsetY - completeRangeBottom; + if (tooMuch < 0) { + viewportOffsetY -= tooMuch; + } + } else { + double tooMuch = mCurrentViewport.top+viewportOffsetY - completeRangeTop; + if (tooMuch > 0) { + viewportOffsetY -= tooMuch; + } + } + } + + mCurrentViewport.top += viewportOffsetY; + mCurrentViewport.bottom += viewportOffsetY; + + // second scale + if (hasSecondScale) { + mGraphView.mSecondScale.mCurrentViewport.top += viewportOffsetY2; + mGraphView.mSecondScale.mCurrentViewport.bottom += viewportOffsetY2; + } } if (canScrollX && scrolledX < 0) { mEdgeEffectLeft.onPull(scrolledX / (float) mGraphView.getGraphContentWidth()); - mEdgeEffectLeftActive = true; } - if (canScrollY && scrolledY < 0) { + if (!hasSecondScale && canScrollY && scrolledY < 0) { mEdgeEffectBottom.onPull(scrolledY / (float) mGraphView.getGraphContentHeight()); - mEdgeEffectBottomActive = true; } if (canScrollX && scrolledX > completeWidth - mGraphView.getGraphContentWidth()) { mEdgeEffectRight.onPull((scrolledX - completeWidth + mGraphView.getGraphContentWidth()) / (float) mGraphView.getGraphContentWidth()); - mEdgeEffectRightActive = true; } - //if (canScrollY && scrolledY > mSurfaceSizeBuffer.y - mContentRect.height()) { - // mEdgeEffectTop.onPull((scrolledY - mSurfaceSizeBuffer.y + mContentRect.height()) - // / (float) mContentRect.height()); - // mEdgeEffectTopActive = true; - //} + if (!hasSecondScale && canScrollY && scrolledY > completeHeight - mGraphView.getGraphContentHeight()) { + mEdgeEffectTop.onPull((scrolledY - completeHeight + mGraphView.getGraphContentHeight()) + / (float) mGraphView.getGraphContentHeight()); + } - // adjust viewport, labels, etc. + // adjustSteps viewport, labels, etc. mGraphView.onDataChanged(true, false); ViewCompat.postInvalidateOnAnimation(mGraphView); @@ -265,13 +471,6 @@ public enum AxisBoundsStatus { */ AUTO_ADJUSTED, - /** - * this flags the status that a scale was - * done and the bounds has to be auto-adjusted - * afterwards. - */ - READJUST_AFTER_SCALE, - /** * means that the bounds are fix (manually) and * are not to be auto-adjusted. @@ -294,31 +493,33 @@ public enum AxisBoundsStatus { * left = minX, right = maxX * bottom = minY, top = maxY */ - protected RectF mCurrentViewport = new RectF(); + protected RectD mCurrentViewport = new RectD(); /** - * this holds the whole range of the data - * left = minX, right = maxX - * bottom = minY, top = maxY + * maximum allowed viewport size (horizontal) + * 0 means use the bounds of the actual data that is + * available */ - protected RectF mCompleteRange = new RectF(); + protected double mMaxXAxisSize = 0; /** - * flag whether scaling is currently active + * maximum allowed viewport size (vertical) + * 0 means use the bounds of the actual data that is + * available */ - protected boolean mScalingActive; + protected double mMaxYAxisSize = 0; /** - * stores the width of the viewport at the time - * of beginning of the scaling. + * this holds the whole range of the data + * left = minX, right = maxX + * bottom = minY, top = maxY */ - protected float mScalingBeginWidth; + protected RectD mCompleteRange = new RectD(); /** - * stores the viewport left at the time of - * beginning of the scaling. + * flag whether scaling is currently active */ - protected float mScalingBeginLeft; + protected boolean mScalingActive; /** * flag whether the viewport is scrollable @@ -330,6 +531,12 @@ public enum AxisBoundsStatus { */ private boolean mIsScalable; + /** + * flag whether the viewport is scalable + * on the Y axis + */ + private boolean scrollableY; + /** * gesture detector to detect scrolling */ @@ -366,62 +573,55 @@ public enum AxisBoundsStatus { private EdgeEffectCompat mEdgeEffectRight; /** - * not used - */ - private boolean mEdgeEffectTopActive; - - /** - * not used + * state of the x axis */ - private boolean mEdgeEffectBottomActive; + protected AxisBoundsStatus mXAxisBoundsStatus; /** - * glow effect when scrolling left + * state of the y axis */ - private boolean mEdgeEffectLeftActive; + protected AxisBoundsStatus mYAxisBoundsStatus; /** - * glow effect when scrolling right + * flag whether the x axis bounds are manual */ - private boolean mEdgeEffectRightActive; + private boolean mXAxisBoundsManual; /** - * stores the viewport at the time of - * the beginning of scaling + * flag whether the y axis bounds are manual */ - private RectF mScrollerStartViewport = new RectF(); + private boolean mYAxisBoundsManual; /** - * stores the viewport left value at the - * time of beginning of the scrolling + * background color of the viewport area + * it is recommended to use a semi-transparent color */ - protected float mScrollingReferenceX = Float.NaN; + private int mBackgroundColor; /** - * state of the x axis + * listener to notify when x bounds changed after + * scaling or scrolling. + * This can be used to load more detailed data. */ - private AxisBoundsStatus mXAxisBoundsStatus; + protected OnXAxisBoundsChangedListener mOnXAxisBoundsChangedListener; /** - * state of the y axis + * optional draw a border between the labels + * and the viewport */ - private AxisBoundsStatus mYAxisBoundsStatus; + private boolean mDrawBorder; /** - * flag whether the x axis bounds are manual + * color of the border + * @see #setDrawBorder(boolean) */ - private boolean mXAxisBoundsManual; + private Integer mBorderColor; /** - * flag whether the y axis bounds are manual + * custom paint to use for the border + * @see #setDrawBorder(boolean) */ - private boolean mYAxisBoundsManual; - - /** - * background color of the viewport area - * it is recommended to use a semi-transparent color - */ - private int mBackgroundColor; + private Paint mBorderPaint; /** * creates the viewport @@ -454,6 +654,19 @@ public enum AxisBoundsStatus { public boolean onTouchEvent(MotionEvent event) { boolean b = mScaleGestureDetector.onTouchEvent(event); b |= mGestureDetector.onTouchEvent(event); + if (mGraphView.isCursorMode()) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mGraphView.getCursorMode().onDown(event); + b |= true; + } + if (event.getAction() == MotionEvent.ACTION_MOVE) { + mGraphView.getCursorMode().onMove(event); + b |= true; + } + if (event.getAction() == MotionEvent.ACTION_UP) { + b |= mGraphView.getCursorMode().onUp(event); + } + } return b; } @@ -513,42 +726,51 @@ public AxisBoundsStatus getYAxisBoundsStatus() { * caches the complete range (minX, maxX, minY, maxY) * by iterating all series and all datapoints and * stores it into #mCompleteRange + * + * for the x-range it will respect the series on the + * second scale - not for y-values */ public void calcCompleteRange() { List series = mGraphView.getSeries(); - mCompleteRange.set(0, 0, 0, 0); - if (!series.isEmpty() && !series.get(0).isEmpty()) { - double d = series.get(0).getLowestValueX(); - for (Series s : series) { + List seriesInclusiveSecondScale = new ArrayList<>(mGraphView.getSeries()); + if (mGraphView.mSecondScale != null) { + seriesInclusiveSecondScale.addAll(mGraphView.mSecondScale.getSeries()); + } + mCompleteRange.set(0d, 0d, 0d, 0d); + if (!seriesInclusiveSecondScale.isEmpty() && !seriesInclusiveSecondScale.get(0).isEmpty()) { + double d = seriesInclusiveSecondScale.get(0).getLowestValueX(); + for (Series s : seriesInclusiveSecondScale) { if (!s.isEmpty() && d > s.getLowestValueX()) { d = s.getLowestValueX(); } } - mCompleteRange.left = (float) d; + mCompleteRange.left = d; - d = series.get(0).getHighestValueX(); - for (Series s : series) { + d = seriesInclusiveSecondScale.get(0).getHighestValueX(); + for (Series s : seriesInclusiveSecondScale) { if (!s.isEmpty() && d < s.getHighestValueX()) { d = s.getHighestValueX(); } } - mCompleteRange.right = (float) d; + mCompleteRange.right = d; - d = series.get(0).getLowestValueY(); - for (Series s : series) { - if (!s.isEmpty() && d > s.getLowestValueY()) { - d = s.getLowestValueY(); + if (!series.isEmpty() && !series.get(0).isEmpty()) { + d = series.get(0).getLowestValueY(); + for (Series s : series) { + if (!s.isEmpty() && d > s.getLowestValueY()) { + d = s.getLowestValueY(); + } } - } - mCompleteRange.bottom = (float) d; + mCompleteRange.bottom = d; - d = series.get(0).getHighestValueY(); - for (Series s : series) { - if (!s.isEmpty() && d < s.getHighestValueY()) { - d = s.getHighestValueY(); + d = series.get(0).getHighestValueY(); + for (Series s : series) { + if (!s.isEmpty() && d < s.getHighestValueY()) { + d = s.getHighestValueY(); + } } + mCompleteRange.top = d; } - mCompleteRange.top = (float) d; } // calc current viewport bounds @@ -580,7 +802,9 @@ public void calcCompleteRange() { } } - mCurrentViewport.bottom = (float) d; + if (d != Double.MAX_VALUE) { + mCurrentViewport.bottom = d; + } // highest d = Double.MIN_VALUE; @@ -593,7 +817,10 @@ public void calcCompleteRange() { } } } - mCurrentViewport.top = (float) d; + + if (d != Double.MIN_VALUE) { + mCurrentViewport.top = d; + } } // fixes blank screen when range is zero @@ -608,9 +835,9 @@ public void calcCompleteRange() { */ public double getMinX(boolean completeRange) { if (completeRange) { - return (double) mCompleteRange.left; + return mCompleteRange.left; } else { - return (double) mCurrentViewport.left; + return mCurrentViewport.left; } } @@ -621,7 +848,7 @@ public double getMinX(boolean completeRange) { */ public double getMaxX(boolean completeRange) { if (completeRange) { - return (double) mCompleteRange.right; + return mCompleteRange.right; } else { return mCurrentViewport.right; } @@ -634,7 +861,7 @@ public double getMaxX(boolean completeRange) { */ public double getMinY(boolean completeRange) { if (completeRange) { - return (double) mCompleteRange.bottom; + return mCompleteRange.bottom; } else { return mCurrentViewport.bottom; } @@ -647,7 +874,7 @@ public double getMinY(boolean completeRange) { */ public double getMaxY(boolean completeRange) { if (completeRange) { - return (double) mCompleteRange.top; + return mCompleteRange.top; } else { return mCurrentViewport.top; } @@ -660,7 +887,7 @@ public double getMaxY(boolean completeRange) { * @param y max / highest value */ public void setMaxY(double y) { - mCurrentViewport.top = (float) y; + mCurrentViewport.top = y; } /** @@ -670,7 +897,7 @@ public void setMaxY(double y) { * @param y min / lowest value */ public void setMinY(double y) { - mCurrentViewport.bottom = (float) y; + mCurrentViewport.bottom = y; } /** @@ -680,7 +907,7 @@ public void setMinY(double y) { * @param x max / highest value */ public void setMaxX(double x) { - mCurrentViewport.right = (float) x; + mCurrentViewport.right = x; } /** @@ -690,18 +917,17 @@ public void setMaxX(double x) { * @param x min / lowest value */ public void setMinX(double x) { - mCurrentViewport.left = (float) x; + mCurrentViewport.left = x; } /** * release the glowing effects */ private void releaseEdgeEffects() { - mEdgeEffectLeftActive - = mEdgeEffectRightActive - = false; mEdgeEffectLeft.onRelease(); mEdgeEffectRight.onRelease(); + mEdgeEffectTop.onRelease(); + mEdgeEffectBottom.onRelease(); } /** @@ -714,7 +940,6 @@ private void fling(int velocityX, int velocityY) { velocityY = 0; releaseEdgeEffects(); // Flings use math in pixels (as opposed to math based on the viewport). - mScrollerStartViewport.set(mCurrentViewport); int maxX = (int)((mCurrentViewport.width()/mCompleteRange.width())*(float)mGraphView.getGraphContentWidth()) - mGraphView.getGraphContentWidth(); int maxY = (int)((mCurrentViewport.height()/mCompleteRange.height())*(float)mGraphView.getGraphContentHeight()) - mGraphView.getGraphContentHeight(); int startX = (int)((mCurrentViewport.left - mCompleteRange.left)/mCompleteRange.width())*maxX; @@ -736,73 +961,6 @@ private void fling(int velocityX, int velocityY) { * not used currently */ public void computeScroll() { - if (true) return; - - boolean needsInvalidate = false; - - if (mScroller.computeScrollOffset()) { - // The scroller isn't finished, meaning a fling or programmatic pan operation is - // currently active. - - int completeWidth = (int)((mCompleteRange.width()/mCurrentViewport.width()) * (float) mGraphView.getGraphContentWidth()); - int completeHeight = (int)((mCompleteRange.height()/mCurrentViewport.height()) * (float) mGraphView.getGraphContentHeight()); - - int currX = mScroller.getCurrX(); - int currY = mScroller.getCurrY(); - - boolean canScrollX = mCurrentViewport.left > mCompleteRange.left - || mCurrentViewport.right < mCompleteRange.right; - boolean canScrollY = mCurrentViewport.bottom > mCompleteRange.bottom - || mCurrentViewport.top < mCompleteRange.top; - - if (canScrollX - && currX < 0 - && mEdgeEffectLeft.isFinished() - && !mEdgeEffectLeftActive) { - mEdgeEffectLeft.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller)); - mEdgeEffectLeftActive = true; - needsInvalidate = true; - } else if (canScrollX - && currX > (completeWidth - mGraphView.getGraphContentWidth()) - && mEdgeEffectRight.isFinished() - && !mEdgeEffectRightActive) { - mEdgeEffectRight.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller)); - mEdgeEffectRightActive = true; - needsInvalidate = true; - } - - if (canScrollY - && currY < 0 - && mEdgeEffectTop.isFinished() - && !mEdgeEffectTopActive) { - mEdgeEffectTop.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller)); - mEdgeEffectTopActive = true; - needsInvalidate = true; - } else if (canScrollY - && currY > (completeHeight - mGraphView.getGraphContentHeight()) - && mEdgeEffectBottom.isFinished() - && !mEdgeEffectBottomActive) { - mEdgeEffectBottom.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller)); - mEdgeEffectBottomActive = true; - needsInvalidate = true; - } - - float currXRange = mCompleteRange.left + mCompleteRange.width() - * currX / completeWidth; - float currYRange = mCompleteRange.top - mCompleteRange.height() - * currY / completeHeight; - - float currWidth = mCurrentViewport.width(); - float currHeight = mCurrentViewport.height(); - mCurrentViewport.left = currXRange; - mCurrentViewport.right = currXRange + currWidth; - //mCurrentViewport.bottom = currYRange; - //mCurrentViewport.top = currYRange + currHeight; - } - - if (needsInvalidate) { - ViewCompat.postInvalidateOnAnimation(mGraphView); - } } /** @@ -826,16 +984,16 @@ private void drawEdgeEffectsUnclipped(Canvas canvas) { canvas.restoreToCount(restoreCount); } - //if (!mEdgeEffectBottom.isFinished()) { - // final int restoreCount = canvas.save(); - // canvas.translate(2 * mContentRect.left - mContentRect.right, mContentRect.bottom); - // canvas.rotate(180, mContentRect.width(), 0); - // mEdgeEffectBottom.setSize(mContentRect.width(), mContentRect.height()); - // if (mEdgeEffectBottom.draw(canvas)) { - // needsInvalidate = true; - // } - // canvas.restoreToCount(restoreCount); - //} + if (!mEdgeEffectBottom.isFinished()) { + final int restoreCount = canvas.save(); + canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight()); + canvas.rotate(180, mGraphView.getGraphContentWidth()/2, 0); + mEdgeEffectBottom.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight()); + if (mEdgeEffectBottom.draw(canvas)) { + needsInvalidate = true; + } + canvas.restoreToCount(restoreCount); + } if (!mEdgeEffectLeft.isFinished()) { final int restoreCount = canvas.save(); @@ -883,6 +1041,39 @@ public void drawFirst(Canvas c) { mPaint ); } + if (mDrawBorder) { + Paint p; + if (mBorderPaint != null) { + p = mBorderPaint; + } else { + p = mPaint; + p.setColor(getBorderColor()); + } + c.drawLine( + mGraphView.getGraphContentLeft(), + mGraphView.getGraphContentTop(), + mGraphView.getGraphContentLeft(), + mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(), + p + ); + c.drawLine( + mGraphView.getGraphContentLeft(), + mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(), + mGraphView.getGraphContentLeft()+mGraphView.getGraphContentWidth(), + mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(), + p + ); + // on the right side if we have second scale + if (mGraphView.mSecondScale != null) { + c.drawLine( + mGraphView.getGraphContentLeft()+mGraphView.getGraphContentWidth(), + mGraphView.getGraphContentTop(), + mGraphView.getGraphContentLeft()+mGraphView.getGraphContentWidth(), + mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(), + p + ); + } + } } /** @@ -983,13 +1174,180 @@ public void setYAxisBoundsManual(boolean mYAxisBoundsManual) { */ public void scrollToEnd() { if (mXAxisBoundsManual) { - float size = mCurrentViewport.width(); + double size = mCurrentViewport.width(); mCurrentViewport.right = mCompleteRange.right; mCurrentViewport.left = mCompleteRange.right - size; - mScrollingReferenceX = Float.NaN; mGraphView.onDataChanged(true, false); } else { Log.w("GraphView", "scrollToEnd works only with manual x axis bounds"); } } + + /** + * @return the listener when there is one registered. + */ + public OnXAxisBoundsChangedListener getOnXAxisBoundsChangedListener() { + return mOnXAxisBoundsChangedListener; + } + + /** + * set a listener to notify when x bounds changed after + * scaling or scrolling. + * This can be used to load more detailed data. + * + * @param l the listener to use + */ + public void setOnXAxisBoundsChangedListener(OnXAxisBoundsChangedListener l) { + mOnXAxisBoundsChangedListener = l; + } + + /** + * optional draw a border between the labels + * and the viewport + * + * @param drawBorder true to draw the border + */ + public void setDrawBorder(boolean drawBorder) { + this.mDrawBorder = drawBorder; + } + + /** + * the border color used. will be ignored when + * a custom paint is set. + * + * @see #setDrawBorder(boolean) + * @return border color. by default the grid color is used + */ + public int getBorderColor() { + if (mBorderColor != null) { + return mBorderColor; + } + return mGraphView.getGridLabelRenderer().getGridColor(); + } + + /** + * the border color used. will be ignored when + * a custom paint is set. + * + * @param borderColor null to reset + */ + public void setBorderColor(Integer borderColor) { + this.mBorderColor = borderColor; + } + + /** + * custom paint to use for the border. border color + * will be ignored + * + * @see #setDrawBorder(boolean) + * @param borderPaint + */ + public void setBorderPaint(Paint borderPaint) { + this.mBorderPaint = borderPaint; + } + + /** + * activate/deactivate the vertical scrolling + * + * @param scrollableY true to activate + */ + public void setScrollableY(boolean scrollableY) { + this.scrollableY = scrollableY; + } + + /** + * the reference number to generate the labels + * @return by default 0, only when manual bounds and no human rounding + * is active, the min y value is returned + */ + protected double getReferenceY() { + // if the bounds is manual then we take the + // original manual min y value as reference + if (isYAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRoundingY()) { + if (Double.isNaN(referenceY)) { + referenceY = getMinY(false); + } + return referenceY; + } else { + // starting from 0 so that the steps have nice numbers + return 0; + } + } + + /** + * activate or deactivate the vertical zooming/scaling functionallity. + * This will automatically activate the vertical scrolling and the + * horizontal scaling/scrolling feature. + * + * @param scalableY true to activate + */ + public void setScalableY(boolean scalableY) { + if (scalableY) { + this.scrollableY = true; + setScalable(true); + + if (android.os.Build.VERSION.SDK_INT < 11) { + Log.w("GraphView", "Vertical scaling requires minimum Android 3.0 (API Level 11)"); + } + } + this.scalableY = scalableY; + } + + /** + * maximum allowed viewport size (horizontal) + * 0 means use the bounds of the actual data that is + * available + */ + public double getMaxXAxisSize() { + return mMaxXAxisSize; + } + + /** + * maximum allowed viewport size (vertical) + * 0 means use the bounds of the actual data that is + * available + */ + public double getMaxYAxisSize() { + return mMaxYAxisSize; + } + + /** + * Set the max viewport size (horizontal) + * This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it + * could force the user to only be able to see 2 hours of data at a time. + * Default value is 0 (disabled) + * + * @param mMaxXAxisViewportSize maximum size of viewport + */ + public void setMaxXAxisSize(double mMaxXAxisViewportSize) { + this.mMaxXAxisSize = mMaxXAxisViewportSize; + } + + /** + * Set the max viewport size (vertical) + * This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it + * could force the user to only be able to see 2 hours of data at a time. + * Default value is 0 (disabled) + * + * @param mMaxYAxisViewportSize maximum size of viewport + */ + public void setMaxYAxisSize(double mMaxYAxisViewportSize) { + this.mMaxYAxisSize = mMaxYAxisViewportSize; + } + + /** + * minimal viewport used for scaling and scrolling. + * this is used if the data that is available is + * less then the viewport that we want to be able to display. + * + * if Double.NaN is used, then this value is ignored + * + * @param minX + * @param maxX + * @param minY + * @param maxY + */ + public void setMinimalViewport(double minX, double maxX, double minY, double maxY) { + mMinimalViewport.set(minX, maxY, maxX, minY); + } } diff --git a/src/main/java/com/jjoe64/graphview/compat/OverScrollerCompat.java b/src/main/java/com/jjoe64/graphview/compat/OverScrollerCompat.java index 20b497c91..b0e73b6da 100644 --- a/src/main/java/com/jjoe64/graphview/compat/OverScrollerCompat.java +++ b/src/main/java/com/jjoe64/graphview/compat/OverScrollerCompat.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.compat; diff --git a/src/main/java/com/jjoe64/graphview/helper/DateAsXAxisLabelFormatter.java b/src/main/java/com/jjoe64/graphview/helper/DateAsXAxisLabelFormatter.java index 1e9fa3cff..f429c6cbb 100644 --- a/src/main/java/com/jjoe64/graphview/helper/DateAsXAxisLabelFormatter.java +++ b/src/main/java/com/jjoe64/graphview/helper/DateAsXAxisLabelFormatter.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.helper; diff --git a/src/main/java/com/jjoe64/graphview/helper/GraphViewXML.java b/src/main/java/com/jjoe64/graphview/helper/GraphViewXML.java index d036805bc..ba6c8f664 100644 --- a/src/main/java/com/jjoe64/graphview/helper/GraphViewXML.java +++ b/src/main/java/com/jjoe64/graphview/helper/GraphViewXML.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.helper; diff --git a/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java b/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java index 4b38dbf4c..3bb4156ca 100644 --- a/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java +++ b/src/main/java/com/jjoe64/graphview/helper/StaticLabelsFormatter.java @@ -1,3 +1,19 @@ +/** + * GraphView + * Copyright 2016 Jonas Gehring + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.jjoe64.graphview.helper; import com.jjoe64.graphview.DefaultLabelFormatter; diff --git a/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java index 8db7bac94..d8ab9ca13 100644 --- a/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java +++ b/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java @@ -1,30 +1,30 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.series; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; +import androidx.core.view.ViewCompat; import android.util.Log; +import android.view.animation.AccelerateInterpolator; import com.jjoe64.graphview.GraphView; +import com.jjoe64.graphview.RectD; import com.jjoe64.graphview.ValueDependentColor; import java.util.HashMap; @@ -40,18 +40,33 @@ * @author jjoe64 */ public class BarGraphSeries extends BaseSeries { + private static final long ANIMATION_DURATION = 333; + /** * paint to do drawing on canvas */ private Paint mPaint; + /** + * custom paint that can be used. + * this will ignore the value dependent color. + */ + private Paint mCustomPaint; + /** * spacing between the bars in percentage. * 0 => no spacing - * 100 => the space bewetten the bars is as big as the bars itself + * 100 => the space between the bars is as big as the bars itself */ private int mSpacing; + /** + * width of a data point + * 0 => no prior knowledge of sampling period, interval between bars will be calculated automatically + * >0 => value is the total distance from one bar to another + */ + private double mDataWidth; + /** * callback to generate value-dependent colors * of the bars @@ -82,7 +97,33 @@ public class BarGraphSeries extends BaseSeries * stores the coordinates of the bars to * trigger tap on series events. */ - private Map mDataPoints = new HashMap(); + private Map mDataPoints = new HashMap(); + + /** + * flag for animated rendering + */ + private boolean mAnimated; + + /** + * store the last value that was animated + */ + private double mLastAnimatedValue = Double.NaN; + + /** + * time of start animation + */ + private long mAnimationStart; + + /** + * animation interpolator + */ + private AccelerateInterpolator mAnimationInterpolator; + + /** + * frame number of animation to avoid lagging + */ + private int mAnimationStartFrameNo; + /** * creates bar series without any data @@ -94,11 +135,13 @@ public BarGraphSeries() { /** * creates bar series with data * - * @param data values + * @param data data points + * important: array has to be sorted from lowest x-value to the highest */ public BarGraphSeries(E[] data) { super(data); mPaint = new Paint(); + mAnimationInterpolator = new AccelerateInterpolator(2f); } /** @@ -116,6 +159,8 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { } mPaint.setTextSize(mValuesOnTopSize); + resetDataPoints(); + // get data double maxX = graphView.getViewport().getMaxX(false); double minX = graphView.getViewport().getMinX(false); @@ -123,8 +168,8 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { double maxY; double minY; if (isSecondScale) { - maxY = graphView.getSecondScale().getMaxY(); - minY = graphView.getSecondScale().getMinY(); + maxY = graphView.getSecondScale().getMaxY(false); + minY = graphView.getSecondScale().getMinY(false); } else { maxY = graphView.getViewport().getMaxY(false); minY = graphView.getViewport().getMinY(false); @@ -166,16 +211,22 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { return; } - Double lastVal = null; double minGap = 0; - for(Double curVal: xVals) { - if(lastVal != null) { - double curGap = Math.abs(curVal - lastVal); - if (minGap == 0 || (curGap > 0 && curGap < minGap)) { - minGap = curGap; + + if(mDataWidth > 0.0) { + minGap = mDataWidth; + } else { + Double lastVal = null; + + for(Double curVal: xVals) { + if(lastVal != null) { + double curGap = Math.abs(curVal - lastVal); + if (minGap == 0 || (curGap > 0 && curGap < minGap)) { + minGap = curGap; + } } + lastVal = curVal; } - lastVal = curVal; } int numBarSlots = (minGap == 0) ? 1 : (int)Math.round((maxX - minX)/minGap) + 1; @@ -184,24 +235,23 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { // Calculate the overall bar slot width - this includes all bars across // all series, and any spacing between sets of bars - float barSlotWidth = numBarSlots == 1 + int barSlotWidth = numBarSlots == 1 ? graphView.getGraphContentWidth() : graphView.getGraphContentWidth() / (numBarSlots-1); - Log.d("BarGraphSeries", "numBars=" + numBarSlots); // Total spacing (both sides) between sets of bars - float spacing = Math.min((float) barSlotWidth*mSpacing/100, barSlotWidth*0.98f); + double spacing = Math.min(barSlotWidth*mSpacing/100, barSlotWidth*0.98f); // Width of an individual bar - float barWidth = (barSlotWidth - spacing) / numBarSeries; + double barWidth = (barSlotWidth - spacing) / numBarSeries; // Offset from the center of a given bar to start drawing - float offset = barSlotWidth/2; + double offset = barSlotWidth/2; double diffY = maxY - minY; double diffX = maxX - minX; - float contentHeight = graphView.getGraphContentHeight(); - float contentWidth = graphView.getGraphContentWidth(); - float contentLeft = graphView.getGraphContentLeft(); - float contentTop = graphView.getGraphContentTop(); + double contentHeight = graphView.getGraphContentHeight(); + double contentWidth = graphView.getGraphContentWidth(); + double contentLeft = graphView.getGraphContentLeft(); + double contentTop = graphView.getGraphContentTop(); // draw data int i=0; @@ -216,7 +266,8 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { double ratY0 = valY0 / diffY; double y0 = contentHeight * ratY0; - double valX = value.getX() - minX; + double valueX = value.getX(); + double valX = valueX - minX; double ratX = valX / diffX; double x = contentWidth * ratX; @@ -227,14 +278,47 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { mPaint.setColor(getColor()); } - float left = (float)x + contentLeft - offset + spacing/2 + currentSeriesOrder*barWidth; - float top = (contentTop - (float)y) + contentHeight; - float right = left + barWidth; - float bottom = (contentTop - (float)y0) + contentHeight - (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1); + double left = x + contentLeft - offset + spacing/2 + currentSeriesOrder*barWidth; + double right = left + barWidth; + if (left > contentLeft + contentWidth || right < contentLeft) { + continue; + } + double top = (contentTop - y) + contentHeight; + double bottom = (contentTop - y0) + contentHeight - (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1); boolean reverse = top > bottom; + + if (mAnimated) { + if ((Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { + long currentTime = System.currentTimeMillis(); + if (mAnimationStart == 0) { + // start animation + mAnimationStart = currentTime; + mAnimationStartFrameNo = 0; + } else { + // anti-lag: wait a few frames + if (mAnimationStartFrameNo < 15) { + // second time + mAnimationStart = currentTime; + mAnimationStartFrameNo++; + } + } + float timeFactor = (float) (currentTime-mAnimationStart) / ANIMATION_DURATION; + float factor = mAnimationInterpolator.getInterpolation(timeFactor); + if (timeFactor <= 1.0) { + double barHeight = bottom - top; + barHeight = barHeight * factor; + top = bottom-barHeight; + ViewCompat.postInvalidateOnAnimation(graphView); + } else { + // animation finished + mLastAnimatedValue = valueX; + } + } + } + if (reverse) { - float tmp = top; + double tmp = top; top = bottom + (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1); bottom = tmp; } @@ -245,9 +329,15 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { bottom = Math.min(bottom, contentTop+contentHeight); top = Math.max(top, contentTop); - mDataPoints.put(new RectF(left, top, right, bottom), value); + mDataPoints.put(new RectD(left, top, right, bottom), value); - canvas.drawRect(left, top, right, bottom, mPaint); + Paint p; + if (mCustomPaint != null) { + p = mCustomPaint; + } else { + p = mPaint; + } + canvas.drawRect((float)left, (float)top, (float)right, (float)bottom, p); // set values on top of graph if (mDrawValuesOnTop) { @@ -262,7 +352,7 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { mPaint.setColor(mValuesOnTopColor); canvas.drawText( graphView.getGridLabelRenderer().getLabelFormatter().formatLabel(value.getY(), false) - , (left+right)/2, top, mPaint); + , (float) (left+right)/2, (float) top, mPaint); } i++; @@ -303,6 +393,22 @@ public void setSpacing(int mSpacing) { this.mSpacing = mSpacing; } + /** + * @return the interval between data points + */ + public double getDataWidth() { + return mDataWidth; + } + + /** + * @param mDataWidth width of a data point (sampling period) + * 0 => no prior knowledge of sampling period, interval between bars will be calculated automatically + * >0 => value is the total distance from one bar to another + */ + public void setDataWidth(double mDataWidth) { + this.mDataWidth = mDataWidth; + } + /** * @return whether the values should be drawn above the bars */ @@ -368,7 +474,7 @@ protected void resetDataPoints() { */ @Override protected E findDataPoint(float x, float y) { - for (Map.Entry entry : mDataPoints.entrySet()) { + for (Map.Entry entry : mDataPoints.entrySet()) { if (x >= entry.getKey().left && x <= entry.getKey().right && y >= entry.getKey().top && y <= entry.getKey().bottom) { return entry.getValue(); @@ -376,4 +482,45 @@ protected E findDataPoint(float x, float y) { } return null; } + + /** + * custom paint that can be used. + * this will ignore the value dependent color. + * + * @return custom paint or null + */ + public Paint getCustomPaint() { + return mCustomPaint; + } + + /** + * custom paint that can be used. + * this will ignore the value dependent color. + * + * @param mCustomPaint custom paint to use or null + */ + public void setCustomPaint(Paint mCustomPaint) { + this.mCustomPaint = mCustomPaint; + } + + /** + * draw the series with an animation + * + * @param animated animation activated or not + */ + public void setAnimated(boolean animated) { + this.mAnimated = animated; + } + + /** + * @return rendering is animated or not + */ + public boolean isAnimated() { + return mAnimated; + } + + @Override + public void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value) { + // TODO + } } diff --git a/src/main/java/com/jjoe64/graphview/series/BaseSeries.java b/src/main/java/com/jjoe64/graphview/series/BaseSeries.java index 8ecdf23a7..141b7497d 100644 --- a/src/main/java/com/jjoe64/graphview/series/BaseSeries.java +++ b/src/main/java/com/jjoe64/graphview/series/BaseSeries.java @@ -1,29 +1,28 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.series; +import android.graphics.Canvas; import android.graphics.PointF; import android.util.Log; import com.jjoe64.graphview.GraphView; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -75,6 +74,16 @@ public abstract class BaseSeries implements Series */ private int mColor = 0xff0077cc; + /** + * cache for lowest y value + */ + private double mLowestYCache = Double.NaN; + + /** + * cahce for highest y value + */ + private double mHighestYCache = Double.NaN; + /** * listener to handle tap events on a data point */ @@ -84,13 +93,14 @@ public abstract class BaseSeries implements Series * stores the graphviews where this series is used. * Can be more than one. */ - private List mGraphViews; + private List> mGraphViews; + private Boolean mIsCursorModeCache; /** * creates series without data */ public BaseSeries() { - mGraphViews = new ArrayList(); + mGraphViews = new ArrayList<>(); } /** @@ -100,10 +110,11 @@ public BaseSeries() { * important: array has to be sorted from lowest x-value to the highest */ public BaseSeries(E[] data) { - mGraphViews = new ArrayList(); + mGraphViews = new ArrayList<>(); for (E d : data) { mData.add(d); } + checkValueOrder(null); } /** @@ -127,6 +138,9 @@ public double getHighestValueX() { */ public double getLowestValueY() { if (mData.isEmpty()) return 0d; + if (!Double.isNaN(mLowestYCache)) { + return mLowestYCache; + } double l = mData.get(0).getY(); for (int i = 1; i < mData.size(); i++) { double c = mData.get(i).getY(); @@ -134,7 +148,7 @@ public double getLowestValueY() { l = c; } } - return l; + return mLowestYCache = l; } /** @@ -142,6 +156,9 @@ public double getLowestValueY() { */ public double getHighestValueY() { if (mData.isEmpty()) return 0d; + if (!Double.isNaN(mHighestYCache)) { + return mHighestYCache; + } double h = mData.get(0).getY(); for (int i = 1; i < mData.size(); i++) { double c = mData.get(i).getY(); @@ -149,7 +166,7 @@ public double getHighestValueY() { h = c; } } - return h; + return mHighestYCache = h; } /** @@ -180,19 +197,21 @@ public Iterator getValues(final double from, final double until) { if (org.hasNext()) { prevValue = org.next(); } - if (prevValue.getX() >= from) { - nextValue = prevValue; - found = true; - } else { - while (org.hasNext()) { - nextValue = org.next(); - if (nextValue.getX() >= from) { - found = true; - nextNextValue = nextValue; - nextValue = prevValue; - break; + if (prevValue != null) { + if (prevValue.getX() >= from) { + nextValue = prevValue; + found = true; + } else { + while (org.hasNext()) { + nextValue = org.next(); + if (nextValue.getX() >= from) { + found = true; + nextNextValue = nextValue; + nextValue = prevValue; + break; + } + prevValue = nextValue; } - prevValue = nextValue; } } if (!found) { @@ -323,6 +342,27 @@ protected E findDataPoint(float x, float y) { return null; } + public E findDataPointAtX(float x) { + float shortestDistance = Float.NaN; + E shortest = null; + for (Map.Entry entry : mDataPoints.entrySet()) { + float x1 = entry.getKey().x; + float x2 = x; + + float distance = Math.abs(x1 - x2); + if (shortest == null || distance < shortestDistance) { + shortestDistance = distance; + shortest = entry.getValue(); + } + } + if (shortest != null) { + if (shortestDistance < 200) { + return shortest; + } + } + return null; + } + /** * register the datapoint to find it at a tap * @@ -331,7 +371,23 @@ protected E findDataPoint(float x, float y) { * @param dp the data point to save */ protected void registerDataPoint(float x, float y, E dp) { - mDataPoints.put(new PointF(x, y), dp); + // performance + // TODO maybe invalidate after setting the listener + if (mOnDataPointTapListener != null || isCursorMode()) { + mDataPoints.put(new PointF(x, y), dp); + } + } + + private boolean isCursorMode() { + if (mIsCursorModeCache != null) { + return mIsCursorModeCache; + } + for (WeakReference graphView : mGraphViews) { + if (graphView != null && graphView.get() != null && graphView.get().isCursorMode()) { + return mIsCursorModeCache = true; + } + } + return mIsCursorModeCache = false; } /** @@ -355,20 +411,24 @@ public void resetData(E[] data) { } checkValueOrder(null); + mHighestYCache = mLowestYCache = Double.NaN; + // update graphview - for (GraphView gv : mGraphViews) { - gv.onDataChanged(true, false); + for (WeakReference gv : mGraphViews) { + if (gv != null && gv.get() != null) { + gv.get().onDataChanged(true, false); + } } } /** - * called when the series was added to a graph + * stores the reference of the used graph * * @param graphView graphview */ @Override public void onGraphViewAttached(GraphView graphView) { - mGraphViews.add(graphView); + mGraphViews.add(new WeakReference<>(graphView)); } /** @@ -378,8 +438,9 @@ public void onGraphViewAttached(GraphView graphView) { * @param scrollToEnd true => graphview will scroll to the end (maxX) * @param maxDataPoints if max data count is reached, the oldest data * value will be lost to avoid memory leaks + * @param silent set true to avoid rerender the graph */ - public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints) { + public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, boolean silent) { checkValueOrder(dataPoint); if (!mData.isEmpty() && dataPoint.getX() < mData.get(mData.size()-1).getX()) { @@ -395,21 +456,52 @@ public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints) { mData.remove(0); mData.add(dataPoint); } + + // update lowest/highest cache + double dataPointY = dataPoint.getY(); + if (!Double.isNaN(mHighestYCache)) { + if (dataPointY > mHighestYCache) { + mHighestYCache = dataPointY; + } + } + if (!Double.isNaN(mLowestYCache)) { + if (dataPointY < mLowestYCache) { + mLowestYCache = dataPointY; + } + } + } - // recalc the labels when it was the first data - boolean keepLabels = mData.size() != 1; + if (!silent) { + // recalc the labels when it was the first data + boolean keepLabels = mData.size() != 1; - // update linked graph views - // update graphview - for (GraphView gv : mGraphViews) { - gv.onDataChanged(keepLabels, scrollToEnd); - if (scrollToEnd) { - gv.getViewport().scrollToEnd(); + // update linked graph views + // update graphview + for (WeakReference gv : mGraphViews) { + if (gv != null && gv.get() != null) { + if (scrollToEnd) { + gv.get().getViewport().scrollToEnd(); + } else { + gv.get().onDataChanged(keepLabels, scrollToEnd); + } + } } } } + /** + * + * @param dataPoint values the values must be in the correct order! + * x-value has to be ASC. First the lowest x value and at least the highest x value. + * @param scrollToEnd true => graphview will scroll to the end (maxX) + * @param maxDataPoints if max data count is reached, the oldest data + * value will be lost to avoid memory leaks + */ + public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints) { + appendData(dataPoint, scrollToEnd, maxDataPoints, false); + } + /** * @return whether there are data points */ @@ -445,4 +537,21 @@ protected void checkValueOrder(DataPointInterface onlyLast) { } } } + + public abstract void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value); + + public void clearCursorModeCache() { + mIsCursorModeCache = null; + } + + @Override + public void clearReference(GraphView graphView) { + // find and remove + for (WeakReference view : mGraphViews) { + if (view != null && view.get() != null && view.get() == graphView) { + mGraphViews.remove(view); + break; + } + } + } } diff --git a/src/main/java/com/jjoe64/graphview/series/DataPoint.java b/src/main/java/com/jjoe64/graphview/series/DataPoint.java index b5f5eb3ef..097a29c02 100644 --- a/src/main/java/com/jjoe64/graphview/series/DataPoint.java +++ b/src/main/java/com/jjoe64/graphview/series/DataPoint.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.series; diff --git a/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java b/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java index 9be683bef..5d641f7d0 100644 --- a/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java +++ b/src/main/java/com/jjoe64/graphview/series/DataPointInterface.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.series; diff --git a/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java index 2dd4988ef..3d56125dd 100644 --- a/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java +++ b/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Copyright 2016 Jonas Gehring + *

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

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.series; @@ -23,6 +20,8 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; +import androidx.core.view.ViewCompat; +import android.view.animation.AccelerateInterpolator; import com.jjoe64.graphview.GraphView; @@ -35,6 +34,8 @@ * @author jjoe64 */ public class LineGraphSeries extends BaseSeries { + private static final long ANIMATION_DURATION = 333; + /** * wrapped styles regarding the line */ @@ -84,6 +85,8 @@ private final class Styles { */ private Styles mStyles; + private Paint mSelectionPaint; + /** * internal paint object */ @@ -110,6 +113,39 @@ private final class Styles { */ private Paint mCustomPaint; + /** + * rendering is animated + */ + private boolean mAnimated; + + /** + * last animated value + */ + private double mLastAnimatedValue = Double.NaN; + + /** + * time of animation start + */ + private long mAnimationStart; + + /** + * animation interpolator + */ + private AccelerateInterpolator mAnimationInterpolator; + + /** + * number of animation frame to avoid lagging + */ + private int mAnimationStartFrameNo; + + /** + * flag whether the line should be drawn as a path + * or with single drawLine commands (more performance) + * By default we use drawLine because it has much more peformance. + * For some styling reasons it can make sense to draw as path. + */ + private boolean mDrawAsPath = false; + /** * creates a series without data */ @@ -120,7 +156,8 @@ public LineGraphSeries() { /** * creates a series with data * - * @param data data points + * @param data data points + * important: array has to be sorted from lowest x-value to the highest */ public LineGraphSeries(E[] data) { super(data); @@ -138,8 +175,14 @@ protected void init() { mPaint.setStyle(Paint.Style.STROKE); mPaintBackground = new Paint(); + mSelectionPaint = new Paint(); + mSelectionPaint.setColor(Color.argb(80, 0, 0, 0)); + mSelectionPaint.setStyle(Paint.Style.FILL); + mPathBackground = new Path(); mPath = new Path(); + + mAnimationInterpolator = new AccelerateInterpolator(2f); } /** @@ -161,8 +204,8 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { double maxY; double minY; if (isSecondScale) { - maxY = graphView.getSecondScale().getMaxY(); - minY = graphView.getSecondScale().getMinY(); + maxY = graphView.getSecondScale().getMaxY(false); + minY = graphView.getSecondScale().getMinY(false); } else { maxY = graphView.getViewport().getMaxY(false); minY = graphView.getViewport().getMinY(false); @@ -186,6 +229,8 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { paint = mPaint; } + mPath.reset(); + if (mStyles.drawBackground) { mPathBackground.reset(); } @@ -200,9 +245,20 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { lastEndY = 0; lastEndX = 0; + + // needed to end the path for background double lastUsedEndX = 0; - float firstX = 0; - int i=0; + double lastUsedEndY = 0; + float firstX = -1; + float firstY = -1; + float lastRenderedX = Float.NaN; + int i = 0; + float lastAnimationReferenceX = graphLeft; + + boolean sameXSkip = false; + float minYOnSameX = 0f; + float maxYOnSameX = 0f; + while (values.hasNext()) { E value = values.next(); @@ -210,7 +266,8 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { double ratY = valY / diffY; double y = graphHeight * ratY; - double valX = value.getX() - minX; + double valueX = value.getX(); + double valX = valueX - minX; double ratX = valX / diffX; double x = graphWidth * ratX; @@ -219,80 +276,248 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { if (i > 0) { // overdraw + boolean isOverdrawY = false; + boolean isOverdrawEndPoint = false; + boolean skipDraw = false; + if (x > graphWidth) { // end right - double b = ((graphWidth - lastEndX) * (y - lastEndY)/(x - lastEndX)); - y = lastEndY+b; + double b = ((graphWidth - lastEndX) * (y - lastEndY) / (x - lastEndX)); + y = lastEndY + b; x = graphWidth; + isOverdrawEndPoint = true; } if (y < 0) { // end bottom - double b = ((0 - lastEndY) * (x - lastEndX)/(y - lastEndY)); - x = lastEndX+b; + // skip when previous and this point is out of bound + if (lastEndY < 0) { + skipDraw = true; + } else { + double b = ((0 - lastEndY) * (x - lastEndX) / (y - lastEndY)); + x = lastEndX + b; + } y = 0; + isOverdrawY = isOverdrawEndPoint = true; } if (y > graphHeight) { // end top - double b = ((graphHeight - lastEndY) * (x - lastEndX)/(y - lastEndY)); - x = lastEndX+b; + // skip when previous and this point is out of bound + if (lastEndY > graphHeight) { + skipDraw = true; + } else { + double b = ((graphHeight - lastEndY) * (x - lastEndX) / (y - lastEndY)); + x = lastEndX + b; + } y = graphHeight; - } - if (lastEndY < 0) { // start bottom - double b = ((0 - y) * (x - lastEndX)/(lastEndY - y)); - lastEndX = x-b; - lastEndY = 0; + isOverdrawY = isOverdrawEndPoint = true; } if (lastEndX < 0) { // start left - double b = ((0 - x) * (y - lastEndY)/(lastEndX - x)); - lastEndY = y-b; + double b = ((0 - x) * (y - lastEndY) / (lastEndX - x)); + lastEndY = y - b; lastEndX = 0; } + + // we need to save the X before it will be corrected when overdraw y + float orgStartX = (float) lastEndX + (graphLeft + 1); + + if (lastEndY < 0) { // start bottom + if (!skipDraw) { + double b = ((0 - y) * (x - lastEndX) / (lastEndY - y)); + lastEndX = x - b; + } + lastEndY = 0; + isOverdrawY = true; + } if (lastEndY > graphHeight) { // start top - double b = ((graphHeight - y) * (x - lastEndX)/(lastEndY - y)); - lastEndX = x-b; + // skip when previous and this point is out of bound + if (!skipDraw) { + double b = ((graphHeight - y) * (x - lastEndX) / (lastEndY - y)); + lastEndX = x - b; + } lastEndY = graphHeight; + isOverdrawY = true; } float startX = (float) lastEndX + (graphLeft + 1); float startY = (float) (graphTop - lastEndY) + graphHeight; float endX = (float) x + (graphLeft + 1); float endY = (float) (graphTop - y) + graphHeight; + float startXAnimated = startX; + float endXAnimated = endX; + + if (endX < startX) { + // dont draw from right to left + skipDraw = true; + } + + // NaN can happen when previous and current value is out of y bounds + if (!skipDraw && !Float.isNaN(startY) && !Float.isNaN(endY)) { + // animation + if (mAnimated) { + if ((Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { + long currentTime = System.currentTimeMillis(); + if (mAnimationStart == 0) { + // start animation + mAnimationStart = currentTime; + mAnimationStartFrameNo = 0; + } else { + // anti-lag: wait a few frames + if (mAnimationStartFrameNo < 15) { + // second time + mAnimationStart = currentTime; + mAnimationStartFrameNo++; + } + } + float timeFactor = (float) (currentTime - mAnimationStart) / ANIMATION_DURATION; + float factor = mAnimationInterpolator.getInterpolation(timeFactor); + if (timeFactor <= 1.0) { + startXAnimated = (startX - lastAnimationReferenceX) * factor + lastAnimationReferenceX; + startXAnimated = Math.max(startXAnimated, lastAnimationReferenceX); + endXAnimated = (endX - lastAnimationReferenceX) * factor + lastAnimationReferenceX; + ViewCompat.postInvalidateOnAnimation(graphView); + } else { + // animation finished + mLastAnimatedValue = valueX; + } + } else { + lastAnimationReferenceX = endX; + } + } + + // draw data point + if (!isOverdrawEndPoint) { + if (mStyles.drawDataPoints) { + // draw first datapoint + Paint.Style prevStyle = paint.getStyle(); + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(endXAnimated, endY, mStyles.dataPointsRadius, paint); + paint.setStyle(prevStyle); + } + registerDataPoint(endX, endY, value); + } + + if (mDrawAsPath) { + mPath.moveTo(startXAnimated, startY); + } + // performance opt. + if (Float.isNaN(lastRenderedX) || Math.abs(endX - lastRenderedX) > .3f) { + if (mDrawAsPath) { + mPath.lineTo(endXAnimated, endY); + } else { + // draw vertical lines that were skipped + if (sameXSkip) { + sameXSkip = false; + renderLine(canvas, new float[]{lastRenderedX, minYOnSameX, lastRenderedX, maxYOnSameX}, paint); + } + renderLine(canvas, new float[]{startXAnimated, startY, endXAnimated, endY}, paint); + } + lastRenderedX = endX; + } else { + // rendering on same x position + // save min+max y position and draw it as line + if (sameXSkip) { + minYOnSameX = Math.min(minYOnSameX, endY); + maxYOnSameX = Math.max(maxYOnSameX, endY); + } else { + // first + sameXSkip = true; + minYOnSameX = Math.min(startY, endY); + maxYOnSameX = Math.max(startY, endY); + } + } - // draw data point - if (mStyles.drawDataPoints) { - //fix: last value was not drawn. Draw here now the end values - canvas.drawCircle(endX, endY, mStyles.dataPointsRadius, paint); } - registerDataPoint(endX, endY, value); - mPath.reset(); - mPath.moveTo(startX, startY); - mPath.lineTo(endX, endY); - canvas.drawPath(mPath, paint); if (mStyles.drawBackground) { - if (i==1) { - firstX = startX; - mPathBackground.moveTo(startX, startY); + if (isOverdrawY) { + // start draw original x + if (firstX == -1) { + firstX = orgStartX; + firstY = startY; + mPathBackground.moveTo(orgStartX, startY); + } + // from original start to new start + mPathBackground.lineTo(startXAnimated, startY); + } + if (firstX == -1) { + firstX = startXAnimated; + firstY = startY; + mPathBackground.moveTo(startXAnimated, startY); } - mPathBackground.lineTo(endX, endY); + mPathBackground.lineTo(startXAnimated, startY); + mPathBackground.lineTo(endXAnimated, endY); } - lastUsedEndX = endX; + + lastUsedEndX = endXAnimated; + lastUsedEndY = endY; } else if (mStyles.drawDataPoints) { //fix: last value not drawn as datapoint. Draw first point here, and then on every step the end values (above) float first_X = (float) x + (graphLeft + 1); float first_Y = (float) (graphTop - y) + graphHeight; - //TODO canvas.drawCircle(first_X, first_Y, dataPointsRadius, mPaint); + + if (first_X >= graphLeft && first_Y <= (graphTop + graphHeight)) { + if (mAnimated && (Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { + long currentTime = System.currentTimeMillis(); + if (mAnimationStart == 0) { + // start animation + mAnimationStart = currentTime; + } + float timeFactor = (float) (currentTime - mAnimationStart) / ANIMATION_DURATION; + float factor = mAnimationInterpolator.getInterpolation(timeFactor); + if (timeFactor <= 1.0) { + first_X = (first_X - lastAnimationReferenceX) * factor + lastAnimationReferenceX; + ViewCompat.postInvalidateOnAnimation(graphView); + } else { + // animation finished + mLastAnimatedValue = valueX; + } + } + + + Paint.Style prevStyle = paint.getStyle(); + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(first_X, first_Y, mStyles.dataPointsRadius, paint); + paint.setStyle(prevStyle); + registerDataPoint(first_X, first_Y, value); + } } lastEndY = orgY; lastEndX = orgX; i++; } - if (mStyles.drawBackground) { + if (mDrawAsPath) { + // draw at the end + canvas.drawPath(mPath, paint); + } + + if (mStyles.drawBackground && firstX != -1) { // end / close path - mPathBackground.lineTo((float) lastUsedEndX, graphHeight + graphTop); + if (lastUsedEndY != graphHeight + graphTop) { + // dont draw line to same point, otherwise the path is completely broken + mPathBackground.lineTo((float) lastUsedEndX, graphHeight + graphTop); + } mPathBackground.lineTo(firstX, graphHeight + graphTop); - mPathBackground.close(); + if (firstY != graphHeight + graphTop) { + // dont draw line to same point, otherwise the path is completely broken + mPathBackground.lineTo(firstX, firstY); + } + //mPathBackground.close(); canvas.drawPath(mPathBackground, mPaintBackground); } + } + /** + * just a wrapper to draw lines on canvas + * + * @param canvas + * @param pts + * @param paint + */ + private void renderLine(Canvas canvas, float[] pts, Paint paint) { + if (pts.length == 4 && pts[0] == pts[2] && pts[1] == pts[3]) { + // avoid zero length lines, to makes troubles on some devices + // see https://github.com/appsthatmatter/GraphView/issues/499 + return; + } + canvas.drawLines(pts, paint); } /** @@ -380,7 +605,7 @@ public void setDataPointsRadius(float dataPointsRadius) { } /** - * @return the background color for the filling under + * @return the background color for the filling under * the line. * @see #setDrawBackground(boolean) */ @@ -406,4 +631,84 @@ public void setBackgroundColor(int backgroundColor) { public void setCustomPaint(Paint customPaint) { this.mCustomPaint = customPaint; } + + /** + * @param animated activate the animated rendering + */ + public void setAnimated(boolean animated) { + this.mAnimated = animated; + } + + /** + * flag whether the line should be drawn as a path + * or with single drawLine commands (more performance) + * By default we use drawLine because it has much more peformance. + * For some styling reasons it can make sense to draw as path. + */ + public boolean isDrawAsPath() { + return mDrawAsPath; + } + + /** + * flag whether the line should be drawn as a path + * or with single drawLine commands (more performance) + * By default we use drawLine because it has much more peformance. + * For some styling reasons it can make sense to draw as path. + * + * @param mDrawAsPath true to draw as path + */ + public void setDrawAsPath(boolean mDrawAsPath) { + this.mDrawAsPath = mDrawAsPath; + } + + /** + * + * @param dataPoint values the values must be in the correct order! + * x-value has to be ASC. First the lowest x value and at least the highest x value. + * @param scrollToEnd true => graphview will scroll to the end (maxX) + * @param maxDataPoints if max data count is reached, the oldest data + * value will be lost to avoid memory leaks + * @param silent set true to avoid rerender the graph + */ + public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, boolean silent) { + if (!isAnimationActive()) { + mAnimationStart = 0; + } + super.appendData(dataPoint, scrollToEnd, maxDataPoints, silent); + } + + /** + * @return currently animation is active + */ + private boolean isAnimationActive() { + if (mAnimated) { + long curr = System.currentTimeMillis(); + return curr - mAnimationStart <= ANIMATION_DURATION; + } + return false; + } + + @Override + public void drawSelection(GraphView graphView, Canvas canvas, boolean b, DataPointInterface value) { + double spanX = graphView.getViewport().getMaxX(false) - graphView.getViewport().getMinX(false); + double spanXPixel = graphView.getGraphContentWidth(); + + double spanY = graphView.getViewport().getMaxY(false) - graphView.getViewport().getMinY(false); + double spanYPixel = graphView.getGraphContentHeight(); + + double pointX = (value.getX() - graphView.getViewport().getMinX(false)) * spanXPixel / spanX; + pointX += graphView.getGraphContentLeft(); + + double pointY = (value.getY() - graphView.getViewport().getMinY(false)) * spanYPixel / spanY; + pointY = graphView.getGraphContentTop() + spanYPixel - pointY; + + // border + canvas.drawCircle((float) pointX, (float) pointY, 30f, mSelectionPaint); + + // fill + Paint.Style prevStyle = mPaint.getStyle(); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle((float) pointX, (float) pointY, 23f, mPaint); + mPaint.setStyle(prevStyle); + } } diff --git a/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java b/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java index 748a1122e..e846e329e 100644 --- a/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java +++ b/src/main/java/com/jjoe64/graphview/series/OnDataPointTapListener.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.series; diff --git a/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java index c57d476d7..2ec74a526 100644 --- a/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java +++ b/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.series; @@ -44,7 +41,7 @@ public class PointsGraphSeries extends BaseSeries< public static interface CustomShape { /** * called when drawing a single data point. - * use the x and y coordinates to render your + * use the x and y coordinates to draw your * drawing at this point. * * @param canvas canvas to draw on @@ -58,9 +55,9 @@ public static interface CustomShape { } /** - * choose a predefined shape to render for + * choose a predefined shape to draw for * each data point. - * You can also render a custom drawing via {@link com.jjoe64.graphview.series.PointsGraphSeries.CustomShape} + * You can also draw a custom drawing via {@link com.jjoe64.graphview.series.PointsGraphSeries.CustomShape} */ public enum Shape { /** @@ -158,8 +155,8 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { double maxY; double minY; if (isSecondScale) { - maxY = graphView.getSecondScale().getMaxY(); - minY = graphView.getSecondScale().getMinY(); + maxY = graphView.getSecondScale().getMaxY(false); + minY = graphView.getSecondScale().getMinY(false); } else { maxY = graphView.getViewport().getMaxY(false); minY = graphView.getViewport().getMinY(false); @@ -211,7 +208,11 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { if (y > graphHeight) { // end top overdraw = true; } - + /* Fix a bug that continue to show the DOT after Y axis */ + if(x < 0) { + overdraw = true; + } + float endX = (float) x + (graphLeft + 1); float endY = (float) (graphTop - y) + graphHeight; registerDataPoint(endX, endY, value); @@ -239,7 +240,7 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { } /** - * helper to render triangle + * helper to draw triangle * * @param point array with 3 coordinates * @param canvas canvas to draw on @@ -301,7 +302,7 @@ public void setShape(Shape s) { } /** - * Use a custom handler to render your own + * Use a custom handler to draw your own * drawing for each data point. * * @param shape handler to use a custom drawing @@ -309,4 +310,9 @@ public void setShape(Shape s) { public void setCustomShape(CustomShape shape) { mCustomShape = shape; } + + @Override + public void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value) { + // TODO + } } diff --git a/src/main/java/com/jjoe64/graphview/series/Series.java b/src/main/java/com/jjoe64/graphview/series/Series.java index dce32eb78..95e82a7e1 100644 --- a/src/main/java/com/jjoe64/graphview/series/Series.java +++ b/src/main/java/com/jjoe64/graphview/series/Series.java @@ -1,21 +1,18 @@ /** * GraphView - * Copyright (C) 2014 Jonas Gehring + * Copyright 2016 Jonas Gehring * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.jjoe64.graphview.series; @@ -122,4 +119,11 @@ public interface Series { * @return whether there are data points */ boolean isEmpty(); + + /** + * clear reference to view and activity + * + * @param graphView + */ + void clearReference(GraphView graphView); } diff --git a/zooming.gif b/zooming.gif new file mode 100644 index 000000000..2b3dc215c Binary files /dev/null and b/zooming.gif differ