diff --git a/README.markdown b/README.markdown index a92cf3288..7732c6a8b 100644 --- a/README.markdown +++ b/README.markdown @@ -1,5 +1,7 @@ # 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 @@ -45,11 +47,11 @@ Supported graph types: 1) Add gradle dependency: ``` -compile 'com.jjoe64:graphview:4.2.1' +implementation 'com.jjoe64:graphview:4.2.2' ``` 2) Add view to layout: -``` +```xml series = new LineGraphSeries(new DataPoint[] { new DataPoint(0, 1), @@ -76,9 +78,9 @@ graph.addSeries(series); ## More examples and documentation -Get started at project homepage +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 page http://android-graphview.org +View GraphView wiki page https://github.com/jjoe64/GraphView/wiki diff --git a/README.new-version.md b/README.new-version.md index a4f6a33cd..8bd815f25 100644 --- a/README.new-version.md +++ b/README.new-version.md @@ -23,9 +23,11 @@ and/or here as ascii => needs some time +hardcode gpg key password in maven_push.gradle + hardcode user/pwd of nexus account in maven_push.gradle -run gradle task uploadArchives +success gradle task uploadArchives - ./gradlew --rerun-tasks uploadArchives - enter gpg info (id:D8C3B041 / path: /Users/jonas/.gnupg/secring.gpg / PWD) @@ -45,8 +47,13 @@ Release entry Wait some days -How to create a new .jar file --------------------------------- -run this gradle task -- ./gradlew --rerun-tasks clearJar makeJar -- cp build/outputs/myCompiledLibrary.jar public/GraphView-4.2.0.jar +## 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/build.gradle b/build.gradle index a83e33dac..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.10' +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" } @@ -35,7 +38,7 @@ android { } dependencies { - compile 'com.android.support:support-v4:22.1.1' + implementation 'androidx.core:core:1.0.0-beta01' } @@ -55,5 +58,10 @@ task makeJar(type: Copy) { makeJar.dependsOn(clearJar, build) -//apply from: './maven_push.gradle' +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/gradle.properties b/gradle.properties index c4365909f..86a27f74c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -VERSION_NAME=4.2.1 -VERSION_CODE=16 +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 13372aef5..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 7c943b4ce..25f587d12 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu Jun 02 09:21:22 CEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-all.zip diff --git a/gradlew b/gradlew index 9d82f7891..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,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# 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 - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,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 @@ -150,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/maven_push.gradle b/maven_push.gradle index 38f6faafd..3d15ef167 100644 --- a/maven_push.gradle +++ b/maven_push.gradle @@ -11,15 +11,15 @@ gradle.taskGraph.whenReady { taskGraph -> console.printf "\n\nWe have to sign some things in this build." + "\n\nPlease enter your signing details.\n\n" - def id = console.readLine("PGP Key Id: ") - def file = console.readLine("PGP Secret Key Ring File (absolute path): ") - def password = console.readPassword("PGP Private Key Password: ") + 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" + console.printf "\nThanks.\n\n" + file } } @@ -35,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 -> @@ -48,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()) @@ -60,6 +66,7 @@ afterEvaluate { project -> description POM_DESCRIPTION url POM_URL + scm { url POM_SCM_URL connection POM_SCM_CONNECTION @@ -111,4 +118,4 @@ afterEvaluate { project -> archives androidSourcesJar archives androidJavadocsJar } -} \ No newline at end of file +} 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/GraphView.java b/src/main/java/com/jjoe64/graphview/GraphView.java index 63ff957ab..a01da0fbf 100644 --- a/src/main/java/com/jjoe64/graphview/GraphView.java +++ b/src/main/java/com/jjoe64/graphview/GraphView.java @@ -17,17 +17,23 @@ 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; @@ -150,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 @@ -286,7 +296,8 @@ public void onDataChanged(boolean keepLabelsSize, boolean keepViewport) { protected void drawGraphElements(Canvas canvas) { // must be in hardware accelerated mode if (android.os.Build.VERSION.SDK_INT >= 11 && !canvas.isHardwareAccelerated()) { - throw new IllegalStateException("GraphView must be used in hardware accelerated mode." + + // 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"); } @@ -302,6 +313,11 @@ protected void drawGraphElements(Canvas canvas) { s.draw(this, canvas, true); } } + + if (mCursorMode != null) { + mCursorMode.draw(canvas); + } + mViewport.draw(canvas); mLegendRenderer.draw(canvas); } @@ -561,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)} @@ -573,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 ece5db7c3..37210b4e7 100644 --- a/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java +++ b/src/main/java/com/jjoe64/graphview/GridLabelRenderer.java @@ -21,7 +21,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.util.Log; import android.util.TypedValue; import java.util.LinkedHashMap; @@ -325,6 +324,13 @@ 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 @@ -333,7 +339,7 @@ public void setSecondScaleLabelVerticalWidth(Integer newWidth) { * By default this is enabled. It makes sense to deactivate it * when using Dates on the x axis. */ - private boolean mHumanRounding; + private boolean mHumanRoundingX; /** * create the default grid label renderer. @@ -347,7 +353,8 @@ public GridLabelRenderer(GraphView graphView) { resetStyles(); mNumVerticalLabels = 5; mNumHorizontalLabels = 5; - mHumanRounding = true; + mHumanRoundingX = true; + mHumanRoundingY = true; } /** @@ -437,8 +444,18 @@ public void reloadStyles() { * @return if human rounding is enabled */ - public boolean isHumanRounding() { - return mHumanRounding; + 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; } /** @@ -449,10 +466,26 @@ public boolean isHumanRounding() { * By default this is enabled. It makes sense to deactivate it * when using Dates on the x axis. * - * @param humanRounding false to deactivate + * @param humanRoundingX false to deactivate + * @param humanRoundingY false to deactivate */ - public void setHumanRounding(boolean humanRounding) { - this.mHumanRounding = humanRounding; + 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; } /** @@ -623,10 +656,14 @@ protected boolean adjustVerticalSecondScale() { // 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<>((int) numVerticalLabels); + mStepsVerticalSecondScale = new LinkedHashMap<>(numVerticalLabels); } int height = mGraphView.getGraphContentHeight(); @@ -686,8 +723,14 @@ protected boolean adjustVertical(boolean changeBounds) { // 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 (isHumanRounding()) { + if (isHumanRoundingY()) { exactSteps = humanRound(exactSteps, changeBounds); } else if (mStepsVertical != null && mStepsVertical.size() > 1) { // else choose other nice steps that previous @@ -819,8 +862,14 @@ protected boolean adjustHorizontal(boolean changeBounds) { // round because of floating error exactSteps = Math.round(exactSteps * 1000000d) / 1000000d; + // smallest viewport + if (exactSteps == 0d) { + exactSteps = 0.0000001d; + maxX = minX + exactSteps * (numHorizontalLabels - 1); + } + // human rounding to have nice numbers (1, 2, 5, ...) - if (isHumanRounding()) { + if (isHumanRoundingX()) { exactSteps = humanRound(exactSteps, false); } else if (mStepsHorizontal != null && mStepsHorizontal.size() > 1) { // else choose other nice steps that previous diff --git a/src/main/java/com/jjoe64/graphview/LegendRenderer.java b/src/main/java/com/jjoe64/graphview/LegendRenderer.java index 9286cb776..61ea019b2 100644 --- a/src/main/java/com/jjoe64/graphview/LegendRenderer.java +++ b/src/main/java/com/jjoe64/graphview/LegendRenderer.java @@ -148,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 * @@ -161,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/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/Viewport.java b/src/main/java/com/jjoe64/graphview/Viewport.java index e65bc660e..ce674cce8 100644 --- a/src/main/java/com/jjoe64/graphview/Viewport.java +++ b/src/main/java/com/jjoe64/graphview/Viewport.java @@ -19,8 +19,8 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -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; @@ -50,14 +50,14 @@ 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 humanRounding=false. it will be the minValueY value. + * 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 humanRounding=false. it will be the minValueX value. + * is set manual and humanRoundingX=false. it will be the minValueX value. */ protected double referenceX = Double.NaN; @@ -66,6 +66,15 @@ public class Viewport { */ 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 @@ -74,7 +83,7 @@ public class Viewport { protected double getReferenceX() { // if the bounds is manual then we take the // original manual min y value as reference - if (isXAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRounding()) { + if (isXAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRoundingX()) { if (Double.isNaN(referenceX)) { referenceX = getMinX(false); } @@ -140,6 +149,9 @@ public boolean onScale(ScaleGestureDetector detector) { // viewportStart must not be < minX 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; @@ -147,6 +159,9 @@ public boolean onScale(ScaleGestureDetector detector) { // viewportStart + viewportSize must not be > maxX double maxX = getMaxX(true); + if (!Double.isNaN(mMinimalViewport.right)) { + maxX = Math.max(maxX, mMinimalViewport.right); + } if (viewportWidth == 0) { mCurrentViewport.right = maxX; } @@ -165,7 +180,7 @@ public boolean onScale(ScaleGestureDetector detector) { // --- vertical scaling --- - if (scalableY && android.os.Build.VERSION.SDK_INT >= 11) { + 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; @@ -177,6 +192,7 @@ public boolean onScale(ScaleGestureDetector detector) { } center = mCurrentViewport.bottom + viewportHeight / 2; + viewportHeight /= detector.getCurrentSpanY()/detector.getPreviousSpanY(); mCurrentViewport.bottom = center - viewportHeight / 2; mCurrentViewport.top = mCurrentViewport.bottom+viewportHeight; @@ -185,6 +201,9 @@ public boolean onScale(ScaleGestureDetector detector) { 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; @@ -192,6 +211,9 @@ public boolean onScale(ScaleGestureDetector detector) { // 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; } @@ -233,6 +255,11 @@ public boolean onScale(ScaleGestureDetector detector) { */ @Override public boolean onScaleBegin(ScaleGestureDetector detector) { + // cursor mode + if (mGraphView.isCursorMode()) { + return false; + } + if (mIsScalable) { mScalingActive = true; return true; @@ -267,6 +294,11 @@ 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. @@ -279,6 +311,10 @@ public boolean onDown(MotionEvent e) { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + // 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). @@ -292,20 +328,41 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d double viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth(); double viewportOffsetY = distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight(); - int completeWidth = (int)((mCompleteRange.width()/mCurrentViewport.width()) * (double) mGraphView.getGraphContentWidth()); - int completeHeight = (int)((mCompleteRange.height()/mCurrentViewport.height()) * (double) 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 - * (mCurrentViewport.bottom + viewportOffsetY - mCompleteRange.bottom) - / mCompleteRange.height()*-1); - 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; @@ -321,12 +378,12 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d if (canScrollX) { if (viewportOffsetX < 0) { - double tooMuch = mCurrentViewport.left+viewportOffsetX - mCompleteRange.left; + double tooMuch = mCurrentViewport.left+viewportOffsetX - completeRangeLeft; if (tooMuch < 0) { viewportOffsetX -= tooMuch; } } else { - double tooMuch = mCurrentViewport.right+viewportOffsetX - mCompleteRange.right; + double tooMuch = mCurrentViewport.right+viewportOffsetX - completeRangeRight; if (tooMuch > 0) { viewportOffsetX -= tooMuch; } @@ -344,12 +401,12 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d // if we have the second axis we ignore the max/min range if (!hasSecondScale) { if (viewportOffsetY < 0) { - double tooMuch = mCurrentViewport.bottom+viewportOffsetY - mCompleteRange.bottom; + double tooMuch = mCurrentViewport.bottom+viewportOffsetY - completeRangeBottom; if (tooMuch < 0) { viewportOffsetY -= tooMuch; } } else { - double tooMuch = mCurrentViewport.top+viewportOffsetY - mCompleteRange.top; + double tooMuch = mCurrentViewport.top+viewportOffsetY - completeRangeTop; if (tooMuch > 0) { viewportOffsetY -= tooMuch; } @@ -597,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; } @@ -1193,7 +1263,7 @@ public void setScrollableY(boolean scrollableY) { protected double getReferenceY() { // if the bounds is manual then we take the // original manual min y value as reference - if (isYAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRounding()) { + if (isYAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRoundingY()) { if (Double.isNaN(referenceY)) { referenceY = getMinY(false); } @@ -1264,4 +1334,20 @@ public void setMaxXAxisSize(double mMaxXAxisViewportSize) { 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/series/BarGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java index 72079297d..d8ab9ca13 100644 --- a/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java +++ b/src/main/java/com/jjoe64/graphview/series/BarGraphSeries.java @@ -19,7 +19,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; -import android.support.v4.view.ViewCompat; +import androidx.core.view.ViewCompat; import android.util.Log; import android.view.animation.AccelerateInterpolator; @@ -279,8 +279,11 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { } double left = x + contentLeft - offset + spacing/2 + currentSeriesOrder*barWidth; - double top = (contentTop - y) + contentHeight; 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; @@ -501,7 +504,7 @@ public void setCustomPaint(Paint mCustomPaint) { } /** - * render the series with an animation + * draw the series with an animation * * @param animated animation activated or not */ @@ -515,4 +518,9 @@ public void setAnimated(boolean animated) { 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 3e505e7b1..141b7497d 100644 --- a/src/main/java/com/jjoe64/graphview/series/BaseSeries.java +++ b/src/main/java/com/jjoe64/graphview/series/BaseSeries.java @@ -16,11 +16,13 @@ */ 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; @@ -91,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<>(); } /** @@ -107,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); } /** @@ -338,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 * @@ -348,11 +373,23 @@ protected E findDataPoint(float x, float y) { protected void registerDataPoint(float x, float y, E dp) { // performance // TODO maybe invalidate after setting the listener - if (mOnDataPointTapListener != null) { + if (mOnDataPointTapListener != null || isCursorMode()) { mDataPoints.put(new PointF(x, y), dp); } } + private boolean isCursorMode() { + if (mIsCursorModeCache != null) { + return mIsCursorModeCache; + } + for (WeakReference graphView : mGraphViews) { + if (graphView != null && graphView.get() != null && graphView.get().isCursorMode()) { + return mIsCursorModeCache = true; + } + } + return mIsCursorModeCache = false; + } + /** * clears the cached data point coordinates */ @@ -377,8 +414,10 @@ public void resetData(E[] data) { 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); + } } } @@ -389,7 +428,7 @@ public void resetData(E[] data) { */ @Override public void onGraphViewAttached(GraphView graphView) { - mGraphViews.add(graphView); + mGraphViews.add(new WeakReference<>(graphView)); } /** @@ -439,11 +478,13 @@ public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, bool // update linked graph views // update graphview - for (GraphView gv : mGraphViews) { - if (scrollToEnd) { - gv.getViewport().scrollToEnd(); - } else { - gv.onDataChanged(keepLabels, scrollToEnd); + for (WeakReference gv : mGraphViews) { + if (gv != null && gv.get() != null) { + if (scrollToEnd) { + gv.get().getViewport().scrollToEnd(); + } else { + gv.get().onDataChanged(keepLabels, scrollToEnd); + } } } } @@ -496,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/LineGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java index 5420cea3f..3d56125dd 100644 --- a/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java +++ b/src/main/java/com/jjoe64/graphview/series/LineGraphSeries.java @@ -1,13 +1,13 @@ /** * 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. @@ -20,7 +20,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; -import android.support.v4.view.ViewCompat; +import androidx.core.view.ViewCompat; import android.view.animation.AccelerateInterpolator; import com.jjoe64.graphview.GraphView; @@ -85,6 +85,8 @@ private final class Styles { */ private Styles mStyles; + private Paint mSelectionPaint; + /** * internal paint object */ @@ -173,6 +175,10 @@ 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(); @@ -245,9 +251,14 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { double lastUsedEndY = 0; float firstX = -1; float firstY = -1; - float lastRenderedX = 0; - int i=0; + 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(); @@ -270,18 +281,18 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { 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 // skip when previous and this point is out of bound if (lastEndY < 0) { - skipDraw=true; + skipDraw = true; } else { - double b = ((0 - lastEndY) * (x - lastEndX)/(y - lastEndY)); - x = lastEndX+b; + double b = ((0 - lastEndY) * (x - lastEndX) / (y - lastEndY)); + x = lastEndX + b; } y = 0; isOverdrawY = isOverdrawEndPoint = true; @@ -289,17 +300,17 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { if (y > graphHeight) { // end top // skip when previous and this point is out of bound if (lastEndY > graphHeight) { - skipDraw=true; + skipDraw = true; } else { - double b = ((graphHeight - lastEndY) * (x - lastEndX)/(y - lastEndY)); - x = lastEndX+b; + double b = ((graphHeight - lastEndY) * (x - lastEndX) / (y - lastEndY)); + x = lastEndX + b; } y = graphHeight; isOverdrawY = isOverdrawEndPoint = true; } if (lastEndX < 0) { // start left - double b = ((0 - x) * (y - lastEndY)/(lastEndX - x)); - lastEndY = y-b; + double b = ((0 - x) * (y - lastEndY) / (lastEndX - x)); + lastEndY = y - b; lastEndX = 0; } @@ -309,7 +320,7 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { if (lastEndY < 0) { // start bottom if (!skipDraw) { double b = ((0 - y) * (x - lastEndX) / (lastEndY - y)); - lastEndX = x-b; + lastEndX = x - b; } lastEndY = 0; isOverdrawY = true; @@ -317,8 +328,8 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { if (lastEndY > graphHeight) { // start top // skip when previous and this point is out of bound if (!skipDraw) { - double b = ((graphHeight - y) * (x - lastEndX)/(lastEndY - y)); - lastEndX = x-b; + double b = ((graphHeight - y) * (x - lastEndX) / (lastEndY - y)); + lastEndX = x - b; } lastEndY = graphHeight; isOverdrawY = true; @@ -354,12 +365,12 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { mAnimationStartFrameNo++; } } - float timeFactor = (float) (currentTime-mAnimationStart) / ANIMATION_DURATION; + float timeFactor = (float) (currentTime - mAnimationStart) / ANIMATION_DURATION; float factor = mAnimationInterpolator.getInterpolation(timeFactor); if (timeFactor <= 1.0) { - startXAnimated = (startX-lastAnimationReferenceX) * factor + lastAnimationReferenceX; + startXAnimated = (startX - lastAnimationReferenceX) * factor + lastAnimationReferenceX; startXAnimated = Math.max(startXAnimated, lastAnimationReferenceX); - endXAnimated = (endX-lastAnimationReferenceX) * factor + lastAnimationReferenceX; + endXAnimated = (endX - lastAnimationReferenceX) * factor + lastAnimationReferenceX; ViewCompat.postInvalidateOnAnimation(graphView); } else { // animation finished @@ -386,13 +397,30 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { mPath.moveTo(startXAnimated, startY); } // performance opt. - if (Math.abs(endX-lastRenderedX) > .3f) { + if (Float.isNaN(lastRenderedX) || Math.abs(endX - lastRenderedX) > .3f) { if (mDrawAsPath) { mPath.lineTo(endXAnimated, endY); } else { - renderLine(canvas, new float[] {startXAnimated, startY, endXAnimated, endY}, paint); + // 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); + } } } @@ -424,17 +452,17 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { float first_X = (float) x + (graphLeft + 1); float first_Y = (float) (graphTop - y) + graphHeight; - if (first_X >= graphLeft && first_Y <= (graphTop+graphHeight)) { + if (first_X >= graphLeft && first_Y <= (graphTop + graphHeight)) { if (mAnimated && (Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { long currentTime = System.currentTimeMillis(); if (mAnimationStart == 0) { // start animation mAnimationStart = currentTime; } - float timeFactor = (float) (currentTime-mAnimationStart) / ANIMATION_DURATION; + float timeFactor = (float) (currentTime - mAnimationStart) / ANIMATION_DURATION; float factor = mAnimationInterpolator.getInterpolation(timeFactor); if (timeFactor <= 1.0) { - first_X = (first_X-lastAnimationReferenceX) * factor + lastAnimationReferenceX; + first_X = (first_X - lastAnimationReferenceX) * factor + lastAnimationReferenceX; ViewCompat.postInvalidateOnAnimation(graphView); } else { // animation finished @@ -447,6 +475,7 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { 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; @@ -483,6 +512,11 @@ public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { * @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); } @@ -571,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) */ @@ -649,8 +683,32 @@ public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, bool private boolean isAnimationActive() { if (mAnimated) { long curr = System.currentTimeMillis(); - return curr-mAnimationStart <= ANIMATION_DURATION; + 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/PointsGraphSeries.java b/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java index d8c76c6a0..2ec74a526 100644 --- a/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java +++ b/src/main/java/com/jjoe64/graphview/series/PointsGraphSeries.java @@ -41,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 @@ -55,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 { /** @@ -240,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 @@ -302,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 @@ -310,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 76e252513..95e82a7e1 100644 --- a/src/main/java/com/jjoe64/graphview/series/Series.java +++ b/src/main/java/com/jjoe64/graphview/series/Series.java @@ -119,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); }