diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..1e022fdb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,14 @@
+Thanks for taking the time to help make Androidplot better! Please note
+that issues should be submitted on Github for bugs only.
+
+How-to questions may be posted under the [Androidplot tag on Stack Overflow](https://stackoverflow.com/questions/tagged/androidplot)
+and feature requests etc. may be posted to the [Google Group Forum](https://groups.google.com/forum/#!forum/androidplot).
+
+When possible, please include the following in your bug report:
+
+* Description of the problem
+* Steps to reproduce the issue
+* Version(s) of Androidplot being used
+* A stacktrace if the issue is causing a crash
+
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..d38f337e
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,57 @@
+name: build androidplot
+on: [push]
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v3
+
+ - name: Set Up JDK 17
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: '17'
+
+ - name: Build and Test
+ run: ./gradlew testDebugUnitTest jacocoTestDebugUnitTestReport
+
+ - name: Build Release
+ env:
+ ENCODED: ${{ secrets.DEMOAPP_KEYSTORE }}
+ KEYSTORE: keystore.jks
+ KEY_ALIAS: Key0
+ KEYSTORE_PASSWORD: ${{ secrets.DEMOAPP_KEYSTORE_PASSWORD }}
+ KEY_PASSWORD: ${{ secrets.DEMOAPP_KEY_PASSWORD }}
+ run: |
+ echo $ENCODED | base64 -di > "${GITHUB_WORKSPACE}/demoapp/keystore.jks"
+ ./gradlew assembleRelease
+
+# TODO: get javadoc working again at some point
+# - name: Javadoc
+# run: ./gradlew javadoc
+
+ - name: Code Coverage
+ run: bash <(curl -s https://codecov.io/bash)
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: Core Library
+ path: androidplot-core/build/outputs/aar/*.aar
+ retention-days: 30
+
+ - uses: actions/upload-artifact@v3
+ if: github.ref_name == 'master'
+ with:
+ name: Demoapp APK
+ path: demoapp/build/outputs/apk/release/*.apk
+ retention-days: 30
+
+ - name: Stage for Maven Central
+ if: github.ref_name == 'master'
+ env:
+ OSSRH_ACTOR: ${{ secrets.OSSRH_ACTOR }}
+ OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }}
+ SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
+ SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }}
+ run: ./gradlew publish
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 58204be4..694f2995 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,5 @@ DemoApp/.settings
DemoApp/bin
DemoApp/gen
DemoApp/target
-.idea/libraries
-.idea/*.xml
+.idea
**/R.java
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 00000000..f49a4e16
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "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.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "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.
+
+ "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).
+
+ "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.
+
+ "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."
+
+ "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.
+
+ 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.
+
+ 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.
+
+ 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:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) 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
+
+ (d) 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.
+
+ 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.
+
+ 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.
+
+ 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.
+
+ 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.
+
+ 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.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
\ No newline at end of file
diff --git a/README.md b/README.md
index 7736e7d4..9617a26e 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,13 @@
-#  Androidplot [](https://circleci.com/gh/halfhp/androidplot) [](https://codecov.io/gh/halfhp/androidplot) [](https://twitter.com/androidplot)
+#  Androidplot [](https://www.appbrain.com/stats/libraries/details/androidplot/androidplot) [](http://codix.io/gh/repo/halfhp/androidplot) [](https://codecov.io/gh/halfhp/androidplot) [](https://twitter.com/androidplot)
-A library for creating dynamic and static charts in Android apps. It’s designed from the ground up for Android,
-compatible with all versions of Android from 1.6 onward and is **used by over
-[1,000 apps](http://www.appbrain.com/stats/libraries/details/androidplot/androidplot) on Google Play**.
+A library for creating dynamic and static charts in Android apps.
-
-
-
-
-
-
-
+Androidplot is compatible with all versions of Android from 1.6 onward, works equally with Kotlin and Java codebases
+and is compatible with Jetpack compose.
+
+If you enjoy the lib, please [rate us on codix.io](http://codix.io/gh/repo/halfhp/androidplot)!
+
+
**Features:**
@@ -37,7 +34,7 @@ compatible with all versions of Android from 1.6 onward and is **used by over
* [Demo App (Google Play Store)](https://play.google.com/store/apps/details?id=com.androidplot.demos&hl=en)
* [Demo App Soure Code](https://bitbucket.org/androidplot/androidplot/src/1538c5dfa56aed0d2cfdcbc7cdc6173e605543cd/demoapp/?at=master)
* [Bugs](https://github.com/halfhp/androidplot/issues) :ant:
-* [Contributing Source Code](http://androidplot.com/docs/working-with-androidplot-source/)
+* [Contributing Source Code](docs/contributing.md)
# Help
Technical questions should be posted using the [androidplot tag](http://stackoverflow.com/questions/tagged/androidplot) on Stack Overflow. For everything else use the [Google Groups forum](https://groups.google.com/d/forum/androidplot).
@@ -45,7 +42,7 @@ Technical questions should be posted using the [androidplot tag](http://stackove
# License
Androidplot has been made available under the Apache 2.0 license:
- Copyright 2016 Androidplot.com
+ Copyright 2021 Androidplot.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 00000000..2f7efbea
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-minimal
\ No newline at end of file
diff --git a/androidplot-core/build.gradle b/androidplot-core/build.gradle
index 2ead754f..b37b2b4a 100644
--- a/androidplot-core/build.gradle
+++ b/androidplot-core/build.gradle
@@ -14,30 +14,84 @@
* limitations under the License.
*/
-apply plugin: 'com.android.library'
-apply plugin: 'com.github.dcendents.android-maven'
-apply plugin: 'com.jfrog.bintray'
-apply plugin: 'com.vanniktech.android.junit.jacoco'
-apply plugin: 'com.github.kt3k.coveralls'
+plugins {
+ id 'com.android.library'
+ id 'com.mxalbert.gradle.jacoco-android'
+ id 'maven-publish'
+ id 'signing'
+}
+
+class AttrMarkdown extends DefaultTask {
+
+ def inFile
+ def outFile
+
+ @TaskAction
+ def generate() {
+ def input = project.file(inFile)
+
+ def output = project.file(outFile)
+ if (output.exists()) {
+ output.delete()
+ }
+ output.parentFile.mkdirs()
+
+ input.text.findAll(//) { match, g1 ->
+ g1
+ if (!g1.startsWith("NODOC")) {
+ output.append(g1)
+ output.append "\n\n"
+ }
+ }
+ }
+}
+
+/**
+ * Generates xml attrs markdown docs. To run:
+ * at the command line from the project root dir type:
+ * ./gradlew generateAttrsMarkdown
+ *
+ * The generated doc will appear in / replace androidplot/docs/attrs.md
+ */
+task generateAttrsMarkdown(type: AttrMarkdown) {
+ inFile = { "src/main/res/values/attrs.xml" }
+ outFile = { "../docs/attrs.md" }
+}
android {
- compileSdkVersion theCompileSdkVersion
- buildToolsVersion theBuildToolsVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
defaultConfig {
- versionCode theVersionCode
- versionName theVersionName
+ compileSdk theCompileSdkVersion
minSdkVersion theMinSdkVersion
targetSdkVersion theTargetSdkVersion
testApplicationId "com.androidplot.test"
}
- /**
- * TODO: enable and address lint issues.
- */
- lintOptions {
+ testOptions {
+ unitTests.all {
+ jacoco {
+ includeNoLocationClasses = true
+ jacoco.excludes = ['jdk.internal.*']
+ }
+ }
+ }
+ lint {
abortOnError false
}
+ namespace 'com.androidplot'
+
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ withJavadocJar()
+ }
+ }
}
group = 'com.androidplot'
@@ -48,98 +102,88 @@ def gitUrl = 'https://github.com/halfhp/androidplot.git'
dependencies {
- compile 'com.halfhp.fig:figlib:1.0.3'
- testCompile "org.mockito:mockito-core:1.10.19"
- testCompile group: 'junit', name: 'junit', version: '4.12'
- testCompile "org.robolectric:robolectric:3.1"
-
- // temp fix for:
- // https://github.com/robolectric/robolectric/issues/1932
- testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
-}
-
-task javadoc(type: Javadoc) {
- source = android.sourceSets.main.java.srcDirs
- classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
- failOnError false
- options {
- links "http://docs.oracle.com/javase/7/docs/api/"
- linksOffline "http://d.android.com/reference","${android.sdkDirectory}/docs/reference"
- }
- exclude '**/BuildConfig.java'
- exclude '**/R.java'
-}
+ implementation 'com.halfhp.fig:figlib:1.0.11'
+ implementation 'androidx.annotation:annotation:1.8.2'
-task javadocJar(type: Jar, dependsOn: javadoc) {
- classifier = 'javadoc'
- from javadoc.destinationDir
+ testImplementation "org.mockito:mockito-core:5.7.0"
+ testImplementation group: 'junit', name: 'junit', version: '4.13.2'
+ testImplementation "org.robolectric:robolectric:4.7.3"
}
-task sourcesJar(type: Jar) {
- classifier = 'sources'
- from android.sourceSets.main.java.srcDirs
-}
+// TODO: get javadoc working again at some point
+//task javadoc(type: Javadoc) {
+// source = android.sourceSets.main.java.srcDirs
+// classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+// failOnError false
+// options {
+// links "http://docs.oracle.com/javase/7/docs/api/"
+// linksOffline "http://d.android.com/reference","${android.sdkDirectory}/docs/reference"
+// }
+// exclude '**/BuildConfig.java'
+// exclude '**/R.java'
+//}
+//
+
+
+// TODO: get javadoc working again at some point
+//javadoc {
+// options.overview = "src/main/java/overview.html"
+//}
+
+afterEvaluate {
+ publishing {
+ repositories {
+ maven {
+ name = "MavenCentral"
+ url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
+ credentials {
+ username = System.getenv("OSSRH_ACTOR")
+ password = System.getenv("OSSRH_TOKEN")
+ }
+ }
+ }
-install {
- repositories.mavenInstaller {
- pom {
- project {
- packaging 'aar'
- name 'Androidplot Core Library'
- description = "Androidplot is a library for creating dynamic and static charts within your Android application."
- url siteUrl
- licenses {
- license {
- name 'The Apache Software License, Version 2.0'
- url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ publications {
+ release(MavenPublication) {
+ from components.release
+
+ // You can then customize attributes of the publication as shown below.
+ groupId = 'com.androidplot'
+ artifactId = 'androidplot-core'
+ version = theVersionName
+
+ pom {
+ packaging 'aar'
+ name = 'Androidplot'
+ description = "Configure any object from XML."
+ url = gitUrl
+ licenses {
+ license {
+ name = 'The Apache Software License, Version 2.0'
+ url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ }
}
- }
- developers {
- developer {
- id 'halfhp'
- name 'Nick Fellows'
- email 'halfhp@gmail.com'
+ developers {
+ developer {
+ id = 'halfhp'
+ name = 'Nick Fellows'
+ email = 'halfhp@gmail.com'
+ }
+ }
+ scm {
+ connection = gitUrl
+ developerConnection = gitUrl
+ url = gitUrl
}
- }
- scm {
- connection gitUrl
- developerConnection gitUrl
- url siteUrl
-
}
}
}
}
}
-bintray {
-
- // these environment vars must be set when deploying to bintray.
- // note: BINTRAY_PASSWORD is actually the api key, but naming it 'password'
- // tells the CI environment to obfuscate the value in public logs.
- user = System.getenv('BINTRAY_USER')
- key = System.getenv('BINTRAY_PASSWORD')
-
-
- configurations = ['archives']
- pkg {
- version {
- name = theVersionName
- }
- repo = "androidplot"
- name = "com.androidplot:androidplot-core"
- issueTrackerUrl = "https://github.com/halfhp/androidplot/issues"
- websiteUrl = siteUrl
- vcsUrl = gitUrl
- licenses = ["Apache-2.0"]
- }
-}
-
-javadoc {
- options.overview = "src/main/java/overview.html"
-}
-
-artifacts {
- archives javadocJar
- archives sourcesJar
+signing {
+ def signingKey = System.getenv("SIGNING_KEY")
+ def signingPassword = System.getenv("SIGNING_PASSWORD")
+ useInMemoryPgpKeys(signingKey, signingPassword)
+ sign(publishing.publications)
}
diff --git a/androidplot-core/src/main/AndroidManifest.xml b/androidplot-core/src/main/AndroidManifest.xml
index e83183d1..3043c930 100644
--- a/androidplot-core/src/main/AndroidManifest.xml
+++ b/androidplot-core/src/main/AndroidManifest.xml
@@ -16,5 +16,5 @@
-
+
\ No newline at end of file
diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java
index 0fe39d2b..63d2e831 100644
--- a/androidplot-core/src/main/java/com/androidplot/Plot.java
+++ b/androidplot-core/src/main/java/com/androidplot/Plot.java
@@ -1,1011 +1,1077 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * 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.androidplot;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.*;
-import android.os.Build;
-import android.os.Looper;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import com.androidplot.exception.PlotRenderException;
-import com.androidplot.ui.*;
-import com.androidplot.ui.Formatter;
-import com.androidplot.ui.TextOrientation;
-import com.androidplot.ui.widget.TextLabelWidget;
-import com.androidplot.ui.SeriesRenderer;
-import com.androidplot.util.AttrUtils;
-import com.androidplot.util.DisplayDimensions;
-import com.androidplot.util.PixelUtils;
-import com.androidplot.ui.HorizontalPositioning;
-import com.androidplot.ui.VerticalPositioning;
-import com.halfhp.fig.*;
-
-import java.lang.reflect.Field;
-import java.util.*;
-
-/**
- * Base class for all Plot implementations.
- */
-public abstract class Plot,
- RegistryType extends SeriesRegistry>
- extends View implements Resizable {
- private static final String TAG = Plot.class.getName();
- private static final String XML_ATTR_PREFIX = "androidplot";
- private static final String BASE_PACKAGE = "com.androidplot.";
-
- private static final int DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP = 10;
-
- public DisplayDimensions getDisplayDimensions() {
- return displayDims;
- }
-
- /**
- * Used for caching renderer instances. Note that once a renderer is initialized it remains initialized
- * for the life of the application; does not and should not be destroyed until the application exits.
- */
- public HashMap, RendererType> getRenderers() {
- return renderers;
- }
-
- /**
- * Associates lists series and getFormatter pairs with the class of the Renderer used to render them.
- */
- public RegistryType getRegistry() {
- return registry;
- }
-
- public void setRegistry(RegistryType registry) {
- this.registry = registry;
- for(BundleType bundle : registry.getSeriesAndFormatterList()) {
- attachSeries(bundle.getSeries(), bundle.getFormatter());
- }
- }
-
- /**
- *
- * @return A new instance of RegistryType
- */
- protected abstract RegistryType getRegistryInstance();
-
- public TextLabelWidget getTitle() {
- return title;
- }
-
- public void setTitle(TextLabelWidget title) {
- this.title = title;
- }
-
- public void setTitle(String title) {
- getTitle().setText(title);
- }
-
- public enum BorderStyle {
- ROUNDED,
- SQUARE,
- NONE
- }
-
- /**
- * The RenderMode used by a Plot to draw it's self onto the screen. The RenderMode can be set
- * in two ways.
- *
- * In an xml layout:
- *
- *
- *
- *
- *
- * Programatically:
- *
- *
- * XYPlot myPlot = new XYPlot(context "MyPlot", Plot.RenderMode.USE_MAIN_THREAD);
- *
- *
- * A Plot's RenderMode cannot be changed after the plot has been initialized.
- * @since 0.5.1
- */
- public enum RenderMode {
- /**
- * Use a second thread and an off-screen buffer to do drawing. This is the preferred method
- * of drawing dynamic data and static data that consists of a large number of points. This mode
- * provides more efficient CPU utilization at the cost of increased memory usage. As of
- * version 0.5.1 this is the default RenderMode.
- *
- * XML value: use_background_thread
- * @since 0.5.1
- */
- USE_BACKGROUND_THREAD,
-
- /**
- * Do everything in the primary thread. This is the preferred method of drawing static charts
- * and dynamic data that consists of a small number of points. This mode uses less memory at
- * the cost of poor CPU utilization.
- *
- * XML value: use_main_thread
- * @since 0.5.1
- */
- USE_MAIN_THREAD
- }
- private BoxModel boxModel = new BoxModel();
-
- // no border by default:
- private BorderStyle borderStyle = Plot.BorderStyle.NONE;
- private float borderRadiusX = 15;
- private float borderRadiusY = 15;
- private Paint borderPaint;
- private Paint backgroundPaint;
- private LayoutManager layoutManager;
- private TextLabelWidget title;
- private DisplayDimensions displayDims = new DisplayDimensions();
- private RenderMode renderMode = RenderMode.USE_MAIN_THREAD;
- private final BufferedCanvas pingPong = new BufferedCanvas();
-
- // used to get rid of flickering when drawing offScreenBitmap to the visible Canvas.
- private final Object renderSynch = new Object();
-
- private HashMap, RendererType> renderers;
-
- private RegistryType registry;
- private final ArrayList listeners;
-
- private Thread renderThread;
- private boolean keepRunning = false;
- private boolean isIdle = true;
-
- {
- listeners = new ArrayList<>();
- registry = getRegistryInstance();
- renderers = new HashMap<>();
-
- borderPaint = new Paint();
- borderPaint.setColor(Color.rgb(150, 150, 150));
- borderPaint.setStyle(Paint.Style.STROKE);
- borderPaint.setStrokeWidth(1.0f);
- borderPaint.setAntiAlias(true);
- backgroundPaint = new Paint();
- backgroundPaint.setColor(Color.DKGRAY);
- backgroundPaint.setStyle(Paint.Style.FILL);
- }
-
-
- /**
- * Any rendering that utilizes a buffer from this class should synchronize rendering on the instance of this class
- * that is being used.
- */
- private static class BufferedCanvas {
- private volatile Bitmap bgBuffer; // all drawing is done on this buffer.
- private volatile Bitmap fgBuffer;
- private Canvas canvas = new Canvas();
-
- /**
- * Call this method once drawing on a Canvas retrieved by {@link #getCanvas()} to mark
- * the buffer as fully rendered. Failure to call this method will result in nothing being drawn.
- */
- public synchronized void swap() {
- Bitmap tmp = bgBuffer;
- bgBuffer = fgBuffer;
- fgBuffer = tmp;
- }
-
- public synchronized void resize(int h, int w) {
- if (w <= 0 || h <= 0) {
- bgBuffer = null;
- fgBuffer = null;
- } else {
- bgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
- fgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
- }
- }
-
- public void recycle() {
- bgBuffer.recycle();
- bgBuffer = null;
-
- fgBuffer.recycle();
- fgBuffer = null;
- System.gc();
- }
-
- /**
- * Get a Canvas for drawing. Actual drawing should be synchronized on the instance
- * of BufferedCanvas being used.
- * @return The Canvas instance to draw onto. Returns null if drawing buffers have not
- * been initialized a la {@link #resize(int, int)}.
- */
- public synchronized Canvas getCanvas() {
- if(bgBuffer != null) {
- canvas.setBitmap(bgBuffer);
- return canvas;
- } else {
- return null;
- }
- }
-
- /**
- * @return The most recent fully rendered Bitmsp
- */
- public Bitmap getBitmap() {
- return fgBuffer;
- }
- }
-
- /**
- * Convenience constructor - wraps {@link #Plot(android.content.Context, String, com.androidplot.Plot.RenderMode)}.
- * RenderMode is set to {@link RenderMode#USE_BACKGROUND_THREAD}.
- * @param context
- * @param title The display title of this Plot.
- */
- public Plot(Context context, String title) {
- this(context, title, RenderMode.USE_MAIN_THREAD);
- }
-
- /**
- * Used for programmatic instantiation.
- * @param context
- * @param title The display title of this Plot.
- */
- public Plot(Context context, String title, RenderMode mode) {
- super(context);
- this.renderMode = mode;
- init(context, null, 0);
- getTitle().setText(title);
- }
-
-
- /**
- * Required by super-class. Extending class' implementations should add
- * the following code immediately before exiting to ensure that loadAttrs
- * is called only once by the derived class:
- *
- * if(getClass().equals(DerivedPlot.class) {
- * loadAttrs(context, attrs);
- * }
- *
- *
- * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet)}
- * for an example.
- * @param context
- * @param attrs
- */
- public Plot(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context, attrs, 0);
- }
-
- /**
- * Required by super-class. Extending class' implementations should add
- * the following code immediately before exiting to ensure that loadAttrs
- * is called only once by the derived class:
- *
- * if(getClass().equals(DerivedPlot.class) {
- * loadAttrs(context, attrs);
- * }
- *
- *
- * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet, int)}
- * for an example.
- * @param context
- * @param attrs
- * @param defStyle
- */
- public Plot(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init(context, attrs, defStyle);
- }
-
- /**
- * Can be overridden by derived classes to control hardware acceleration state.
- * Note that this setting is only used on Honeycomb and later environments.
- * @return True if hardware acceleration is allowed, false otherwise.
- * @since 0.5.1
- */
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- protected boolean isHwAccelerationSupported() {
- return false;
- }
-
- /**
- * Sets the render mode used by the Plot.
- * WARNING: This method is not currently designed for general use outside of Configurator.
- * Attempting to reassign the render mode at runtime will result in unexpected behavior.
- * @param mode
- */
- public void setRenderMode(RenderMode mode) {
- this.renderMode = mode;
- }
-
- /**
- * Concrete implementations may do any final setup / initialization
- * here. Immediately following this method's invocation, AndroidPlot assumes
- * that the Plot instance is ready for final configuration via the Configurator.
- */
- protected void onPreInit() {
- // nothing to do by default
- }
-
- /**
- * Invoked immediately following configurator / styleable attr application.
- */
- protected void onAfterConfig() {
- // nothing to do by default
- }
-
- protected final void init(Context context, AttributeSet attrs, int defStyle) {
- PixelUtils.init(context);
- layoutManager = new LayoutManager();
- title = new TextLabelWidget(layoutManager, new Size(25,
- SizeMode.ABSOLUTE, 100,
- SizeMode.ABSOLUTE),
- TextOrientation.HORIZONTAL);
- title.position(0, HorizontalPositioning.RELATIVE_TO_CENTER, 0,
- VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.TOP_MIDDLE);
-
- // initialize attr defaults:
- title.getLabelPaint().setTextSize(
- PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP));
-
- onPreInit();
- // make sure the title widget is always the topmost widget:
- layoutManager.moveToTop(title);
- if(context != null && attrs != null) {
- loadAttrs(attrs, defStyle);
- }
-
- onAfterConfig();
-
- layoutManager.onPostInit();
- if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
- renderThread = new Thread(new Runnable() {
- @Override
- public void run() {
-
- keepRunning = true;
- while (keepRunning) {
- isIdle = false;
- synchronized (pingPong) {
- Canvas c = pingPong.getCanvas();
- renderOnCanvas(c);
- pingPong.swap();
- }
- synchronized (renderSynch) {
- postInvalidate();
- // prevent this thread from becoming an orphan
- // after the view is destroyed
- if (keepRunning) {
- try {
- renderSynch.wait();
- } catch (InterruptedException e) {
- keepRunning = false;
- }
- }
- }
- }
- pingPong.recycle();
- }
- });
- }
- }
-
- /**
- * If a styleable is available for the derived class, this method will be invoked with those attrs.
- * The derived implementation is only responsible for setting derived class attributes, ie. it should
- * not attempt to apply the Plot.title styleable attribute etc. Do not invoke recycle() on attrs.
- * @param attrs Attrs for the derived class.
- */
- protected abstract void processAttrs(TypedArray attrs);
-
- /**
- * Apply base class attrs.
- * @param attrs
- */
- private void processBaseAttrs(TypedArray attrs) {
-
- // markup mode
- boolean markupEnabled = attrs.getBoolean(R.styleable.Plot_markupEnabled, false);
- setMarkupEnabled(markupEnabled);
-
- // renderMode
- RenderMode renderMode = RenderMode.values()
- [attrs.getInt(R.styleable.Plot_renderMode, getRenderMode().ordinal())];
- if(renderMode != getRenderMode()) {
- setRenderMode(renderMode);
- }
-
- // margins & padding
- AttrUtils.configureBoxModelable(attrs, boxModel, R.styleable.Plot_marginTop, R.styleable.Plot_marginBottom,
- R.styleable.Plot_marginLeft, R.styleable.Plot_marginRight, R.styleable.Plot_paddingTop,
- R.styleable.Plot_paddingBottom, R.styleable.Plot_paddingLeft, R.styleable.Plot_paddingRight);
-
- // title
- getTitle().setText(attrs.getString(R.styleable.Plot_title));
- getTitle().getLabelPaint().setTextSize(
- attrs.getDimension(R.styleable.Plot_titleTextSize,
- PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP)));
-
- getTitle().getLabelPaint().setColor(attrs.getColor(
- R.styleable.Plot_titleTextColor, getTitle().getLabelPaint().getColor()));
-
- getBackgroundPaint().setColor(
- attrs.getColor(R.styleable.Plot_backgroundColor, getBackgroundPaint().getColor()));
-
- AttrUtils.configureLinePaint(attrs, getBorderPaint(),
- R.styleable.Plot_borderColor, R.styleable.Plot_borderThickness);
- }
-
- /**
- * Parse XML Attributes. Should only be called once and at the end of the base class constructor.
- * The first-pass attempts to locate styleable attributes and apply those first. After that,
- * configurator-style attributes are applied, overriding any styleable attrs that may have
- * been previously applied.
- *
- * @param attrs
- */
- private void loadAttrs(AttributeSet attrs, int defStyle) {
-
- if (attrs != null) {
-
- Field styleableFieldInR = null;
- TypedArray typedAttrs = null;
-
- Class styleableClass = R.styleable.class;
- String styleableName = getClass().getName().substring(BASE_PACKAGE.length());
- styleableName = styleableName.replace('.', '_');
- try {
- /**
- * Use reflection to safely run for the existence of styleable defs for Plot
- * and it's derivatives. This safety run is necessary to avoid runtime exceptions
- * in apps that don't include Androidplot as a .aar and won't have access to
- * the resources defined in the core library.
- */
- styleableFieldInR = styleableClass.getField(styleableName);
- } catch (NoSuchFieldException e) {
- Log.d(TAG, "Styleable definition not found for: " + styleableName);
- }
- if (styleableFieldInR != null) {
- try {
- int[] resIds = (int[]) styleableFieldInR.get(null);
- typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0);
- } catch (IllegalAccessException e) {
- // nothing to do
- } finally {
- if (typedAttrs != null) {
- // apply derived class' attrs:
- processAttrs(typedAttrs);
- typedAttrs.recycle();
- }
- }
- }
-
- try {
- styleableFieldInR = styleableClass.getField(Plot.class.getSimpleName());
- if (styleableFieldInR != null) {
- int[] resIds = (int[]) styleableFieldInR.get(null);
- typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0);
- }
- } catch (IllegalAccessException e) {
- // nothing to do
- } catch (NoSuchFieldException e) {
- Log.d(TAG, "Styleable definition not found for: " + Plot.class.getSimpleName());
- } finally {
- if (typedAttrs != null) {
- // apply base attrs:
- processBaseAttrs(typedAttrs);
- typedAttrs.recycle();
- }
- }
-
- // apply "configurator" attrs: (overrides any previously applied styleable attrs)
- // filter out androidplot prefixed attrs:
- HashMap attrHash = new HashMap();
- for (int i = 0; i < attrs.getAttributeCount(); i++) {
- String attrName = attrs.getAttributeName(i);
-
- // case insensitive run to see if this attr begins with our prefix:
- if (attrName != null && attrName.toUpperCase().startsWith(XML_ATTR_PREFIX.toUpperCase())) {
- attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i));
- }
- }
- Fig.configure(getContext(), this, attrHash);
- }
- }
-
- public RenderMode getRenderMode() {
- return renderMode;
- }
-
- public synchronized boolean addListener(PlotListener listener) {
- return !listeners.contains(listener) && listeners.add(listener);
- }
-
- public synchronized boolean removeListener(PlotListener listener) {
- return listeners.remove(listener);
- }
-
- protected ArrayList getListeners() {
- return listeners;
- }
-
- protected void notifyListenersBeforeDraw(Canvas canvas) {
- for (PlotListener listener : listeners) {
- listener.onBeforeDraw(this, canvas);
- }
- }
-
- protected void notifyListenersAfterDraw(Canvas canvas) {
- for (PlotListener listener : listeners) {
- listener.onAfterDraw(this, canvas);
- }
- }
-
- /**
- * Convenience method to add a multiple series at once using the same formatter.
- * If a problem is encountered, the method immediately returns false and the plot
- * will contain whatever series were added before the failure.
- * @param formatter
- * @param series
- * @return True if all series were successfully added, false otherwise.
- * @since 0.9.7
- */
- public synchronized boolean addSeries(FormatterType formatter, SeriesType... series) {
- for(SeriesType s : series) {
- if(!addSeries(s, formatter)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Add a new Series to the Plot.
- * @param series
- * @param formatter
- * @return True if the series was added or false if the series / formatter pair already exists in the registry.
- */
- public synchronized boolean addSeries(SeriesType series, FormatterType formatter) {
- final boolean result = getRegistry().add(series, formatter);
- attachSeries(series, formatter);
- return result;
- }
-
- protected void attachSeries(SeriesType series, FormatterType formatter) {
-
- Class rendererClass = formatter.getRendererClass();
-
- // initialize the Renderer if necessary:
- if(!getRenderers().containsKey(rendererClass)) {
- getRenderers().put(rendererClass, (RendererType) formatter.getRendererInstance(this));
- }
-
- // if this series implements PlotListener, add it as a listener:
- if(series instanceof PlotListener) {
- addListener((PlotListener)series);
- }
- }
-
- /**
- *
- * @param series
- * @param rendererClass
- * @return The {@link SeriesBundle} that matches the series and rendererClass params, or null if one is not found.
- */
- protected SeriesBundle getSeries(SeriesType series, Class extends RendererType> rendererClass) {
- for(SeriesBundle thisPair : getSeries(series)) {
- if(thisPair.getFormatter().getRendererClass() == rendererClass) {
- return thisPair;
- }
- }
- return null;
- }
-
- /**
- *
- * @param series
- * @return A List of {@link SeriesBundle} instances that reference series.
- */
- protected List> getSeries(SeriesType series) {
- return getRegistry().get(series);
- }
-
- /**
- *
- * Remove a series for a specific Renderer only. Use {@link #removeSeries(Series)} to remove the series
- * from the plot completely.
- * @param series
- * @param rendererClass
- * @return True if anything was removed, false otherwise
- */
- public synchronized boolean removeSeries(SeriesType series, Class extends RendererType> rendererClass) {
-
- List removedItems = getRegistry().remove(series, rendererClass);
-
- // if series implements PlotListener and is not assigned to any other renderers remove it as a listener:
- if (removedItems.size() == 1 && series instanceof PlotListener) {
- removeListener((PlotListener) series);
- return true;
- }
- return false;
- }
-
- /**
- * Remove all occurrences of series regardless of the associated Renderer.
- * @param series
- */
- public synchronized void removeSeries(SeriesType series) {
- // if series implements PlotListener, remove it from listeners:
- if (series instanceof PlotListener) {
- removeListener((PlotListener) series);
- }
-
- getRegistry().remove(series);
- }
-
- /**
- * Remove all series from the plot.
- */
- public void clear() {
- for(SeriesType series : getRegistry().getSeriesList()) {
- if(series instanceof PlotListener) {
- removeListener((PlotListener) series);
- }
- }
- getRegistry().clear();
- }
-
- public boolean isEmpty() {
- return getRegistry().isEmpty();
- }
-
- /**
- *
- * @param series
- * @param rendererClass
- * @return The Formatter instance corresponding to the specified series / renderer pair.
- */
- public FormatterType getFormatter(SeriesType series, Class extends RendererType> rendererClass) {
- return getSeries(series, rendererClass).getFormatter();
- }
-
- public T getRenderer(Class rendererClass) {
- return (T) getRenderers().get(rendererClass);
- }
-
- public List getRendererList() {
- return new ArrayList<>(getRenderers().values());
- }
-
- public void setMarkupEnabled(boolean enabled) {
- this.layoutManager.setMarkupEnabled(enabled);
- }
-
- /**
- * Causes the plot to be redrawn.
- * @since 0.5.1
- */
- public void redraw() {
-
- if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
-
- // only enter synchronized block if the call is expected to block OR
- // if the render thread is idle, so we know that we won't have to wait to
- // obtain a lock.
- if (isIdle) {
- synchronized (renderSynch) {
- renderSynch.notify();
- }
- }
- } else if(renderMode == RenderMode.USE_MAIN_THREAD) {
-
- // are we on the UI thread?
- if (Looper.myLooper() == Looper.getMainLooper()) {
- invalidate();
- } else {
- postInvalidate();
- }
- } else {
- throw new IllegalArgumentException("Unsupported Render Mode: " + renderMode);
- }
- }
-
- @Override
- public synchronized void layout(final DisplayDimensions dims) {
- this.displayDims = dims;
- layoutManager.layout(displayDims);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- synchronized(renderSynch) {
- keepRunning = false;
- renderSynch.notify();
- }
- }
-
-
- @Override
- protected synchronized void onSizeChanged (int w, int h, int oldw, int oldh) {
-
- // update pixel conversion values
- PixelUtils.init(getContext());
-
- // disable hardware acceleration if it's not explicitly supported
- // by the current Plot implementation. this run only applies to
- // honeycomb and later environments.
- if (Build.VERSION.SDK_INT >= 11) {
- if (!isHwAccelerationSupported() && isHardwareAccelerated()) {
- setLayerType(View.LAYER_TYPE_SOFTWARE, null);
- }
- }
-
- // pingPong is only used in background rendering mode.
- if(renderMode == RenderMode.USE_BACKGROUND_THREAD) {
- pingPong.resize(h, w);
- }
-
- RectF cRect = new RectF(0, 0, w, h);
- RectF mRect = boxModel.getMarginatedRect(cRect);
- RectF pRect = boxModel.getPaddedRect(mRect);
-
- layout(new DisplayDimensions(cRect, mRect, pRect));
- super.onSizeChanged(w, h, oldw, oldh);
- if(renderThread != null && !renderThread.isAlive()) {
- renderThread.start();
- }
- }
-
- /**
- * Called whenever the plot needs to be drawn via the Handler, which invokes invalidate().
- * Should never be called directly; use {@link #redraw()} instead.
- * @param canvas
- */
- @Override
- protected void onDraw(Canvas canvas) {
- if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
- synchronized(pingPong) {
- Bitmap bmp = pingPong.getBitmap();
- if(bmp != null) {
- canvas.drawBitmap(bmp, 0, 0, null);
- }
- }
- } else if (renderMode == RenderMode.USE_MAIN_THREAD) {
- renderOnCanvas(canvas);
- } else {
- throw new IllegalArgumentException("Unsupported Render Mode: " + renderMode);
- }
- }
-
- /**
- * Renders the plot onto a canvas. Used by both main thread to draw directly
- * onto the View's canvas as well as by background draw to render onto a
- * Bitmap buffer. At the end of the day this is the main entry for a plot's
- * "heavy lifting".
- * @param canvas
- */
- protected synchronized void renderOnCanvas(Canvas canvas) {
- try {
- // any series interested in synchronizing with plot should
- // implement PlotListener.onBeforeDraw(...) and do a read lock from within its
- // invocation. This is the entry point into that call:
- notifyListenersBeforeDraw(canvas);
- try {
- // need to completely erase what was on the canvas before redrawing, otherwise
- // some odd aliasing artifacts begin to build up around the edges of aa'd entities
- // over time.
- canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
- if (backgroundPaint != null) {
- drawBackground(canvas, displayDims.marginatedRect);
- }
-
- layoutManager.draw(canvas);
-
- if (getBorderPaint() != null) {
- drawBorder(canvas, displayDims.marginatedRect);
- }
- } catch (PlotRenderException e) {
- Log.e(TAG, "Exception while rendering Plot.", e);
- } catch (Exception e) {
- Log.e(TAG, "Exception while rendering Plot.", e);
- }
-
- isIdle = true;
- // any series interested in synchronizing with plot should
- // implement PlotListener.onAfterDraw(...) and do a read unlock from within that
- // invocation. This is the entry point for that invocation.
- notifyListenersAfterDraw(canvas);
- } finally {
- }
- }
-
-
- /**
- * Sets the visual style of the plot's border.
- * @param style
- * @param radiusX Sets the X radius for BorderStyle.ROUNDED. Use null for all other styles.
- * @param radiusY Sets the Y radius for BorderStyle.ROUNDED. Use null for all other styles.
- */
- public void setBorderStyle(BorderStyle style, Float radiusX, Float radiusY) {
- if (style == Plot.BorderStyle.ROUNDED) {
- if (radiusX == null || radiusY == null){
- throw new IllegalArgumentException("radiusX and radiusY cannot be null when using BorderStyle.ROUNDED");
- }
- this.borderRadiusX = radiusX;
- this.borderRadiusY = radiusY;
- }
- this.borderStyle = style;
- }
-
- /**
- * Draws the plot's outer border.
- * @param canvas
- * @throws PlotRenderException
- */
- protected void drawBorder(Canvas canvas, RectF dims) {
- drawRect(canvas, dims, borderPaint);
- }
-
- protected void drawBackground(Canvas canvas, RectF dims) {
- drawRect(canvas, dims, backgroundPaint);
- }
-
- protected void drawRect(Canvas canvas, RectF dims, Paint paint) {
- switch (borderStyle) {
- case ROUNDED:
- canvas.drawRoundRect(dims, borderRadiusX, borderRadiusY, paint);
- break;
- case SQUARE:
- default:
- canvas.drawRect(dims, paint);
- break;
- }
- }
-
- public LayoutManager getLayoutManager() {
- return layoutManager;
- }
-
- public void setLayoutManager(LayoutManager layoutManager) {
- this.layoutManager = layoutManager;
- }
-
- public Paint getBackgroundPaint() {
- return backgroundPaint;
- }
-
- public void setBackgroundPaint(Paint backgroundPaint) {
- this.backgroundPaint = backgroundPaint;
- }
-
- /**
- * Convenience method - wraps the individual setMarginXXX methods into a single method.
- * @param left
- * @param top
- * @param right
- * @param bottom
- */
- public void setPlotMargins(float left, float top, float right, float bottom) {
- setPlotMarginLeft(left);
- setPlotMarginTop(top);
- setPlotMarginRight(right);
- setPlotMarginBottom(bottom);
- }
-
- /**
- * Convenience method - wraps the individual setPaddingXXX methods into a single method.
- * @param left
- * @param top
- * @param right
- * @param bottom
- */
- public void setPlotPadding(float left, float top, float right, float bottom) {
- setPlotPaddingLeft(left);
- setPlotPaddingTop(top);
- setPlotPaddingRight(right);
- setPlotPaddingBottom(bottom);
- }
-
- public float getPlotMarginTop() {
- return boxModel.getMarginTop();
- }
-
- public void setPlotMarginTop(float plotMarginTop) {
- boxModel.setMarginTop(plotMarginTop);
- }
-
- public float getPlotMarginBottom() {
- return boxModel.getMarginBottom();
- }
-
- public void setPlotMarginBottom(float plotMarginBottom) {
- boxModel.setMarginBottom(plotMarginBottom);
- }
-
- public float getPlotMarginLeft() {
- return boxModel.getMarginLeft();
- }
-
- public void setPlotMarginLeft(float plotMarginLeft) {
- boxModel.setMarginLeft(plotMarginLeft);
- }
-
- public float getPlotMarginRight() {
- return boxModel.getMarginRight();
- }
-
- public void setPlotMarginRight(float plotMarginRight) {
- boxModel.setMarginRight(plotMarginRight);
- }
-
- public float getPlotPaddingTop() {
- return boxModel.getPaddingTop();
- }
-
- public void setPlotPaddingTop(float plotPaddingTop) {
- boxModel.setPaddingTop(plotPaddingTop);
- }
-
- public float getPlotPaddingBottom() {
- return boxModel.getPaddingBottom();
- }
-
- public void setPlotPaddingBottom(float plotPaddingBottom) {
- boxModel.setPaddingBottom(plotPaddingBottom);
- }
-
- public float getPlotPaddingLeft() {
- return boxModel.getPaddingLeft();
- }
-
- public void setPlotPaddingLeft(float plotPaddingLeft) {
- boxModel.setPaddingLeft(plotPaddingLeft);
- }
-
- public float getPlotPaddingRight() {
- return boxModel.getPaddingRight();
- }
-
- public void setPlotPaddingRight(float plotPaddingRight) {
- boxModel.setPaddingRight(plotPaddingRight);
- }
-
- public Paint getBorderPaint() {
- return borderPaint;
- }
-
- /**
- * Set's the paint used to draw the border. Note that this method
- * copies borderPaint and set's the copy's Paint.Style attribute to
- * Paint.Style.STROKE.
- * @param borderPaint
- */
- public void setBorderPaint(Paint borderPaint) {
- if(borderPaint == null) {
- this.borderPaint = null;
- } else {
- this.borderPaint = new Paint(borderPaint);
- this.borderPaint.setStyle(Paint.Style.STROKE);
- }
- }
-}
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * 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.androidplot;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.androidplot.ui.Anchor;
+import com.androidplot.ui.BoxModel;
+import com.androidplot.ui.Formatter;
+import com.androidplot.ui.HorizontalPositioning;
+import com.androidplot.ui.LayoutManager;
+import com.androidplot.ui.Resizable;
+import com.androidplot.ui.SeriesBundle;
+import com.androidplot.ui.SeriesRenderer;
+import com.androidplot.ui.Size;
+import com.androidplot.ui.SizeMode;
+import com.androidplot.ui.TextOrientation;
+import com.androidplot.ui.VerticalPositioning;
+import com.androidplot.ui.widget.TextLabelWidget;
+import com.androidplot.util.AttrUtils;
+import com.androidplot.util.DisplayDimensions;
+import com.androidplot.util.PixelUtils;
+import com.halfhp.fig.Fig;
+import com.halfhp.fig.FigException;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Base class for all Plot implementations.
+ */
+public abstract class Plot,
+ RegistryType extends SeriesRegistry>
+ extends View implements Resizable {
+ private static final String TAG = Plot.class.getName();
+ private static final String XML_ATTR_PREFIX = "androidplot";
+ private static final String BASE_PACKAGE = "com.androidplot.";
+
+ private static final int DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP = 10;
+
+ public DisplayDimensions getDisplayDimensions() {
+ return displayDims;
+ }
+
+ /**
+ * Used for caching renderer instances. Note that once a renderer is initialized it remains initialized
+ * for the life of the application; does not and should not be destroyed until the application exits.
+ */
+ public HashMap, RendererType> getRenderers() {
+ return renderers;
+ }
+
+ /**
+ * Associates lists series and getFormatter pairs with the class of the Renderer used to render them.
+ */
+ public RegistryType getRegistry() {
+ return registry;
+ }
+
+ public void setRegistry(RegistryType registry) {
+ this.registry = registry;
+ for(BundleType bundle : registry.getSeriesAndFormatterList()) {
+ attachSeries(bundle.getSeries(), bundle.getFormatter());
+ }
+ }
+
+ /**
+ *
+ * @return A new instance of RegistryType
+ */
+ protected abstract RegistryType getRegistryInstance();
+
+ public TextLabelWidget getTitle() {
+ return title;
+ }
+
+ public void setTitle(TextLabelWidget title) {
+ this.title = title;
+ }
+
+ public void setTitle(String title) {
+ getTitle().setText(title);
+ }
+
+ public enum BorderStyle {
+ ROUNDED,
+ SQUARE,
+ NONE
+ }
+
+ /**
+ * The RenderMode used by a Plot to draw it's self onto the screen. The RenderMode can be set
+ * in two ways.
+ *
+ * In an xml layout:
+ *
+ *
+ *
+ *
+ *
+ * Programatically:
+ *
+ *
+ * XYPlot myPlot = new XYPlot(context "MyPlot", Plot.RenderMode.USE_MAIN_THREAD);
+ *
+ *
+ * A Plot's RenderMode cannot be changed after the plot has been initialized.
+ * @since 0.5.1
+ */
+ public enum RenderMode {
+ /**
+ * Do everything in the primary thread. This is the preferred method of drawing static charts
+ * and dynamic data that consists of a small number of points. This mode uses less memory at
+ * the cost of poor CPU utilization.
+ *
+ * XML value: use_main_thread
+ * @since 0.5.1
+ */
+ USE_MAIN_THREAD,
+
+ /**
+ * Use a second thread and an off-screen buffer to do drawing. This is the preferred method
+ * of drawing dynamic data and static data that consists of a large number of points. This mode
+ * provides more efficient CPU utilization at the cost of increased memory usage. As of
+ * version 0.5.1 this is the default RenderMode.
+ *
+ * XML value: use_background_thread
+ * @since 0.5.1
+ */
+ USE_BACKGROUND_THREAD
+ }
+ private BoxModel boxModel = new BoxModel();
+
+ // no border by default:
+ private BorderStyle borderStyle = Plot.BorderStyle.NONE;
+ private float borderRadiusX = 15;
+ private float borderRadiusY = 15;
+ private Paint borderPaint;
+ private Paint backgroundPaint;
+ private LayoutManager layoutManager;
+ private TextLabelWidget title;
+ private DisplayDimensions displayDims = new DisplayDimensions();
+ private RenderMode renderMode = RenderMode.USE_MAIN_THREAD;
+ private final BufferedCanvas pingPong = new BufferedCanvas();
+
+ // used to get rid of flickering when drawing offScreenBitmap to the visible Canvas.
+ private final Object renderSync = new Object();
+
+ private HashMap, RendererType> renderers;
+
+ private RegistryType registry;
+ private final ArrayList listeners;
+
+ private Thread renderThread;
+ private boolean keepRunning = false;
+ private boolean isIdle = true;
+
+ {
+ listeners = new ArrayList<>();
+ registry = getRegistryInstance();
+ renderers = new HashMap<>();
+
+ borderPaint = new Paint();
+ borderPaint.setColor(Color.rgb(150, 150, 150));
+ borderPaint.setStyle(Paint.Style.STROKE);
+ borderPaint.setStrokeWidth(1.0f);
+ borderPaint.setAntiAlias(true);
+ backgroundPaint = new Paint();
+ backgroundPaint.setColor(Color.DKGRAY);
+ backgroundPaint.setStyle(Paint.Style.FILL);
+ }
+
+
+ /**
+ * Any rendering that utilizes a buffer from this class should synchronize rendering on the instance of this class
+ * that is being used.
+ */
+ private static class BufferedCanvas {
+ private int lastHeight = 0;
+ private int lastWidth = 0;
+ private volatile Bitmap bgBuffer; // all drawing is done on this buffer.
+ private volatile Bitmap fgBuffer;
+ private Canvas canvas = new Canvas();
+
+ /**
+ * Call this method once drawing on a Canvas retrieved by {@link #getCanvas()} to mark
+ * the buffer as fully rendered. Failure to call this method will result in nothing being drawn.
+ */
+ public synchronized void swap() {
+ Bitmap tmp = bgBuffer;
+ bgBuffer = fgBuffer;
+ fgBuffer = tmp;
+ }
+
+ /**
+ * Used when rendering in background mode and a view is attached
+ * but not resized, so that rendering buffers will be reinitialized.
+ */
+ public void resizeToLast() {
+ resize(lastHeight, lastWidth);
+ }
+
+ public synchronized void resize(int h, int w) {
+ if (w <= 0 || h <= 0) {
+ bgBuffer = null;
+ fgBuffer = null;
+ } else {
+ lastHeight = h;
+ lastWidth = w;
+ try {
+ bgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ fgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ } catch(IllegalArgumentException e) {
+ throw new IllegalArgumentException("Illegal argument passed to Bitmap.createBitmap. width: " + w + " height: " + h);
+ }
+ }
+ }
+
+ public void recycle() {
+ /**
+ * TODO: Issue #93 There have been rare reports of NPE's originating from here.
+ * Most likely there is something deeper that is amiss, but for now we'll simply
+ * do a null check before recycling.
+ */
+ if(bgBuffer != null) {
+ bgBuffer.recycle();
+ bgBuffer = null;
+ }
+
+ if(fgBuffer != null) {
+ fgBuffer.recycle();
+ fgBuffer = null;
+ }
+
+ System.gc();
+ }
+
+ /**
+ * Get a Canvas for drawing. Actual drawing should be synchronized on the instance
+ * of BufferedCanvas being used.
+ * @return The Canvas instance to draw onto. Returns null if drawing buffers have not
+ * been initialized a la {@link #resize(int, int)}.
+ */
+ public synchronized Canvas getCanvas() {
+ if(bgBuffer != null) {
+ canvas.setBitmap(bgBuffer);
+ return canvas;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return The most recent fully rendered Bitmsp
+ */
+ public Bitmap getBitmap() {
+ return fgBuffer;
+ }
+ }
+
+ /**
+ * Convenience constructor - wraps {@link #Plot(android.content.Context, String, com.androidplot.Plot.RenderMode)}.
+ * RenderMode is set to {@link RenderMode#USE_BACKGROUND_THREAD}.
+ * @param context
+ * @param title The display title of this Plot.
+ */
+ public Plot(Context context, String title) {
+ this(context, title, RenderMode.USE_MAIN_THREAD);
+ }
+
+ /**
+ * Used for programmatic instantiation.
+ * @param context
+ * @param title The display title of this Plot.
+ */
+ public Plot(Context context, String title, RenderMode mode) {
+ super(context);
+ this.renderMode = mode;
+ init(context, null, 0);
+ getTitle().setText(title);
+ }
+
+
+ /**
+ * Required by super-class. Extending class' implementations should add
+ * the following code immediately before exiting to ensure that loadAttrs
+ * is called only once by the derived class:
+ *
+ * if(getClass().equals(DerivedPlot.class) {
+ * loadAttrs(context, attrs);
+ * }
+ *
+ *
+ * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet)}
+ * for an example.
+ * @param context
+ * @param attrs
+ */
+ public Plot(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0);
+ }
+
+ /**
+ * Required by super-class. Extending class' implementations should add
+ * the following code immediately before exiting to ensure that loadAttrs
+ * is called only once by the derived class:
+ *
+ * if(getClass().equals(DerivedPlot.class) {
+ * loadAttrs(context, attrs);
+ * }
+ *
+ *
+ * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet, int)}
+ * for an example.
+ * @param context
+ * @param attrs
+ * @param defStyle
+ */
+ public Plot(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs, defStyle);
+ }
+
+ /**
+ * Can be overridden by derived classes to control hardware acceleration state.
+ * Note that this setting is only used on Honeycomb and later environments.
+ * @return True if hardware acceleration is allowed, false otherwise.
+ * @since 0.5.1
+ */
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ protected boolean isHwAccelerationSupported() {
+ return false;
+ }
+
+ /**
+ * Sets the render mode used by the Plot.
+ * WARNING: This method is not currently designed for general use outside of Configurator.
+ * Attempting to reassign the render mode at runtime will result in unexpected behavior.
+ * @param mode
+ */
+ public void setRenderMode(RenderMode mode) {
+ this.renderMode = mode;
+ }
+
+ /**
+ * Concrete implementations may do any final setup / initialization
+ * here. Immediately following this method's invocation, AndroidPlot assumes
+ * that the Plot instance is ready for final configuration via the Configurator.
+ */
+ protected void onPreInit() {
+ // nothing to do by default
+ }
+
+ /**
+ * Invoked immediately following configurator / styleable attr application.
+ */
+ protected void onAfterConfig() {
+ // nothing to do by default
+ }
+
+ protected final void init(Context context, AttributeSet attrs, int defStyle) {
+ PixelUtils.init(context);
+ layoutManager = new LayoutManager();
+ title = new TextLabelWidget(layoutManager, new Size(25,
+ SizeMode.ABSOLUTE, 100,
+ SizeMode.ABSOLUTE),
+ TextOrientation.HORIZONTAL);
+ title.position(0, HorizontalPositioning.RELATIVE_TO_CENTER, 0,
+ VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.TOP_MIDDLE);
+
+ // initialize attr defaults:
+ title.getLabelPaint().setTextSize(
+ PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP));
+
+ onPreInit();
+ // make sure the title widget is always the topmost widget:
+ layoutManager.moveToTop(title);
+ if(context != null && attrs != null) {
+ loadAttrs(attrs, defStyle);
+ }
+
+ onAfterConfig();
+
+ layoutManager.onPostInit();
+ if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
+ startBackgroundRendering();
+ }
+ }
+
+ protected void startBackgroundRendering() {
+ if(renderThread != null) {
+ return;
+ }
+
+ renderThread = new Thread(() -> {
+ System.out.println("Thread started with id " + this.hashCode());
+
+ keepRunning = true;
+ while (keepRunning) {
+ isIdle = false;
+ synchronized (pingPong) {
+ Canvas c = pingPong.getCanvas();
+ renderOnCanvas(c);
+ pingPong.swap();
+ }
+ synchronized (renderSync) {
+ postInvalidate();
+ // prevent this thread from becoming an orphan
+ // after the view is destroyed
+ if (keepRunning) {
+ try {
+ renderSync.wait();
+ } catch (InterruptedException e) {
+ keepRunning = false;
+ }
+ }
+ }
+ }
+ System.out.println("Thread exited with id " + this.hashCode());
+ renderThread = null;
+ pingPong.recycle();
+ }, "Androidplot renderThread");
+ }
+
+ /**
+ * If a styleable is available for the derived class, this method will be invoked with those attrs.
+ * The derived implementation is only responsible for setting derived class attributes, ie. it should
+ * not attempt to apply the Plot.title styleable attribute etc. Do not invoke recycle() on attrs.
+ * @param attrs Attrs for the derived class.
+ */
+ protected abstract void processAttrs(TypedArray attrs);
+
+ /**
+ * Apply base class attrs.
+ * @param attrs
+ */
+ private void processBaseAttrs(TypedArray attrs) {
+
+ // markup mode
+ boolean markupEnabled = attrs.getBoolean(R.styleable.Plot_markupEnabled, false);
+ setMarkupEnabled(markupEnabled);
+
+ // renderMode
+ RenderMode renderMode = RenderMode.values()
+ [attrs.getInt(R.styleable.Plot_renderMode, getRenderMode().ordinal())];
+ if(renderMode != getRenderMode()) {
+ setRenderMode(renderMode);
+ }
+
+ // margins & padding
+ AttrUtils.configureBoxModelable(attrs, boxModel, R.styleable.Plot_marginTop, R.styleable.Plot_marginBottom,
+ R.styleable.Plot_marginLeft, R.styleable.Plot_marginRight, R.styleable.Plot_paddingTop,
+ R.styleable.Plot_paddingBottom, R.styleable.Plot_paddingLeft, R.styleable.Plot_paddingRight);
+
+ // title
+ getTitle().setText(attrs.getString(R.styleable.Plot_title));
+ getTitle().getLabelPaint().setTextSize(
+ attrs.getDimension(R.styleable.Plot_titleTextSize,
+ PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP)));
+
+ getTitle().getLabelPaint().setColor(attrs.getColor(
+ R.styleable.Plot_titleTextColor, getTitle().getLabelPaint().getColor()));
+
+ getBackgroundPaint().setColor(
+ attrs.getColor(R.styleable.Plot_backgroundColor, getBackgroundPaint().getColor()));
+
+ AttrUtils.configureLinePaint(attrs, getBorderPaint(),
+ R.styleable.Plot_borderColor, R.styleable.Plot_borderThickness);
+ }
+
+ /**
+ * Parse XML Attributes. Should only be called once and at the end of the base class constructor.
+ * The first-pass attempts to locate styleable attributes and apply those first. After that,
+ * configurator-style attributes are applied, overriding any styleable attrs that may have
+ * been previously applied.
+ *
+ * @param attrs
+ */
+ private void loadAttrs(AttributeSet attrs, int defStyle) {
+
+ if (attrs != null) {
+
+ Field styleableFieldInR = null;
+ TypedArray typedAttrs = null;
+
+ Class styleableClass = R.styleable.class;
+ String styleableName = getClass().getName().substring(BASE_PACKAGE.length());
+ styleableName = styleableName.replace('.', '_');
+ try {
+ /**
+ * Use reflection to safely run for the existence of styleable defs for Plot
+ * and it's derivatives. This safety run is necessary to avoid runtime exceptions
+ * in apps that don't include Androidplot as a .aar and won't have access to
+ * the resources defined in the core library.
+ */
+ styleableFieldInR = styleableClass.getField(styleableName);
+ } catch (NoSuchFieldException e) {
+ Log.d(TAG, "Styleable definition not found for: " + styleableName);
+ }
+ if (styleableFieldInR != null) {
+ try {
+ int[] resIds = (int[]) styleableFieldInR.get(null);
+ typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0);
+ } catch (IllegalAccessException e) {
+ // nothing to do
+ } finally {
+ if (typedAttrs != null) {
+ // apply derived class' attrs:
+ processAttrs(typedAttrs);
+ typedAttrs.recycle();
+ }
+ }
+ }
+
+ try {
+ styleableFieldInR = styleableClass.getField(Plot.class.getSimpleName());
+ if (styleableFieldInR != null) {
+ int[] resIds = (int[]) styleableFieldInR.get(null);
+ typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0);
+ }
+ } catch (IllegalAccessException e) {
+ // nothing to do
+ } catch (NoSuchFieldException e) {
+ Log.d(TAG, "Styleable definition not found for: " + Plot.class.getSimpleName());
+ } finally {
+ if (typedAttrs != null) {
+ // apply base attrs:
+ processBaseAttrs(typedAttrs);
+ typedAttrs.recycle();
+ }
+ }
+
+ // apply "configurator" attrs: (overrides any previously applied styleable attrs)
+ // filter out androidplot prefixed attrs:
+ HashMap attrHash = new HashMap<>();
+ for (int i = 0; i < attrs.getAttributeCount(); i++) {
+ String attrName = attrs.getAttributeName(i);
+
+ // case insensitive run to see if this attr begins with our prefix:
+ if (attrName != null && attrName.toUpperCase().startsWith(XML_ATTR_PREFIX.toUpperCase())) {
+ attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i));
+ }
+ }
+ try {
+ Fig.configure(getContext(), this, attrHash);
+ } catch (FigException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public RenderMode getRenderMode() {
+ return renderMode;
+ }
+
+ public synchronized boolean addListener(PlotListener listener) {
+ return !listeners.contains(listener) && listeners.add(listener);
+ }
+
+ public synchronized boolean removeListener(PlotListener listener) {
+ return listeners.remove(listener);
+ }
+
+ protected ArrayList getListeners() {
+ return listeners;
+ }
+
+ protected void notifyListenersBeforeDraw(Canvas canvas) {
+ for (PlotListener listener : listeners) {
+ listener.onBeforeDraw(this, canvas);
+ }
+ }
+
+ protected void notifyListenersAfterDraw(Canvas canvas) {
+ for (PlotListener listener : listeners) {
+ listener.onAfterDraw(this, canvas);
+ }
+ }
+
+ /**
+ * Convenience method to add a multiple series at once using the same formatter.
+ * If a problem is encountered, the method immediately returns false and the plot
+ * will contain whatever series were added before the failure.
+ * @param formatter
+ * @param series
+ * @return True if all series were successfully added, false otherwise.
+ * @since 0.9.7
+ */
+ public synchronized boolean addSeries(FormatterType formatter, SeriesType... series) {
+ for(SeriesType s : series) {
+ if(!addSeries(s, formatter)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Add a new Series to the Plot.
+ * @param series
+ * @param formatter
+ * @return True if the series was added or false if the series / formatter pair already exists in the registry.
+ */
+ public synchronized boolean addSeries(SeriesType series, FormatterType formatter) {
+ final boolean result = getRegistry().add(series, formatter);
+ attachSeries(series, formatter);
+ return result;
+ }
+
+ protected void attachSeries(SeriesType series, FormatterType formatter) {
+
+ Class rendererClass = formatter.getRendererClass();
+
+ // initialize the Renderer if necessary:
+ if(!getRenderers().containsKey(rendererClass)) {
+ getRenderers().put(rendererClass, (RendererType) formatter.getRendererInstance(this));
+ }
+
+ // if this series implements PlotListener, add it as a listener:
+ if(series instanceof PlotListener) {
+ addListener((PlotListener)series);
+ }
+ }
+
+ /**
+ *
+ * @param series
+ * @param rendererClass
+ * @return The {@link SeriesBundle} that matches the series and rendererClass params, or null if one is not found.
+ */
+ protected SeriesBundle getSeries(SeriesType series, Class extends RendererType> rendererClass) {
+ for(SeriesBundle thisPair : getSeries(series)) {
+ if(thisPair.getFormatter().getRendererClass() == rendererClass) {
+ return thisPair;
+ }
+ }
+ return null;
+ }
+
+ /**
+ *
+ * @param series
+ * @return A List of {@link SeriesBundle} instances that reference series.
+ */
+ protected List> getSeries(SeriesType series) {
+ return getRegistry().get(series);
+ }
+
+ /**
+ *
+ * Remove a series for a specific Renderer only. Use {@link #removeSeries(Series)} to remove the series
+ * from the plot completely.
+ * @param series
+ * @param rendererClass
+ * @return True if anything was removed, false otherwise
+ */
+ public synchronized boolean removeSeries(SeriesType series, Class extends RendererType> rendererClass) {
+
+ List removedItems = getRegistry().remove(series, rendererClass);
+
+ // if series implements PlotListener and is not assigned to any other renderers remove it as a listener:
+ if (removedItems.size() == 1 && series instanceof PlotListener) {
+ removeListener((PlotListener) series);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Remove all occurrences of series regardless of the associated Renderer.
+ * @param series
+ */
+ public synchronized void removeSeries(SeriesType series) {
+ // if series implements PlotListener, remove it from listeners:
+ if (series instanceof PlotListener) {
+ removeListener((PlotListener) series);
+ }
+
+ getRegistry().remove(series);
+ }
+
+ /**
+ * Remove all series from the plot.
+ */
+ public void clear() {
+ for(SeriesType series : getRegistry().getSeriesList()) {
+ if(series instanceof PlotListener) {
+ removeListener((PlotListener) series);
+ }
+ }
+ getRegistry().clear();
+ }
+
+ public boolean isEmpty() {
+ return getRegistry().isEmpty();
+ }
+
+ /**
+ *
+ * @param series
+ * @param rendererClass
+ * @return The Formatter instance corresponding to the specified series / renderer pair.
+ */
+ public FormatterType getFormatter(SeriesType series, Class extends RendererType> rendererClass) {
+ return getSeries(series, rendererClass).getFormatter();
+ }
+
+ public T getRenderer(Class rendererClass) {
+ return (T) getRenderers().get(rendererClass);
+ }
+
+ public List getRendererList() {
+ return new ArrayList<>(getRenderers().values());
+ }
+
+ public void setMarkupEnabled(boolean enabled) {
+ this.layoutManager.setMarkupEnabled(enabled);
+ }
+
+ /**
+ * Causes the plot to be redrawn.
+ * @since 0.5.1
+ */
+ public void redraw() {
+
+ if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
+
+ // only enter synchronized block if the call is expected to block OR
+ // if the render thread is idle, so we know that we won't have to wait to
+ // obtain a lock.
+ if (renderThread != null && isIdle) {
+ synchronized (renderSync) {
+ renderSync.notify();
+ }
+ }
+
+ } else if(renderMode == RenderMode.USE_MAIN_THREAD) {
+
+ // are we on the UI thread?
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ invalidate();
+ } else {
+ postInvalidate();
+ }
+ } else {
+ throw new IllegalArgumentException("Unsupported Render Mode: " + renderMode);
+ }
+ }
+
+ @Override
+ public synchronized void layout(final DisplayDimensions dims) {
+ this.displayDims = dims;
+ layoutManager.layout(displayDims);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ synchronized(renderSync) {
+ keepRunning = false;
+ renderSync.notify();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // necessary to support rendering in recyclerview etc.
+ if(renderMode == RenderMode.USE_BACKGROUND_THREAD && renderThread == null) {
+ pingPong.resizeToLast();
+ startBackgroundRendering();
+ renderThread.start();
+ }
+ }
+
+ @Override
+ protected synchronized void onSizeChanged (int w, int h, int oldw, int oldh) {
+
+ // update pixel conversion values
+ PixelUtils.init(getContext());
+
+ // disable hardware acceleration if it's not explicitly supported
+ // by the current Plot implementation. this run only applies to
+ // honeycomb and later environments.
+ if (Build.VERSION.SDK_INT >= 11) {
+ if (!isHwAccelerationSupported() && isHardwareAccelerated()) {
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ }
+
+ // pingPong is only used in background rendering mode.
+ if(renderMode == RenderMode.USE_BACKGROUND_THREAD) {
+ pingPong.resize(h, w);
+ }
+
+ RectF cRect = new RectF(0, 0, w, h);
+ RectF mRect = boxModel.getMarginatedRect(cRect);
+ RectF pRect = boxModel.getPaddedRect(mRect);
+
+ layout(new DisplayDimensions(cRect, mRect, pRect));
+ super.onSizeChanged(w, h, oldw, oldh);
+ if(renderThread != null && !renderThread.isAlive()) {
+ renderThread.start();
+ }
+ }
+
+ /**
+ * Called whenever the plot needs to be drawn via the Handler, which invokes invalidate().
+ * Should never be called directly; use {@link #redraw()} instead.
+ * @param canvas
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
+ synchronized(pingPong) {
+ Bitmap bmp = pingPong.getBitmap();
+ if(bmp != null) {
+ canvas.drawBitmap(bmp, 0, 0, null);
+ }
+ }
+ } else if (renderMode == RenderMode.USE_MAIN_THREAD) {
+ renderOnCanvas(canvas);
+ } else {
+ throw new IllegalArgumentException("Unsupported Render Mode: " + renderMode);
+ }
+ }
+
+ /**
+ * Renders the plot onto a canvas. Used by both main thread to draw directly
+ * onto the View's canvas as well as by background draw to render onto a
+ * Bitmap buffer. At the end of the day this is the main entry for a plot's
+ * "heavy lifting".
+ * @param canvas
+ */
+ protected synchronized void renderOnCanvas(@Nullable Canvas canvas) {
+ if(canvas == null) {
+ return;
+ }
+ try {
+ // any series interested in synchronizing with plot should
+ // implement PlotListener.onBeforeDraw(...) and do a read lock from within its
+ // invocation. This is the entry point into that call:
+ notifyListenersBeforeDraw(canvas);
+ try {
+ // need to completely erase what was on the canvas before redrawing, otherwise
+ // some odd aliasing artifacts begin to build up around the edges of aa'd entities
+ // over time.
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+ if (backgroundPaint != null) {
+ drawBackground(canvas, displayDims.marginatedRect);
+ }
+
+ layoutManager.draw(canvas);
+
+ if (getBorderPaint() != null) {
+ drawBorder(canvas, displayDims.marginatedRect);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while rendering Plot.", e);
+ }
+
+ isIdle = true;
+ // any series interested in synchronizing with plot should
+ // implement PlotListener.onAfterDraw(...) and do a read unlock from within that
+ // invocation. This is the entry point for that invocation.
+ notifyListenersAfterDraw(canvas);
+ } finally {
+ }
+ }
+
+
+ /**
+ * Sets the visual style of the plot's border.
+ * @param style
+ * @param radiusX Sets the X radius for BorderStyle.ROUNDED. Use null for all other styles.
+ * @param radiusY Sets the Y radius for BorderStyle.ROUNDED. Use null for all other styles.
+ */
+ public void setBorderStyle(BorderStyle style, Float radiusX, Float radiusY) {
+ if (style == Plot.BorderStyle.ROUNDED) {
+ if (radiusX == null || radiusY == null){
+ throw new IllegalArgumentException("radiusX and radiusY cannot be null when using BorderStyle.ROUNDED");
+ }
+ this.borderRadiusX = radiusX;
+ this.borderRadiusY = radiusY;
+ }
+ this.borderStyle = style;
+ }
+
+ /**
+ * Draws the plot's outer border.
+ * @param canvas
+ */
+ protected void drawBorder(Canvas canvas, RectF dims) {
+ drawRect(canvas, dims, borderPaint);
+ }
+
+ protected void drawBackground(Canvas canvas, RectF dims) {
+ drawRect(canvas, dims, backgroundPaint);
+ }
+
+ protected void drawRect(Canvas canvas, RectF dims, Paint paint) {
+ switch (borderStyle) {
+ case ROUNDED:
+ canvas.drawRoundRect(dims, borderRadiusX, borderRadiusY, paint);
+ break;
+ case SQUARE:
+ default:
+ canvas.drawRect(dims, paint);
+ break;
+ }
+ }
+
+ public LayoutManager getLayoutManager() {
+ return layoutManager;
+ }
+
+ public void setLayoutManager(LayoutManager layoutManager) {
+ this.layoutManager = layoutManager;
+ }
+
+ public Paint getBackgroundPaint() {
+ return backgroundPaint;
+ }
+
+ public void setBackgroundPaint(Paint backgroundPaint) {
+ this.backgroundPaint = backgroundPaint;
+ }
+
+ /**
+ * Convenience method - wraps the individual setMarginXXX methods into a single method.
+ * @param left
+ * @param top
+ * @param right
+ * @param bottom
+ */
+ public void setPlotMargins(float left, float top, float right, float bottom) {
+ setPlotMarginLeft(left);
+ setPlotMarginTop(top);
+ setPlotMarginRight(right);
+ setPlotMarginBottom(bottom);
+ }
+
+ /**
+ * Convenience method - wraps the individual setPaddingXXX methods into a single method.
+ * @param left
+ * @param top
+ * @param right
+ * @param bottom
+ */
+ public void setPlotPadding(float left, float top, float right, float bottom) {
+ setPlotPaddingLeft(left);
+ setPlotPaddingTop(top);
+ setPlotPaddingRight(right);
+ setPlotPaddingBottom(bottom);
+ }
+
+ public float getPlotMarginTop() {
+ return boxModel.getMarginTop();
+ }
+
+ public void setPlotMarginTop(float plotMarginTop) {
+ boxModel.setMarginTop(plotMarginTop);
+ }
+
+ public float getPlotMarginBottom() {
+ return boxModel.getMarginBottom();
+ }
+
+ public void setPlotMarginBottom(float plotMarginBottom) {
+ boxModel.setMarginBottom(plotMarginBottom);
+ }
+
+ public float getPlotMarginLeft() {
+ return boxModel.getMarginLeft();
+ }
+
+ public void setPlotMarginLeft(float plotMarginLeft) {
+ boxModel.setMarginLeft(plotMarginLeft);
+ }
+
+ public float getPlotMarginRight() {
+ return boxModel.getMarginRight();
+ }
+
+ public void setPlotMarginRight(float plotMarginRight) {
+ boxModel.setMarginRight(plotMarginRight);
+ }
+
+ public float getPlotPaddingTop() {
+ return boxModel.getPaddingTop();
+ }
+
+ public void setPlotPaddingTop(float plotPaddingTop) {
+ boxModel.setPaddingTop(plotPaddingTop);
+ }
+
+ public float getPlotPaddingBottom() {
+ return boxModel.getPaddingBottom();
+ }
+
+ public void setPlotPaddingBottom(float plotPaddingBottom) {
+ boxModel.setPaddingBottom(plotPaddingBottom);
+ }
+
+ public float getPlotPaddingLeft() {
+ return boxModel.getPaddingLeft();
+ }
+
+ public void setPlotPaddingLeft(float plotPaddingLeft) {
+ boxModel.setPaddingLeft(plotPaddingLeft);
+ }
+
+ public float getPlotPaddingRight() {
+ return boxModel.getPaddingRight();
+ }
+
+ public void setPlotPaddingRight(float plotPaddingRight) {
+ boxModel.setPaddingRight(plotPaddingRight);
+ }
+
+ public Paint getBorderPaint() {
+ return borderPaint;
+ }
+
+ /**
+ * Set's the paint used to draw the border. Note that this method
+ * copies borderPaint and set's the copy's Paint.Style attribute to
+ * Paint.Style.STROKE.
+ * @param borderPaint
+ */
+ public void setBorderPaint(Paint borderPaint) {
+ if(borderPaint == null) {
+ this.borderPaint = null;
+ } else {
+ this.borderPaint = new Paint(borderPaint);
+ this.borderPaint.setStyle(Paint.Style.STROKE);
+ }
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/Region.java b/androidplot-core/src/main/java/com/androidplot/Region.java
index 2ace04a7..f22f9115 100644
--- a/androidplot-core/src/main/java/com/androidplot/Region.java
+++ b/androidplot-core/src/main/java/com/androidplot/Region.java
@@ -17,7 +17,7 @@
package com.androidplot;
-import com.androidplot.util.*;
+import com.androidplot.util.FastNumber;
/**
* A one dimensional region represented by a starting and ending value.
@@ -71,7 +71,7 @@ public Number length() {
Number l = getMax() == null || getMin() == null ?
null : getMax().doubleValue() - getMin().doubleValue();
if(l != null) {
- cachedLength = new FastNumber(l);
+ cachedLength = FastNumber.orNull(l);
}
}
return cachedLength;
@@ -215,8 +215,8 @@ public void setMin(Number min) {
} else {
this.min = null;
}
- } else {
- this.min = new FastNumber(min);
+ } else if (this.min == null || !this.min.equals(min)) {
+ this.min = FastNumber.orNull(min);
}
}
@@ -237,8 +237,8 @@ public void setMax(Number max) {
} else {
this.max = null;
}
- } else {
- this.max = new FastNumber(max);
+ } else if (this.max == null || !this.max.equals(max)) {
+ this.max = FastNumber.orNull(max);
}
}
@@ -249,4 +249,20 @@ public void setMax(Number max) {
public boolean isDefined() {
return min != null && max != null;
}
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("Region{");
+ sb.append("min=").append(min);
+ sb.append(", max=").append(max);
+ sb.append(", cachedLength=").append(cachedLength);
+ sb.append(", defaults=");
+ if (defaults != this) {
+ sb.append(defaults);
+ } else {
+ sb.append("this");
+ }
+ sb.append('}');
+ return sb.toString();
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java
index 948d1d11..7a7b3023 100644
--- a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java
+++ b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java
@@ -19,7 +19,7 @@
import com.androidplot.ui.Formatter;
import com.androidplot.ui.SeriesBundle;
-import java.io.*;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -38,7 +38,7 @@ public List getSeriesAndFormatterList() {
return registry;
}
public List getSeriesList() {
- List result = new ArrayList<>();
+ List result = new ArrayList<>(registry.size());
for(SeriesBundle sfPair : registry) {
result.add(sfPair.getSeries());
}
@@ -54,6 +54,9 @@ public boolean isEmpty() {
}
public boolean add(SeriesType series, FormatterType formatter) {
+ if(series == null || formatter == null) {
+ throw new IllegalArgumentException("Neither series nor formatter param may be null.");
+ }
return registry.add(newSeriesBundle(series, formatter));
}
@@ -122,4 +125,13 @@ public List> getLegendEnabledItems() {
}
return sfList;
}
+
+ public boolean contains(SeriesType series, Class extends FormatterType> formatterClass) {
+ for(BundleType b : registry) {
+ if(b.getFormatter().getClass() == formatterClass && b.getSeries() == series) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java b/androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java
deleted file mode 100644
index 62a6e6d9..00000000
--- a/androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * 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.androidplot.exception;
-
-public class PlotRenderException extends Exception {
- public PlotRenderException(String message) {
- super(message);
- }
-}
diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java
index 615efaaa..35e5e416 100644
--- a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java
+++ b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java
@@ -37,15 +37,18 @@ public class PieChart extends Plot {
+
+ private PieChart pieChart;
+
+ public PieLegendWidget(LayoutManager layoutManager, PieChart pieChart,
+ Size widgetSize,
+ TableModel tableModel,
+ Size iconSize) {
+ super(tableModel, layoutManager, widgetSize, iconSize);
+ this.pieChart = pieChart;
+ }
+
+ @Override
+ protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull PieLegendItem item) {
+ canvas.drawRect(iconRect, item.formatter.getFillPaint());
+ }
+
+ @Override
+ protected List getLegendItems() {
+ final List legendItems = new ArrayList<>();
+ for(SeriesBundle item : pieChart.getRegistry().getLegendEnabledItems()) {
+ legendItems.add(new PieLegendItem(item.getSeries(), item.getFormatter()));
+ }
+ return legendItems;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java
index 5d52d94b..db8921dc 100644
--- a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java
@@ -18,7 +18,6 @@
import android.graphics.*;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.ui.SeriesBundle;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.ui.RenderStack;
@@ -30,10 +29,14 @@
*/
public class PieRenderer extends SeriesRenderer {
+ private static final float FULL_PIE_DEGS = 360f;
+ private static final float HALF_PIE_DEGS = 180f;
+
// starting angle to use when drawing the first radial line of the first segment.
- @SuppressWarnings("FieldCanBeLocal")
- private float startDeg = 0;
- private float endDeg = 360;
+ private float startDegs = 0;
+
+ // number of degrees to extend from startDegs; can be used to "shape" the pie chart.
+ private float extentDegs = FULL_PIE_DEGS;
// TODO: express donut in units other than px.
private float donutSize = 0.5f;
@@ -41,7 +44,6 @@ public class PieRenderer extends SeriesRenderer sfPair : getSeriesAndFormatterList()) {
float lastOffset = offset;
- float sweep = (float) (scale * (values[i]) * 360);
+ float sweep = (float) (scale * (values[i]) * extentDegs);
offset += sweep;
- drawSegment(canvas, rec, sfPair.getSeries(), sfPair.getFormatter(), radius, lastOffset, sweep);
+ drawSegment(canvas, rec, sfPair.getSeries(), sfPair.getFormatter(), radius, lastOffset,
+ sweep);
i++;
}
}
protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f,
- float rad, float startAngle, float sweep) {
+ float rad, float startAngle, float sweep) {
canvas.save();
startAngle = startAngle + f.getRadialInset();
sweep = sweep - (f.getRadialInset() * 2);
// midpoint angle between startAngle and endAngle
- final float halfSweepEndAngle = startAngle + (sweep/2);
+ final float halfSweepEndAngle = startAngle + (sweep / 2);
PointF translated = calculateLineEnd(
bounds.centerX(), bounds.centerY(), f.getOffset(), halfSweepEndAngle);
@@ -95,24 +100,22 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm
final float cy = translated.y;
float donutSizePx;
- switch(donutMode) {
+ switch (donutMode) {
case PERCENT:
donutSizePx = donutSize * rad;
break;
case PIXELS:
- donutSizePx = (donutSize > 0)?donutSize:(rad + donutSize);
+ donutSizePx = (donutSize > 0) ? donutSize : (rad + donutSize);
break;
default:
- throw new UnsupportedOperationException("Not yet implemented.");
+ throw new UnsupportedOperationException("Unsupported DonutMde: " + donutMode);
}
final float outerRad = rad - f.getOuterInset();
- final float innerRad = donutSizePx == 0 ? 0 : donutSizePx + f.getInnerInset();
- //final float outerRad = rad;
- //final float innerRad = donutSizePx;
+ final float innerRad = donutSizePx == 0 ? 0 : donutSizePx + f.getInnerInset();
- // do we have a pie chart of less than 100%
- if(Math.abs(sweep - 360f) > Float.MIN_VALUE) {
+ // do we have a segment of less than 100%
+ if (Math.abs(sweep - extentDegs) > Float.MIN_VALUE) {
// vertices of the first radial:
PointF r1Outer = calculateLineEnd(cx, cy, outerRad, startAngle);
PointF r1Inner = calculateLineEnd(cx, cy, innerRad, startAngle);
@@ -129,9 +132,9 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm
// and cannot be easily anti aliased. Really we only care about masking off the
// radial edges.
clip.arcTo(new RectF(bounds.left - outerRad,
- bounds.top - outerRad,
- bounds.right + outerRad,
- bounds.bottom + outerRad),
+ bounds.top - outerRad,
+ bounds.right + outerRad,
+ bounds.bottom + outerRad),
startAngle, sweep);
clip.lineTo(cx, cy);
clip.close();
@@ -141,20 +144,20 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm
// outer arc:
p.arcTo(new RectF(
- cx - outerRad,
- cy - outerRad,
- cx + outerRad,
- cy + outerRad)
+ cx - outerRad,
+ cy - outerRad,
+ cx + outerRad,
+ cy + outerRad)
, startAngle, sweep);
p.lineTo(r2Inner.x, r2Inner.y);
// inner arc:
// sweep back to original angle:
p.arcTo(new RectF(
- cx - innerRad,
- cy - innerRad,
- cx + innerRad,
- cy + innerRad),
+ cx - innerRad,
+ cy - innerRad,
+ cx + innerRad,
+ cy + innerRad),
startAngle + sweep, -sweep);
p.close();
@@ -165,9 +168,8 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm
// draw radial lines
canvas.drawLine(r1Inner.x, r1Inner.y, r1Outer.x, r1Outer.y, f.getRadialEdgePaint());
canvas.drawLine(r2Inner.x, r2Inner.y, r2Outer.x, r2Outer.y, f.getRadialEdgePaint());
- }
- else {
- canvas.save(Canvas.CLIP_SAVE_FLAG);
+ } else {
+ canvas.save();
Path chart = new Path();
chart.addCircle(cx, cy, outerRad, Path.Direction.CW);
Path inside = new Path();
@@ -185,18 +187,18 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm
canvas.restore();
PointF labelOrigin = calculateLineEnd(cx, cy,
- (outerRad-((outerRad- innerRad)/2)), halfSweepEndAngle);
+ (outerRad - ((outerRad - innerRad) / 2)), halfSweepEndAngle);
// TODO: move segment labelling outside the segment drawing loop
// TODO: so that the labels will not be clipped by the edge of the next
// TODO: segment being drawn.
- if(f.getLabelPaint() != null) {
+ if (f.getLabelPaint() != null) {
drawSegmentLabel(canvas, labelOrigin, seg, f);
}
}
protected void drawSegmentLabel(Canvas canvas, PointF origin,
- Segment seg, SegmentFormatter f) {
+ Segment seg, SegmentFormatter f) {
canvas.drawText(seg.getTitle(), origin.x, origin.y, f.getLabelPaint());
}
@@ -213,22 +215,26 @@ protected void doDrawLegendIcon(Canvas canvas, RectF rect, SegmentFormatter form
protected double calculateScale(double[] values) {
double total = 0;
for (int i = 0; i < values.length; i++) {
- total += values[i];
- }
+ total += values[i];
+ }
return (1d / total);
}
-
- protected double[] getValues() {
+
+ /**
+ * Retreive the raw values being rendered from each {@link Segment}.
+ * @return
+ */
+ protected double[] getValues() {
List> seriesList = getSeriesAndFormatterList();
- double[] result = new double[seriesList.size()];
- int i = 0;
- for (SeriesBundle sfPair : seriesList) {
- result[i] = sfPair.getSeries().getValue().doubleValue();
- i++;
- }
- return result;
- }
+ double[] result = new double[seriesList.size()];
+ int i = 0;
+ for (SeriesBundle sfPair : seriesList) {
+ result[i] = sfPair.getSeries().getValue().doubleValue();
+ i++;
+ }
+ return result;
+ }
protected PointF calculateLineEnd(float x, float y, float rad, float deg) {
return calculateLineEnd(new PointF(x, y), rad, deg);
@@ -236,7 +242,7 @@ protected PointF calculateLineEnd(float x, float y, float rad, float deg) {
protected PointF calculateLineEnd(PointF origin, float rad, float deg) {
- double radians = deg * Math.PI / 180F;
+ double radians = deg * Math.PI / HALF_PIE_DEGS;
double x = rad * Math.cos(radians);
double y = rad * Math.sin(radians);
@@ -244,16 +250,26 @@ protected PointF calculateLineEnd(PointF origin, float rad, float deg) {
return new PointF(origin.x + (float) x, origin.y + (float) y);
}
+ /**
+ * Set the size of the pie's empty inner space. May be specified as either pixels or as a percentage.
+ * If using {@link DonutMode#PIXELS}, the best practice is to specify values in dp using
+ * {@link com.androidplot.util.PixelUtils#dpToPix(float)}.
+ *
+ * If using {@link DonutMode#PERCENT} the value must be within the range 0 - 1. The value being
+ * set corresponds to the size of the donut radius relative to the pie's total radius.
+ * @param size
+ * @param mode
+ */
public void setDonutSize(float size, DonutMode mode) {
- switch(mode) {
+ switch (mode) {
case PERCENT:
- if(size < 0 || size > 1) {
+ if (size < 0 || size > 1) {
throw new IllegalArgumentException(
"Size parameter must be between 0 and 1 when operating in PERCENT mode.");
}
break;
case PIXELS:
- break;
+ break;
default:
throw new UnsupportedOperationException("Not yet implemented.");
}
@@ -264,7 +280,7 @@ public void setDonutSize(float size, DonutMode mode) {
/**
* Retrieve the segment containing the specified point. This current implementation
* only matches against angle; clicks outside of the pie/donut inner/outer boundaries
- * will still trigger a match on the segment whose begining and ending angle contains
+ * will still trigger a match on the segment whose beginning and ending angle contains
* the angle of the line drawn between the pie chart's center point and the clicked point.
* @param point The clicked point
* @return Segment containing the clicked point.
@@ -278,11 +294,10 @@ public Segment getContainingSegment(PointF point) {
float dx = point.x - origin.x;
float dy = point.y - origin.y;
double theta = Math.atan2(dy, dx);
- double angle = (theta * (180f/Math.PI));
- if(angle < 0) {
- // convert angle to 0-360 range with 0 being in the
- // traditional "westerly" orientation:
- angle += 360;
+ double angle = (theta * (HALF_PIE_DEGS / Math.PI));
+ if (angle < 0) {
+ // bring into 0-360 range
+ angle += FULL_PIE_DEGS;
}
// find the segment whose starting and ending angle (degs) contains
@@ -291,33 +306,97 @@ public Segment getContainingSegment(PointF point) {
int i = 0;
double[] values = getValues();
double scale = calculateScale(values);
- float offset = startDeg;
+ float offset = degsToScreenDegs(startDegs);
for (SeriesBundle sfPair : seriesList) {
float lastOffset = offset;
- float sweep = (float) (scale * (values[i]) * 360);
+ float sweep = (float) (scale * (values[i]) * extentDegs);
offset += sweep;
-
- if(angle >= lastOffset && angle <= offset) {
+ offset = offset % FULL_PIE_DEGS;
+
+ final double dist = signedDistance(offset, angle);
+ double endDist = signedDistance(offset, lastOffset);
+ if(endDist < 0) {
+ // segment accounts for more than 50% of the pie and wrapped around
+ // need to correct:
+ endDist = FULL_PIE_DEGS + endDist;
+ }
+ if(dist > 0 && dist <= endDist) {
return sfPair.getSeries();
}
i++;
}
return null;
}
-
- public void setStartDeg(float deg) {
- startDeg = deg;
+
+ /**
+ * convert conventional degrees (90 degrees is north) to screen degrees (90 degrees is south)
+ * Values >= 369 will be converted back into the range of 0 - 359.999...
+ * @param degs
+ * @return
+ */
+ protected static float degsToScreenDegs(float degs) {
+ degs = degs % FULL_PIE_DEGS;
+
+ if (degs > 0) {
+ return FULL_PIE_DEGS - degs;
+ } else {
+ return degs;
+ }
+ }
+
+ /**
+ * Compute the signed shortest angular distance between two angles
+ * @param angle1
+ * @param angle2
+ * @return
+ */
+ protected static double signedDistance(double angle1, double angle2) {
+ double d = Math.abs(angle1 - angle2) % FULL_PIE_DEGS;
+ double r = d > HALF_PIE_DEGS ? FULL_PIE_DEGS - d : d;
+
+ //calculate sign
+ int sign = (angle1 - angle2 >= 0 && angle1 - angle2 <= HALF_PIE_DEGS)
+ || (angle1 - angle2 <= -HALF_PIE_DEGS && angle1 - angle2 >= -FULL_PIE_DEGS) ? 1 : -1;
+ r *= sign;
+ return r;
+ }
+
+ /**
+ * Throws an IllegalArgumentException if the input value is outside of the range of 0.0 to 360.0
+ * @param degs
+ */
+ protected static void validateInputDegs(float degs) {
+ if(degs < 0 || degs > FULL_PIE_DEGS) {
+ throw new IllegalArgumentException("Degrees values must be between 0.0 and 360.");
+ }
+ }
+
+ /**
+ * Set the starting point in degrees from which series will be drawn in order.
+ * The input value must be within the range 0 - 360.
+ * @param degs
+ */
+ public void setStartDegs(float degs) {
+ validateInputDegs(degs);
+ startDegs = degs;
}
- public float getStartDeg() {
- return startDeg;
+ public float getStartDegs() {
+ return startDegs;
}
-
- public void setEndDeg(float deg) {
- endDeg = deg;
+
+ /**
+ * Set the size in degrees of the pie chart, extending from the startDegs. The input value
+ * must be within the range 0 - 360. An input value would represent 100% of the pie as a half
+ * circle while an input value of 360 would be a full circle.
+ * @param degs
+ */
+ public void setExtentDegs(float degs) {
+ validateInputDegs(degs);
+ extentDegs = degs;
}
- public float getEndDeg() {
- return endDeg;
+ public float getExtentDegs() {
+ return extentDegs;
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieWidget.java b/androidplot-core/src/main/java/com/androidplot/pie/PieWidget.java
index 38c23b27..1b7b2a80 100644
--- a/androidplot-core/src/main/java/com/androidplot/pie/PieWidget.java
+++ b/androidplot-core/src/main/java/com/androidplot/pie/PieWidget.java
@@ -17,7 +17,6 @@
package com.androidplot.pie;
import android.graphics.*;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.ui.LayoutManager;
import com.androidplot.ui.Size;
import com.androidplot.ui.widget.Widget;
@@ -38,7 +37,7 @@ public PieWidget(LayoutManager layoutManager, PieChart pieChart, Size metrics) {
}
@Override
- protected void doOnDraw(Canvas canvas, RectF widgetRect) throws PlotRenderException {
+ protected void doOnDraw(Canvas canvas, RectF widgetRect) {
renderStack.sync();
for(RenderStack.StackElement thisElement : renderStack.getElements()) {
if(thisElement.isEnabled()) {
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java b/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java
index c7c7f066..ff973b9d 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java
@@ -149,10 +149,10 @@ public TableModelIterator(DynamicTableModel dynamicTableModel, RectF tableRect,
calculatedRows = dynamicTableModel.getNumRows();
// round up:
- calculatedColumns = new Float((totalElements / (float) calculatedRows) + 0.5).intValue();
+ calculatedColumns = Float.valueOf((totalElements / (float) calculatedRows) + 0.5f).intValue();
} else if(dynamicTableModel.getNumRows() == 0 && dynamicTableModel.getNumColumns() >= 1) {
calculatedColumns = dynamicTableModel.getNumColumns();
- calculatedRows = new Float((totalElements / (float) calculatedColumns) + 0.5).intValue();
+ calculatedRows = Float.valueOf((totalElements / (float) calculatedColumns) + 0.5f).intValue();
// unlimited rows and columns (impossible) so default a single row with n columns:
}else if(dynamicTableModel.getNumColumns() == 0 && dynamicTableModel.getNumRows() == 0) {
calculatedRows = 1;
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java
index 8f2a354b..d920ab36 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java
@@ -48,7 +48,11 @@ public Formatter(Context ctx, int xmlCfgId) {
}
public void configure(Context ctx, int xmlCfgId) {
- Fig.configure(ctx, this, xmlCfgId);
+ try {
+ Fig.configure(ctx, this, xmlCfgId);
+ } catch (FigException e) {
+ throw new RuntimeException(e);
+ }
}
/**
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java b/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java
index 013cb003..45f79a43 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java
@@ -16,10 +16,15 @@
package com.androidplot.ui;
-import android.graphics.*;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.Region;
import android.view.MotionEvent;
import android.view.View;
-import com.androidplot.exception.PlotRenderException;
+
import com.androidplot.ui.widget.Widget;
import com.androidplot.util.DisplayDimensions;
import com.androidplot.util.LinkedLayerList;
@@ -77,7 +82,7 @@ public void setMarkupEnabled(boolean enabled) {
setDrawOutlineShadowsEnabled(enabled);
}
- public void draw(Canvas canvas) throws PlotRenderException {
+ public void draw(Canvas canvas) {
if(isDrawMarginsEnabled()) {
drawSpacing(canvas, displayDims.canvasRect, displayDims.marginatedRect, marginPaint);
}
@@ -85,13 +90,12 @@ public void draw(Canvas canvas) throws PlotRenderException {
drawSpacing(canvas, displayDims.marginatedRect, displayDims.paddedRect, paddingPaint);
}
for (Widget widget : elements()) {
- //int canvasState = canvas.save(Canvas.ALL_SAVE_FLAG); // preserve clipping etc
try {
- canvas.save(Canvas.ALL_SAVE_FLAG);
+ canvas.save();
PositionMetrics metrics = widget.getPositionMetrics();
float elementWidth = widget.getWidthPix(displayDims.paddedRect.width());
float elementHeight = widget.getHeightPix(displayDims.paddedRect.height());
- PointF coords = widget.getElementCoordinates(elementHeight,
+ PointF coords = Widget.calculateCoordinates(elementHeight,
elementWidth, displayDims.paddedRect, metrics);
DisplayDimensions dims = widget.getWidgetDimensions();
@@ -133,9 +137,9 @@ public void draw(Canvas canvas) throws PlotRenderException {
}
}
- private void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) {
+ private static void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) {
try {
- canvas.save(Canvas.ALL_SAVE_FLAG);
+ canvas.save();
canvas.clipRect(inner, Region.Op.DIFFERENCE);
canvas.drawRect(outer, paint);
} finally {
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java b/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java
index 3381f20f..377e9e04 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java
@@ -16,6 +16,8 @@
package com.androidplot.ui;
+import androidx.annotation.NonNull;
+
public class PositionMetrics implements Comparable {
private HorizontalPosition horizontalPosition;
@@ -47,7 +49,7 @@ public void setAnchor(Anchor anchor) {
}
@Override
- public int compareTo(PositionMetrics o) {
+ public int compareTo(@NonNull PositionMetrics o) {
if(this.layerDepth < o.layerDepth) {
return -1;
} else if(this.layerDepth == o.layerDepth) {
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java
index a1a07c25..99227563 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java
@@ -23,7 +23,7 @@
import java.util.List;
/**
- * A stack of series to be rendered. The stack order is immutable but individual elements may be
+ * A stack of series to be rendered. The stack order is immutable but individual elements may be
* manipulated via the public methods of {@link RenderStack.StackElement}.
*/
public class RenderStack {
@@ -80,9 +80,6 @@ public RenderStack(Plot plot) {
* rendering series data to an XYGraphWidget.
*/
public void sync() {
- /**
- * TODO: rendering performance *might* be improved by reusing StackElement instances but I'm skeptical...
- */
getElements().clear();
List> pairList
= plot.getRegistry().getSeriesAndFormatterList();
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java b/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java
index f535ee32..bfedeeb8 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java
@@ -20,7 +20,6 @@
import android.graphics.RectF;
import android.graphics.Region;
import com.androidplot.Series;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.Plot;
import java.util.ArrayList;
@@ -52,10 +51,9 @@ public SeriesFormatterType getFormatter(SeriesType series) {
* @param canvas
* @param plotArea
* @param sfPair The series / formatter pair to be rendered
- * @throws PlotRenderException
*/
public void render(Canvas canvas, RectF plotArea, SeriesBundle sfPair, RenderStack stack) throws PlotRenderException {
+ SeriesFormatterType> sfPair, RenderStack stack) {
onRender(canvas, plotArea, sfPair.getSeries(), sfPair.getFormatter(), stack);
}
@@ -67,10 +65,9 @@ public void render(Canvas canvas, RectF plotArea, SeriesBundle> getSeriesAndFormatterList() {
List> results = new ArrayList<>();
List sfList = getPlot().getRegistry().getSeriesAndFormatterList();
- getPlot().getRegistry().getSeriesAndFormatterList();
for(SeriesBundle thisPair : sfList) {
if(thisPair.rendersWith(this)) {
results.add(thisPair);
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java b/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java
index e3819acd..d7f3e58b 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java
@@ -43,7 +43,6 @@ protected void validatePair(float value, SizeMode layoutType) {
@Override
public float getPixelValue(float size) {
- //switch(layoutType)
switch(getLayoutType()) {
case ABSOLUTE:
return getValue();
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/EmptyWidget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/EmptyWidget.java
deleted file mode 100644
index f6c30439..00000000
--- a/androidplot-core/src/main/java/com/androidplot/ui/widget/EmptyWidget.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * 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.androidplot.ui.widget;
-
-import android.graphics.Canvas;
-import android.graphics.RectF;
-import com.androidplot.exception.PlotRenderException;
-import com.androidplot.ui.LayoutManager;
-import com.androidplot.ui.Size;
-
-public class EmptyWidget extends Widget {
-
- public EmptyWidget(LayoutManager layoutManager, Size size) {
- super(layoutManager, size);
- }
- @Override
- protected void doOnDraw(Canvas canvas, RectF widgetRect) throws PlotRenderException {
- // nothing to do
- }
-}
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java
new file mode 100644
index 00000000..5bb428ac
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java
@@ -0,0 +1,13 @@
+package com.androidplot.ui.widget;
+
+/**
+ * An item to be displayed by {@link LegendWidget}.
+ */
+public interface LegendItem {
+
+ /**
+ *
+ * @return The user facing label for this item.
+ */
+ String getTitle();
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java
new file mode 100644
index 00000000..0da56a3a
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java
@@ -0,0 +1,194 @@
+package com.androidplot.ui.widget;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import androidx.annotation.NonNull;
+import com.androidplot.ui.LayoutManager;
+import com.androidplot.ui.Size;
+import com.androidplot.ui.TableModel;
+import com.androidplot.util.FontUtils;
+import com.androidplot.util.PixelUtils;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Provides core functionality for displaying a legend widget within a {@link com.androidplot.Plot}.
+ * @param
+ */
+public abstract class LegendWidget extends Widget {
+
+ private static final float DEFAULT_TEXT_SIZE_DP = 20;
+
+ private TableModel tableModel;
+ private Size iconSize;
+
+ private Paint textPaint;
+ private Paint iconBackgroundPaint;
+ private Paint iconBorderPaint;
+
+ private boolean drawIconBackgroundEnabled = true;
+ private boolean drawIconBorderEnabled = true;
+
+ private Comparator legendItemComparator;
+
+ {
+ textPaint = new Paint();
+ textPaint.setColor(Color.LTGRAY);
+ textPaint.setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_DP));
+ textPaint.setAntiAlias(true);
+
+ iconBackgroundPaint = new Paint();
+ iconBackgroundPaint.setColor(Color.BLACK);
+
+ iconBorderPaint = new Paint();
+ iconBorderPaint.setColor(Color.TRANSPARENT);
+ iconBorderPaint.setStyle(Paint.Style.STROKE);
+ }
+
+
+ public LegendWidget(@NonNull TableModel tableModel, @NonNull LayoutManager layoutManager,
+ @NonNull Size size, @NonNull Size iconSize) {
+ super(layoutManager, size);
+ setTableModel(tableModel);
+ this.iconSize = iconSize;
+ }
+
+ @Override
+ protected void doOnDraw(Canvas canvas, RectF widgetRect) {
+ final List items = getLegendItems();
+ if(legendItemComparator != null) {
+ Collections.sort(items, legendItemComparator);
+ }
+ final Iterator cellRectIterator = tableModel.getIterator(widgetRect, items.size());
+ for(ItemT item : items) {
+ final RectF cellRect = cellRectIterator.next();
+ final RectF iconRect = getIconRect(cellRect);
+ beginDrawingCell(canvas, iconRect);
+ drawItem(canvas, iconRect, item);
+ finishDrawingCell(canvas, cellRect, iconRect, item);
+ }
+ }
+
+ protected void drawItem(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item) {
+ drawIcon(canvas, iconRect, item);
+ }
+
+ /**
+ * Draw the icon representing the legend item
+ * @param canvas
+ * @param iconRect The space to be occupied by the icon.
+ * @param item
+ */
+ protected abstract void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item);
+
+ /**
+ *
+ * @return The list of legend items to be drawn. This is used to calculate table dimensions etc.
+ */
+ protected abstract List getLegendItems();
+
+ private RectF getIconRect(RectF cellRect) {
+ float cellRectCenterY = cellRect.top + (cellRect.height()/2);
+ RectF iconRect = iconSize.getRectF(cellRect);
+
+ // center the icon rect vertically
+ float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2);
+ iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY);
+ return iconRect;
+ }
+
+ /**
+ * Done at the start of rendering a new cell. Whatever is drawn here will be beneath the rest
+ * of the cell content; typically used to draw backgrounds.
+ * @param canvas
+ * @param iconRect
+ */
+ protected void beginDrawingCell(Canvas canvas, RectF iconRect) {
+
+ if(drawIconBackgroundEnabled && iconBackgroundPaint != null) {
+ canvas.drawRect(iconRect, iconBackgroundPaint);
+ }
+ }
+
+ /**
+ * Done at the end of rendering a new cell. Whatever is drawn here will be on top of
+ * the rest of the cell content; typically used to draw borders and text.
+ * @param canvas
+ * @param cellRect
+ * @param iconRect
+ * @param legendItem
+ */
+ protected void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, LegendItem legendItem) {
+
+ if(drawIconBorderEnabled && iconBorderPaint != null) {
+ canvas.drawRect(iconRect, iconBorderPaint);
+ }
+
+ float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2);
+
+ if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) {
+ canvas.drawText(legendItem.getTitle(), iconRect.left - 2, centeredTextOriginY, textPaint);
+ } else {
+ canvas.drawText(legendItem.getTitle(), iconRect.right + 2, centeredTextOriginY, textPaint);
+ }
+ }
+
+ protected static float getRectCenterY(RectF cellRect) {
+ return cellRect.top + (cellRect.height()/2);
+ }
+
+ public synchronized void setTableModel(TableModel tableModel) {
+ this.tableModel = tableModel;
+ }
+
+ public Paint getTextPaint() {
+ return textPaint;
+ }
+
+ public void setTextPaint(Paint textPaint) {
+ this.textPaint = textPaint;
+ }
+
+ public boolean isDrawIconBackgroundEnabled() {
+ return drawIconBackgroundEnabled;
+ }
+
+ public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) {
+ this.drawIconBackgroundEnabled = drawIconBackgroundEnabled;
+ }
+
+ public boolean isDrawIconBorderEnabled() {
+ return drawIconBorderEnabled;
+ }
+
+ public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) {
+ this.drawIconBorderEnabled = drawIconBorderEnabled;
+ }
+
+ public Size getIconSize() {
+ return iconSize;
+ }
+
+ public void setIconSize(Size iconSize) {
+ this.iconSize = iconSize;
+ }
+
+ public Comparator getLegendItemComparator() {
+ return legendItemComparator;
+ }
+
+ /**
+ * Set a scheme for sorting the display order or legend items. By default no sorting is applied
+ * and {@link com.androidplot.Series} items typically appear in the order which the series was
+ * added to the {@link com.androidplot.Plot}.
+ * @param legendItemComparator
+ */
+ public void setLegendItemComparator(Comparator legendItemComparator) {
+ this.legendItemComparator = legendItemComparator;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java
index 9f47aea9..06bf54ff 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java
@@ -21,13 +21,9 @@
import com.androidplot.util.FontUtils;
public class TextLabelWidget extends Widget {
- private static final String TAG = TextLabelWidget.class.getName();
-
private String text;
private Paint labelPaint;
-
private TextOrientation orientation;
-
private boolean autoPackEnabled = true;
{
@@ -67,16 +63,12 @@ public void onPostInit() {
}
}
- //protected abstract String getText();
-
/**
* Sets the dimensions of the widget to exactly contain the text contents
*/
public void pack() {
- //Log.d(TAG, "Packing...");
Rect size = FontUtils.getStringDimensions(text, getLabelPaint());
if(size == null) {
- //Log.w(TAG, "Attempt to pack empty text.");
return;
}
switch(orientation) {
@@ -103,16 +95,13 @@ public void doOnDraw(Canvas canvas, RectF widgetRect) {
if(text == null || text.length() == 0) {
return;
}
- //FontUtils.getStringDimensions(text, labelPaint);
+
float vOffset = labelPaint.getFontMetrics().descent;
PointF start = getAnchorCoordinates(widgetRect,
Anchor.CENTER);
- // BEGIN ROTATION CALCULATION
- //int canvasState = canvas.save(Canvas.ALL_SAVE_FLAG);
-
try {
- canvas.save(Canvas.ALL_SAVE_FLAG);
+ canvas.save();
canvas.translate(start.x, start.y);
switch (orientation) {
case HORIZONTAL:
@@ -129,11 +118,8 @@ public void doOnDraw(Canvas canvas, RectF widgetRect) {
}
canvas.drawText(text, 0, vOffset, labelPaint);
} finally {
- //canvas.restoreToCount(canvasState);
canvas.restore();
}
-
- // END ROTATION CALCULATION
}
public Paint getLabelPaint() {
@@ -173,7 +159,6 @@ public void setAutoPackEnabled(boolean autoPackEnabled) {
}
public void setText(String text) {
- //Log.d(TAG, "Setting textLabel to: " + text);
this.text = text;
if(autoPackEnabled) {
pack();
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java
index e12934f1..6d21767b 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java
@@ -17,7 +17,9 @@
package com.androidplot.ui.widget;
import android.graphics.*;
-import com.androidplot.exception.PlotRenderException;
+import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+
import com.androidplot.ui.*;
import com.androidplot.util.DisplayDimensions;
import com.androidplot.ui.HorizontalPositioning;
@@ -41,6 +43,16 @@ public abstract class Widget implements BoxModelable, Resizable {
private PositionMetrics positionMetrics;
private LayoutManager layoutManager;
+ private Rotation rotation = Rotation.NONE;
+ private RectF lastWidgetRect = null;
+
+ public enum Rotation {
+ NINETY_DEGREES,
+ NEGATIVE_NINETY_DEGREES,
+ ONE_HUNDRED_EIGHTY_DEGREES,
+ NONE,
+ }
+
public Widget(LayoutManager layoutManager, SizeMetric heightMetric, SizeMetric widthMetric) {
this(layoutManager, new Size(heightMetric, widthMetric));
}
@@ -68,6 +80,7 @@ public void setAnchor(Anchor anchor) {
/**
* Same as {@link #position(float, HorizontalPositioning, float, VerticalPositioning, Anchor)}
* but with the anchor parameter defaulted to the upper left corner.
+ *
* @param x
* @param horizontalPositioning
* @param y
@@ -78,11 +91,11 @@ public void position(float x, HorizontalPositioning horizontalPositioning, float
}
/**
- * @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1.
+ * @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1.
* @param horizontalPositioning LayoutType to use when orienting this element's X-Coordinate.
- * @param y Y_VALS_ONLY-Coordinate of the top-left corner of element. When using RELATIVE, must be a value between 0 and 1.
- * @param verticalPositioning LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate.
- * @param anchor The point of reference used by this positioning call.
+ * @param y Y_VALS_ONLY-Coordinate of the top-left corner of element. When using RELATIVE, must be a value between 0 and 1.
+ * @param verticalPositioning LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate.
+ * @param anchor The point of reference used by this positioning call.
*/
public void position(float x, HorizontalPositioning horizontalPositioning, float y,
VerticalPositioning verticalPositioning, Anchor anchor) {
@@ -113,7 +126,6 @@ public void onPostInit() {
* @return
*/
public boolean containsPoint(PointF point) {
- //return outlineRect != null && outlineRect.contains(point.x, point.y);
return widgetDimensions.canvasRect.contains(point.x, point.y);
}
@@ -263,7 +275,7 @@ public float getMarginRight() {
* into this Widget's size or position is altered.
*/
public synchronized void refreshLayout() {
- if(positionMetrics == null) {
+ if (positionMetrics == null) {
// make sure positionMetrics have been set. this method can be
// automatically called during xml configuration of certain params
// before the widget is fully configured.
@@ -271,7 +283,7 @@ public synchronized void refreshLayout() {
}
float elementWidth = getWidthPix(plotDimensions.paddedRect.width());
float elementHeight = getHeightPix(plotDimensions.paddedRect.height());
- PointF coords = getElementCoordinates(elementHeight,
+ PointF coords = calculateCoordinates(elementHeight,
elementWidth, plotDimensions.paddedRect, positionMetrics);
RectF widgetRect = new RectF(coords.x, coords.y,
@@ -288,70 +300,132 @@ public synchronized void layout(final DisplayDimensions plotDimensions) {
refreshLayout();
}
- public PointF getElementCoordinates(float height, float width, RectF viewRect, PositionMetrics metrics) {
- float x = metrics.getXPositionMetric().getPixelValue(viewRect.width()) + viewRect.left;
- float y = metrics.getYPositionMetric().getPixelValue(viewRect.height()) + viewRect.top;
- PointF point = new PointF(x, y);
- return PixelUtils.sub(point, getAnchorOffset(width, height, metrics.getAnchor()));
- }
+
+ public static PointF calculateCoordinates(float height, float width, RectF viewRect, PositionMetrics metrics) {
+ float x = metrics.getXPositionMetric().getPixelValue(viewRect.width()) + viewRect.left;
+ float y = metrics.getYPositionMetric().getPixelValue(viewRect.height()) + viewRect.top;
+ PointF point = new PointF(x, y);
+ return PixelUtils.sub(point, getAnchorOffset(width, height, metrics.getAnchor()));
+ }
public static PointF getAnchorOffset(float width, float height, Anchor anchor) {
- PointF point = new PointF();
- switch (anchor) {
- case LEFT_TOP:
- break;
- case LEFT_MIDDLE:
- point.set(0, height / 2);
- break;
- case LEFT_BOTTOM:
- point.set(0, height);
- break;
- case RIGHT_TOP:
- point.set(width, 0);
- break;
- case RIGHT_BOTTOM:
- point.set(width, height);
- break;
- case RIGHT_MIDDLE:
- point.set(width, height / 2);
- break;
- case TOP_MIDDLE:
- point.set(width / 2, 0);
- break;
- case BOTTOM_MIDDLE:
- point.set(width / 2, height);
- break;
- case CENTER:
- point.set(width / 2, height / 2);
- break;
- default:
- throw new IllegalArgumentException("Unsupported anchor location: " + anchor);
- }
- return point;
+ PointF point = new PointF();
+ switch (anchor) {
+ case LEFT_TOP:
+ break;
+ case LEFT_MIDDLE:
+ point.set(0, height / 2);
+ break;
+ case LEFT_BOTTOM:
+ point.set(0, height);
+ break;
+ case RIGHT_TOP:
+ point.set(width, 0);
+ break;
+ case RIGHT_BOTTOM:
+ point.set(width, height);
+ break;
+ case RIGHT_MIDDLE:
+ point.set(width, height / 2);
+ break;
+ case TOP_MIDDLE:
+ point.set(width / 2, 0);
+ break;
+ case BOTTOM_MIDDLE:
+ point.set(width / 2, height);
+ break;
+ case CENTER:
+ point.set(width / 2, height / 2);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported anchor location: " + anchor);
}
+ return point;
+ }
public static PointF getAnchorCoordinates(RectF widgetRect, Anchor anchor) {
- return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top),
- getAnchorOffset(widgetRect.width(), widgetRect.height(), anchor));
- }
+ return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top),
+ getAnchorOffset(widgetRect.width(), widgetRect.height(), anchor));
+ }
- public static PointF getAnchorCoordinates(float x, float y, float width, float height, Anchor anchor) {
- return getAnchorCoordinates(new RectF(x, y, x+width, y+height), anchor);
+ public static PointF getAnchorCoordinates(float x, float y, float width, float height, Anchor anchor) {
+ return getAnchorCoordinates(new RectF(x, y, x + width, y + height), anchor);
+ }
+
+ private void checkSize(@NonNull RectF widgetRect) {
+ if (lastWidgetRect == null || !lastWidgetRect.equals(widgetRect)) {
+ onResize(lastWidgetRect, widgetRect);
}
+ lastWidgetRect = widgetRect;
+ }
- public void draw(Canvas canvas) throws PlotRenderException {
+ /**
+ * Called whenever the height or width of the Widget's reserved space has changed,
+ * immediately before {@link #doOnDraw(Canvas, RectF)}.
+ * May be used to efficiently carry out expensive operations only when necessary.
+ *
+ * @param oldRect
+ * @param newRect
+ */
+ protected void onResize(@Nullable RectF oldRect, @NonNull RectF newRect) {
+ // do nothing by default
+ }
+
+ public void draw(Canvas canvas) {
if (isVisible()) {
if (backgroundPaint != null) {
drawBackground(canvas, widgetDimensions.canvasRect);
}
- doOnDraw(canvas, widgetDimensions.paddedRect);
+ canvas.save();
+ final RectF widgetRect = applyRotation(canvas, widgetDimensions.paddedRect);
+ checkSize(widgetRect);
+ doOnDraw(canvas, widgetRect);
+ canvas.restore();
if (borderPaint != null) {
- drawBorder(canvas, widgetDimensions.paddedRect);
+ drawBorder(canvas, widgetRect);
}
}
}
+ protected RectF applyRotation(Canvas canvas, RectF rect) {
+ float rotationDegs = 0;
+ final float cx = widgetDimensions.paddedRect.centerX();
+ final float cy = widgetDimensions.paddedRect.centerY();
+ final float halfHeight = widgetDimensions.paddedRect.height() / 2;
+ final float halfWidth = widgetDimensions.paddedRect.width() / 2;
+ switch (rotation) {
+ case NINETY_DEGREES:
+ rotationDegs = 90;
+ rect = new RectF(
+ cx - halfHeight,
+ cy - halfWidth,
+ cx + halfHeight,
+ cy + halfWidth);
+ break;
+ case NEGATIVE_NINETY_DEGREES:
+ rotationDegs = -90;
+ rect = new RectF(
+ cx - halfHeight,
+ cy - halfWidth,
+ cx + halfHeight,
+ cy + halfWidth);
+ break;
+ case ONE_HUNDRED_EIGHTY_DEGREES:
+ rotationDegs = 180;
+ // fall through
+ case NONE:
+ break;
+ default:
+ throw new UnsupportedOperationException("Not yet implemented.");
+
+ }
+ if (rotation != Rotation.NONE) {
+ canvas.rotate(rotationDegs, cx, cy);
+ }
+ return rect;
+ }
+
protected void drawBorder(Canvas canvas, RectF paddedRect) {
canvas.drawRect(paddedRect, borderPaint);
}
@@ -364,7 +438,7 @@ protected void drawBackground(Canvas canvas, RectF widgetRect) {
* @param canvas The Canvas to draw onto
* @param widgetRect the size and coordinates of this widget
*/
- protected abstract void doOnDraw(Canvas canvas, RectF widgetRect) throws PlotRenderException;
+ protected abstract void doOnDraw(Canvas canvas, RectF widgetRect);
public Paint getBorderPaint() {
return borderPaint;
@@ -405,4 +479,12 @@ public PositionMetrics getPositionMetrics() {
public void setPositionMetrics(PositionMetrics positionMetrics) {
this.positionMetrics = positionMetrics;
}
+
+ public Rotation getRotation() {
+ return rotation;
+ }
+
+ public void setRotation(Rotation rotation) {
+ this.rotation = rotation;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java
index b8b6c7f7..853ae25a 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java
@@ -188,6 +188,12 @@ public static void configureWidget(TypedArray attrs, Widget widget, int heightSi
}
}
+ public static void configureWidgetRotation(TypedArray attrs, Widget widget, int rotationAttr) {
+ if(attrs != null) {
+ widget.setRotation(getWidgetRotation(attrs, rotationAttr, Widget.Rotation.NONE));
+ }
+ }
+
/**
* Configure a {@link Widget} from xml attrs.
* @param attrs
@@ -231,7 +237,7 @@ private static Number getIntFloatDimenValue(TypedArray attrs, int valueAttr, Num
} else if (valueType == TypedValue.TYPE_FLOAT) {
result = attrs.getFloat(valueAttr, defaultValue.floatValue());
} else {
- throw new IllegalArgumentException("Invalid value type - must be float or dimension.");
+ throw new IllegalArgumentException("Invalid value type - must be int, float or dimension.");
}
}
return result;
@@ -245,6 +251,10 @@ private static VerticalPositioning getYLayoutStyle(TypedArray attrs, int attr, V
return VerticalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
+ private static Widget.Rotation getWidgetRotation(TypedArray attrs, int attr, Widget.Rotation defaultValue) {
+ return Widget.Rotation.values()[attrs.getInt(attr, defaultValue.ordinal())];
+ }
+
private static Anchor getAnchorPosition(TypedArray attrs, int attr, Anchor defaultValue) {
return Anchor.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java
index 049431b3..4fa11d8a 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java
@@ -1,11 +1,14 @@
package com.androidplot.util;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
/**
* An extension of {@link Number} optimized for speed at the cost of memory.
*/
public class FastNumber extends Number {
- private Number number;
+ @NonNull private final Number number;
private boolean hasDoublePrimitive;
private boolean hasFloatPrimitive;
private boolean hasIntPrimitive;
@@ -14,7 +17,25 @@ public class FastNumber extends Number {
private float floatPrimitive;
private int intPrimitive;
- public FastNumber(Number number) {
+ /**
+ * Safe-instantiator of FastNumber; returns a null result if the input Number is also null.
+ * @param number
+ * @return
+ */
+ public static FastNumber orNull(@NonNull Number number) {
+ if(number == null) {
+ return null;
+ } else {
+ return new FastNumber(number);
+ }
+ }
+
+ private FastNumber(@NonNull Number number) {
+
+ //noinspection ConstantConditions //in case someone ignores the @NonNull annotation
+ if (number == null) {
+ throw new IllegalArgumentException("number parameter cannot be null");
+ }
// avoid nested instances of FastNumber :
if(number instanceof FastNumber) {
@@ -63,4 +84,36 @@ public double doubleValue() {
}
return doublePrimitive;
}
+
+ /**
+ * To be equal, two instances must both be instances of {@link FastNumber}. The inner {@link
+ * #number} field must also be a common type. Numbers which are mathematically equal are not
+ * necessarily equal. This keeps with the java implementation of common Number classes where for
+ * instance {@code new Integer(0).equals(new Double(0))} returns {@code false}
+ */
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ FastNumber that = (FastNumber) o;
+
+ return number.equals(that.number);
+
+ }
+
+ @Override
+ public int hashCode() {
+ return number.hashCode();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.valueOf(doubleValue());
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java b/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java
index 61e25c97..f11737a6 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java
@@ -153,4 +153,28 @@ public synchronized boolean remove(KeyType key) {
return false;
}
}
+
+ public ValueType getTop() {
+ return hash.get(zlist.getLast());
+ }
+
+ public ValueType getBottom() {
+ return hash.get(zlist.getFirst());
+ }
+
+ public ValueType getAbove(KeyType key) {
+ final int index = zlist.indexOf(key);
+ if(index >= 0 && index < size() - 1) {
+ return hash.get(zlist.get(index + 1));
+ }
+ return null;
+ }
+
+ public ValueType getBeneath(KeyType key) {
+ final int index = zlist.indexOf(key);
+ if(index > 0) {
+ return hash.get(zlist.get(index - 1));
+ }
+ return null;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java b/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java
index 1d597b9e..b56337ed 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java
@@ -39,6 +39,7 @@ public class PlotStatistics implements PlotListener {
long latencySamples = 0;
long latencySum = 0;
String annotationString = "";
+ private boolean annotatePlotEnabled;
private Paint paint;
{
@@ -49,11 +50,6 @@ public class PlotStatistics implements PlotListener {
resetCounters();
}
-
- private boolean annotatePlotEnabled;
-
-
-
public PlotStatistics(long updateDelayMs, boolean annotatePlotEnabled) {
this.updateDelayMs = updateDelayMs;
this.annotatePlotEnabled = annotatePlotEnabled;
@@ -109,4 +105,8 @@ public void onAfterDraw(Plot source, Canvas canvas) {
latencySamples++;
annotatePlot(source, canvas);
}
+
+ public void setEnabled(boolean isEnabled) {
+ this.annotatePlotEnabled = isEnabled;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java b/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java
index 0b5db607..3915e2ad 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java
@@ -59,4 +59,24 @@ public static RectF applyInsets(RectF rect, Insets insets) {
return rect;
}
}
+
+ /**
+ * Generates a RectF from two height and two width values; the h and w values will
+ * be passed into the RectF constructor such that RectF.left <= RectF.right and
+ * RectF.top <= RectF.bottom.
+ * @param w1 width1
+ * @param h1 height1
+ * @param w2 width2
+ * @param h2 height2
+ * @return
+ */
+ public static RectF createFromEdges(float w1, float h1, float w2, float h2) {
+ final boolean w1IsLeft = w1 <= w2;
+ final boolean h1IsTop = h1 <= h2;
+ return new RectF(
+ w1IsLeft ? w1 : w2,
+ h1IsTop ? h1 : h2,
+ w1IsLeft ? w2 : w1,
+ h1IsTop ? h2 : h1);
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java
index 266541d2..4bc118e0 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java
@@ -17,16 +17,17 @@
package com.androidplot.util;
import android.util.Log;
+
import com.androidplot.Plot;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
/**
* Utility class for invoking Plot.redraw() on a background thread
- * at a set frequency. Callers should be sure to create a separate thread from which to use this class.
+ * at a set frequency.
*/
public class Redrawer implements Runnable {
@@ -52,12 +53,12 @@ public class Redrawer implements Runnable {
* @param startImmediately If true, invokes run() immediately after construction.
*/
public Redrawer(List plots, float maxRefreshRate, boolean startImmediately) {
- this.plots = new ArrayList<>();
+ this.plots = new ArrayList<>(plots.size());
for(Plot plot : plots) {
this.plots.add(new WeakReference<>(plot));
}
setMaxRefreshRate(maxRefreshRate);
- thread = new Thread(this);
+ thread = new Thread(this, "Androidplot Redrawer");
thread.start();
if(startImmediately) {
start();
@@ -65,7 +66,7 @@ public Redrawer(List plots, float maxRefreshRate, boolean startImmediately
}
public Redrawer(Plot plot, float maxRefreshRate, boolean startImmediately) {
- this(Arrays.asList(new Plot[]{plot}), maxRefreshRate, startImmediately);
+ this(Collections.singletonList(plot), maxRefreshRate, startImmediately);
}
/**
@@ -121,7 +122,7 @@ public void run() {
}
}
}
- } catch(InterruptedException e) {
+ } catch (InterruptedException ignored) {
} finally {
Log.d(TAG, "Redrawer thread exited.");
diff --git a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java
index 922e51b9..b9526a24 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java
@@ -16,10 +16,14 @@
package com.androidplot.util;
-import com.androidplot.*;
-import com.androidplot.xy.*;
+import com.androidplot.Region;
+import com.androidplot.xy.FastXYSeries;
+import com.androidplot.xy.OrderedXYSeries;
+import com.androidplot.xy.RectRegion;
+import com.androidplot.xy.XYConstraints;
+import com.androidplot.xy.XYSeries;
-import java.util.*;
+import java.util.List;
/**
* Utilities for dealing with Series data.
@@ -34,6 +38,26 @@ public static RectRegion minMax(XYSeries... seriesList) {
return minMax(null, seriesList);
}
+ public static Region minMaxX(XYSeries... seriesList) {
+ final Region bounds = new Region();
+ for (XYSeries series : seriesList) {
+ for (int i = 0; i < series.size(); i++) {
+ bounds.union(series.getX(i));
+ }
+ }
+ return bounds;
+ }
+
+ public static Region minMaxY(XYSeries... seriesList) {
+ final Region bounds = new Region();
+ for (XYSeries series : seriesList) {
+ for (int i = 0; i < series.size(); i++) {
+ bounds.union(series.getY(i));
+ }
+ }
+ return bounds;
+ }
+
/**
* @param constraints may be null.
* @param seriesList
@@ -41,7 +65,6 @@ public static RectRegion minMax(XYSeries... seriesList) {
* @since 0.9.7
*/
public static RectRegion minMax(XYConstraints constraints, List seriesList) {
- // TODO: this is inefficient...clean it up!
return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()]));
}
@@ -62,28 +85,24 @@ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArr
for (XYSeries series : seriesArray) {
// if this is an advanced xy series then minMax have already been calculated for us:
- if(series instanceof FastXYSeries) {
+ if (series instanceof FastXYSeries) {
final RectRegion b = ((FastXYSeries) series).minMax();
- if(constraints == null) {
+ if(b == null) {
+ //this series doesn't currently have min or max region (might be empty)
+ continue;
+ }
+ if(constraints == null || constraints.contains(b)) {
bounds.union(b);
- } else {
- if(constraints.contains(b.getMinX(), b.getMinY())) {
- bounds.union(b.getMinX(), b.getMinY());
- }
- if(constraints.contains(b.getMaxX(), b.getMaxY())) {
- bounds.union(b.getMaxX(), b.getMaxY());
- }
+ continue;
}
+ }
+ for (int i = 0; i < series.size(); i++) {
+ final Number xi = series.getX(i);
+ final Number yi = series.getY(i);
- } else if (series.size() > 0) {
- for (int i = 0; i < series.size(); i++) {
- final Number xi = series.getX(i);
- final Number yi = series.getY(i);
-
- // if constraints have been set, make sure this xy coordinate exists within them:
- if (constraints == null || constraints.contains(xi, yi)) {
- bounds.union(xi, yi);
- }
+ // if constraints have been set, make sure this xy coordinate exists within them:
+ if (constraints == null || constraints.contains(xi, yi)) {
+ bounds.union(xi, yi);
}
}
}
@@ -101,7 +120,6 @@ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArr
public static Region minMax(Region bounds, List... lists) {
for (final List list : lists) {
for (final Number i : list) {
- //minMax(bounds, i);
bounds.union(i);
}
}
@@ -240,26 +258,6 @@ public static Region minMax(List... lists) {
return minMax(new Region(), lists);
}
- public static void main(String[] args) {
-
- // seed the list:
- ArrayList values = new ArrayList<>();
- for (int i = 0; i < 1000000; i++) {
- values.add(Math.random() * 100);
- }
- final int numIterations = 20;
- long sumTime = 0;
- for(int j = 0; j < numIterations; j++) {
- final long startTime = System.currentTimeMillis();
- Region bounds = minMax(values);
- final long thisIteration = System.currentTimeMillis() - startTime;
- System.out.println("thisIteration took: " + thisIteration + "ms");
- sumTime += thisIteration;
- }
-
- System.out.println("Benchmark avg:" + (sumTime / numIterations) + "ms.");
- }
-
/**
* Determine the XVal order of an XYSeries. If series does not implement {@link OrderedXYSeries}
* then {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE} is assumed.
@@ -268,6 +266,6 @@ public static void main(String[] args) {
*/
public static OrderedXYSeries.XOrder getXYOrder(XYSeries series) {
return series instanceof OrderedXYSeries ?
- ((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE;
+ ((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE;
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java
index ee3e7e24..11b9e8e8 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java
@@ -18,7 +18,6 @@
import android.content.*;
import android.graphics.*;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesRenderer;
@@ -41,7 +40,7 @@ public AdvancedLineAndPointRenderer(XYPlot plot) {
}
@Override
- protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) throws PlotRenderException {
+ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) {
PointF thisPoint;
PointF lastPoint = null;
for (int i = 0; i < series.size(); i++) {
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java
index ca7bcbea..3272748d 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java
@@ -40,6 +40,11 @@ public void setBorderPaint(Paint borderPaint) {
private Paint fillPaint;
private Paint borderPaint;
+ private float marginTop;
+ private float marginBottom;
+ private float marginLeft;
+ private float marginRight;
+
/**
* Should only be used in conjunction with calls to configure()...
*/
@@ -72,4 +77,36 @@ public Class extends SeriesRenderer> getRendererClass() {
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
return new BarRenderer(plot);
}
+
+ public float getMarginTop() {
+ return marginTop;
+ }
+
+ public void setMarginTop(float marginTop) {
+ this.marginTop = marginTop;
+ }
+
+ public float getMarginBottom() {
+ return marginBottom;
+ }
+
+ public void setMarginBottom(float marginBottom) {
+ this.marginBottom = marginBottom;
+ }
+
+ public float getMarginLeft() {
+ return marginLeft;
+ }
+
+ public void setMarginLeft(float marginLeft) {
+ this.marginLeft = marginLeft;
+ }
+
+ public float getMarginRight() {
+ return marginRight;
+ }
+
+ public void setMarginRight(float marginRight) {
+ this.marginRight = marginRight;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java
index 108d0c6c..ced83885 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java
@@ -20,105 +20,102 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-import java.util.Map.Entry;
-import java.util.TreeMap;
import android.graphics.Canvas;
import android.graphics.RectF;
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesBundle;
+import com.androidplot.util.PixelUtils;
+import com.androidplot.util.RectFUtils;
/**
- * Renders the points in an XYSeries as bars.
+ * Renders the points in an XYSeries as bars. See {@link BarOrientation} javadoc for details on supported
+ * presentation styles.
+ *
*/
public class BarRenderer extends GroupRenderer {
- private Style style = Style.OVERLAID; // default Render Style
- private BarWidthMode barWidthMode = BarWidthMode.FIXED_WIDTH; // default Width Style
- private float barWidth = 5;
- private float barGap = 1;
- private Comparator barComparator = new BarComparator();
+ private BarOrientation barOrientation = BarOrientation.OVERLAID; // default Render Style
+ private BarGroupWidthMode barGroupWidthMode = BarGroupWidthMode.FIXED_WIDTH; // default Width Style
+
+ /**
+ * Represents the size in pixels of either bar width or bar gap width, depending on the current
+ * value of barWidthMode.
+ */
+ private float width = PixelUtils.dpToPix(3);
+
+ /**
+ * How bars should be laid out when in a group of 2 or more series.
+ */
+ public enum BarOrientation {
+
+ /**
+ * Bars are drawn overlapping one another, in the order their respective series
+ * was added to the plot.
+ */
+ IN_ORDER,
- /**
- * How bars should be laid out when in a group of 2 or more series.
- */
- public enum Style {
+ /**
+ * Bars are drawn overlapping one another, with taller bars being drawn behind
+ * the shorter ones.
+ */
OVERLAID, // bars are overlaid in descending y-val order (largest val in back)
+
+ /**
+ * Bars are stacked on top of one another so that the sum of their yVals produces the final
+ * height of that bar.
+ */
STACKED, // bars are drawn stacked vertically on top of each other
+
+ /**
+ * Bars are drawn next to one another, grouped by iVal
+ */
SIDE_BY_SIDE // bars are drawn horizontally next to each-other
}
- /**
- * Mode with which to calculate the width of each bar.
- */
- public enum BarWidthMode {
- FIXED_WIDTH, // bar width is always barWidth
- VARIABLE_WIDTH // bar width is calculated so that there is only barGap between each bar
+ /**
+ * Mode with which to calculate the width of each bar.
+ */
+ public enum BarGroupWidthMode {
+ FIXED_WIDTH, // bar width is always barWidth
+ FIXED_GAP // bar width is calculated relative to a fixed gap width between each bar
}
public BarRenderer(XYPlot plot) {
super(plot);
}
- /**
- * Sets the width of the bars when using the FIXED_WIDTH render style
- * @param barWidth
- */
- public void setBarWidth(float barWidth) {
- this.barWidth = barWidth;
+ public void setBarOrientation(BarOrientation renderBarOrientation) {
+ this.barOrientation = renderBarOrientation;
}
- /**
- * Sets the size of the gap between the bar (or bar groups) when using the VARIABLE_WIDTH render style
- * @param barGap
- */
- public void setBarGap(float barGap) {
- this.barGap = barGap;
+ public BarOrientation getBarOrientation() {
+ return this.barOrientation;
}
- public void setStyle(Style renderStyle) {
- this.style = renderStyle;
+ public BarGroupWidthMode getBarGroupWidthMode() {
+ return this.barGroupWidthMode;
}
- public Style getStyle() {
- return this.style;
- }
-
- public void setBarWidthMode(BarWidthMode widthStyle) {
- this.barWidthMode = widthStyle;
+ public float getBarGroupWidth() {
+ return this.width;
}
- public BarWidthMode getBarWidthMode() {
- return this.barWidthMode;
- }
-
- public void setBarWidth(BarWidthMode mode, float value) {
- setBarWidthMode(mode);
- switch (mode) {
- case FIXED_WIDTH:
- setBarWidth(value);
- break;
- case VARIABLE_WIDTH:
- setBarGap(value);
- break;
- default:
- break;
- }
+ public void setBarGroupWidth(BarGroupWidthMode mode, float width) {
+ this.barGroupWidthMode = mode;
+ this.width = width;
}
-
- /**
- * Sets a {@link Comparator} used for sorting bars.
- */
- public void setBarComparator(Comparator barComparator) {
- this.barComparator = barComparator;
+
+ protected BarComparator getBarComparator(float rangeOriginPx) {
+ return new BarComparator(getBarOrientation(), rangeOriginPx);
}
@Override
public void doDrawLegendIcon(Canvas canvas, RectF rect, BarFormatter formatter) {
- if(formatter.hasFillPaint()) {
- canvas.drawRect(rect, formatter.getFillPaint());
- }
+ if (formatter.hasFillPaint()) {
+ canvas.drawRect(rect, formatter.getFillPaint());
+ }
canvas.drawRect(rect, formatter.getBorderPaint());
}
@@ -134,274 +131,255 @@ public FormatterType getFormatter(int index, XYSeries series) {
return null;
}
+ @Override
+ public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) {
- @Override
- public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) {
-
- TreeMap axisMap = new TreeMap();
+ List barGroups = new ArrayList<>();
/*
* Build the axisMap (yVal,BarGroup)... a TreeMap of BarGroups
* BarGroups represent a point on the X axis where a single or group of bars need to be drawn.
*/
- for(SeriesBundle thisPair : sfList) {
- BarGroup barGroup;
-
- // For each value in the series
- for(int i = 0; i < seriesSize; i++) {
-
- if (thisPair.getSeries().getX(i) != null) {
-
- // get a new bar object
- Bar bar = new Bar(thisPair.getSeries(), thisPair.getFormatter(),i,plotArea);
-
- // Find or create the barGroup
- if (axisMap.containsKey(bar.intX)) {
- barGroup = axisMap.get(bar.intX);
- } else {
- barGroup = new BarGroup(bar.intX,plotArea);
- axisMap.put(bar.intX, barGroup);
- }
- barGroup.addBar(bar);
- }
+ for(int i = 0; i < seriesSize; i++) {
+ final BarGroup group = new BarGroup(i, 0, plotArea);
+ int seriesOrder = 0;
+ for(SeriesBundle bundle : sfList) {
+ // TODO: is this null check really necessary?
+ if(bundle.getSeries().getX(i) != null) {
+ Bar bar = new Bar(getPlot(), bundle.getSeries(),
+ bundle.getFormatter(), seriesOrder, i, plotArea);
+ group.addBar(bar);
+ group.centerPix = bar.xPix;
+ }
+ seriesOrder++;
}
+ barGroups.add(group);
}
- // Loop through the axisMap linking up prev pointers
- BarGroup prev, current;
- prev = null;
- for(Entry mapEntry : axisMap.entrySet()) {
- current = mapEntry.getValue();
- current.prev = prev;
- prev = current;
- }
-
- // The default gap between each bar section
- int gap = (int) barGap;
-
- // Determine roughly how wide (rough_width) this bar should be. This is then used as a default width
- // when there are gaps in the data or for the first/last bars.
- float f_rough_width = ((plotArea.width() - ((axisMap.size() - 1) * gap)) / (axisMap.size() - 1));
- int rough_width = (int) f_rough_width;
- if (rough_width < 0) rough_width = 0;
- if (gap > rough_width) {
- gap = rough_width / 2;
- }
-
/*
- * Calculate the dimensions of each barGroup and then draw each bar within it according to
+ * Calculate the dimensions of each barGroup and then draw each bar within it according to
* the Render Style and Width Style.
*/
- for(Number key : axisMap.keySet()) {
-
- BarGroup barGroup = axisMap.get(key);
-
- // Determine the exact left and right X for the Bar Group
- switch (barWidthMode) {
- case FIXED_WIDTH:
- // use intX and go halfwidth either side.
- barGroup.leftX = barGroup.intX - (int) (barWidth / 2);
- barGroup.width = (int) barWidth;
- barGroup.rightX = barGroup.leftX + barGroup.width;
- break;
- case VARIABLE_WIDTH:
- if (barGroup.prev != null) {
- if (barGroup.intX - barGroup.prev.intX - gap - 1 > (int)(rough_width * 1.5)) {
- // use intX and go halfwidth either side.
- barGroup.leftX = barGroup.intX - (rough_width / 2);
- barGroup.width = rough_width;
- barGroup.rightX = barGroup.leftX + barGroup.width;
- } else {
- // base left off prev right to get the gap correct.
- barGroup.leftX = barGroup.prev.rightX + gap + 1;
- if (barGroup.leftX > barGroup.intX) barGroup.leftX = barGroup.intX;
- // base right off intX + halfwidth.
- barGroup.rightX = barGroup.intX + (rough_width / 2);
- // calculate the width
- barGroup.width = barGroup.rightX - barGroup.leftX;
- }
- } else {
- // use intX and go halfwidth either side.
- barGroup.leftX = barGroup.intX - (rough_width / 2);
- barGroup.width = rough_width;
- barGroup.rightX = barGroup.leftX + barGroup.width;
- }
- break;
- default:
- break;
- }
+ final int groupCount = barGroups.size();
+ for(BarGroup barGroup : barGroups) {
+
+ // Determine the exact left and right X for the Bar Group
+ switch (barGroupWidthMode) {
+ case FIXED_WIDTH:
+ barGroup.leftPix = barGroup.centerPix - (width / 2);
+ barGroup.rightPix = barGroup.leftPix + width;
+ break;
+ case FIXED_GAP:
+ float barWidth = plotArea.width();
+ if(groupCount > 1) {
+ barWidth = (barGroups.get(1).centerPix - barGroups.get(0).centerPix) - width;
+ }
+
+ final float halfWidth = barWidth / 2;
+ barGroup.leftPix = barGroup.centerPix - halfWidth;
+ barGroup.rightPix = barGroup.centerPix + halfWidth;
+ break;
+ default:
+ break;
+ }
/*
* Draw the bars within the barGroup area.
*/
double rangeOrigin = getPlot().getRangeOrigin().doubleValue();
- float basePositionY = (float) getPlot().getBounds().yRegion
- .transform(rangeOrigin, plotArea.top, plotArea.bottom, true);
-
- switch (style) {
- case OVERLAID:
- Collections.sort(barGroup.bars, barComparator);
- for (Bar bar : barGroup.bars) {
- BarFormatter formatter = bar.getFormatter();
- PointLabelFormatter plf = formatter.hasPointLabelFormatter()
- ? formatter.getPointLabelFormatter() : null;
- PointLabeler pointLabeler = null;
- if (formatter != null) {
- pointLabeler = formatter.getPointLabeler();
- }
-
- if (bar.yVal= 2) {
- canvas.drawRect(bar.barGroup.leftX, basePositionY, bar.barGroup.rightX, bar.intY, formatter.getFillPaint());
- }
- canvas.drawRect(bar.barGroup.leftX, basePositionY, bar.barGroup.rightX, bar.intY, formatter.getBorderPaint());
- } else { // rising bar
- if (formatter.hasFillPaint() && bar.barGroup.width >= 2) {
- canvas.drawRect(bar.barGroup.leftX, bar.intY, bar.barGroup.rightX, basePositionY, formatter.getFillPaint());
- }
- canvas.drawRect(bar.barGroup.leftX, bar.intY, bar.barGroup.rightX, basePositionY, formatter.getBorderPaint());
- }
- if(plf != null && plf.hasTextPaint() && pointLabeler != null) {
- canvas.drawText(pointLabeler.getLabel(bar.series, bar.seriesIndex), bar.intX + plf.hOffset, bar.intY + plf.vOffset, plf.getTextPaint());
- }
- }
- break;
- case SIDE_BY_SIDE:
- int width = barGroup.width / barGroup.bars.size();
- int leftX = barGroup.leftX;
- Collections.sort(barGroup.bars, barComparator);
- for (Bar bar : barGroup.bars) {
- BarFormatter formatter = bar.getFormatter();
- PointLabelFormatter plf = formatter.hasPointLabelFormatter()
- ? formatter.getPointLabelFormatter() : null;
- PointLabeler pointLabeler = null;
- if (formatter != null) {
- pointLabeler = formatter.getPointLabeler();
- }
-
- if (bar.yVal= 2) {
- canvas.drawRect(leftX, basePositionY, leftX + width, bar.intY, formatter.getFillPaint());
- }
- canvas.drawRect(leftX, basePositionY, leftX + width, bar.intY, formatter.getBorderPaint());
- } else { // rising bar
- if (formatter.hasFillPaint() && bar.barGroup.width >= 2) {
- canvas.drawRect(leftX, bar.intY, leftX + width, basePositionY, formatter.getFillPaint());
- }
- canvas.drawRect(leftX, bar.intY, leftX + width, basePositionY, formatter.getBorderPaint());
- }
- if(plf != null && plf.hasTextPaint() && pointLabeler != null) {
- canvas.drawText(pointLabeler.getLabel(bar.series, bar.seriesIndex), leftX + width/2 + plf.hOffset, bar.intY + plf.vOffset, plf.getTextPaint());
- }
- leftX = leftX + width;
- }
- break;
- case STACKED:
- int bottom = (int) barGroup.plotArea.bottom;
- Collections.sort(barGroup.bars, barComparator);
- for (Bar b : barGroup.bars) {
- BarFormatter formatter = b.getFormatter();
- PointLabelFormatter plf = formatter.hasPointLabelFormatter()
- ? formatter.getPointLabelFormatter() : null;
- PointLabeler pointLabeler = null;
- if (formatter != null) {
- pointLabeler = formatter.getPointLabeler();
- }
- int height = (int) b.barGroup.plotArea.bottom - b.intY;
- int top = bottom - height;
- if (formatter.hasFillPaint() && b.barGroup.width >= 2) {
- canvas.drawRect(b.barGroup.leftX, top, b.barGroup.rightX, bottom, formatter.getFillPaint());
- }
-
- canvas.drawRect(b.barGroup.leftX, top, b.barGroup.rightX, bottom, formatter.getBorderPaint());
- if(plf != null && plf.hasTextPaint() && pointLabeler != null) {
- //canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), b.intX + plf.hOffset, b.intY + plf.vOffset, plf.getTextPaint());
- // b.intY should be replaced by top as Text label should be drawn on top of each bar
- canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), b.intX + plf.hOffset, top + plf.vOffset, plf.getTextPaint());
- }
- bottom = top;
- }
- break;
- default:
- break;
- }
- }
+ float rangeOriginPx = (float) getPlot().getBounds().yRegion
+ .transform(rangeOrigin, plotArea.top, plotArea.bottom, true);
+
+ final BarComparator comparator = getBarComparator(rangeOriginPx);
+ switch (barOrientation) {
+ case IN_ORDER:
+ case OVERLAID:
+ Collections.sort(barGroup.bars, comparator);
+ for (Bar bar : barGroup.bars) {
+ drawBar(canvas, bar, createBarRect(
+ bar.barGroup.leftPix,
+ bar.yPix,
+ bar.barGroup.rightPix,
+ rangeOriginPx, bar.formatter));
+ }
+ break;
+ case SIDE_BY_SIDE:
+ final float width = barGroup.getWidth() / barGroup.bars.size();
+ float leftX = barGroup.leftPix;
+ Collections.sort(barGroup.bars, comparator);
+ for (Bar bar : barGroup.bars) {
+ drawBar(canvas, bar, createBarRect(
+ leftX, bar.yPix,
+ leftX + width, rangeOriginPx,
+ bar.formatter));
+ leftX = leftX + width;
+ }
+ break;
+ case STACKED:
+ float bottom = (int) barGroup.plotArea.bottom;
+ Collections.sort(barGroup.bars, comparator);
+ for (Bar bar : barGroup.bars) {
+ // TODO: handling sub range-origin values for the purpose of labeling
+ final float height = (int) bar.barGroup.plotArea.bottom - bar.yPix;
+ final float top = bottom - height;
+ drawBar(canvas, bar, createBarRect(
+ bar.barGroup.leftPix, top,
+ bar.barGroup.rightPix, bottom,
+ bar.formatter));
+ bottom = top;
+ }
+ break;
+ default:
+ throw new UnsupportedOperationException("Unexpected BarOrientation: " + barOrientation);
+ }
+ }
+ }
+
+ protected RectF createBarRect(float w1, float h1, float w2, float h2, BarFormatter formatter) {
+ final RectF result = RectFUtils.createFromEdges(w1, h1,w2, h2);
+ result.left += formatter.getMarginLeft();
+ result.right -= formatter.getMarginRight();
+ result.top += formatter.getMarginTop();
+ result.bottom -= formatter.getMarginBottom();
+ return result;
+ }
+
+ protected void drawBar(Canvas canvas, Bar bar, RectF rect) {
+
+ // null yVals are skipped:
+ if(bar.getY() == null) {
+ return;
+ }
+
+ BarFormatter formatter = getFormatter(bar.i, bar.series);
+ if(formatter == null) {
+ formatter = bar.formatter;
+ }
+ if(rect.height() > 0 && rect.width() > 0) {
+ if (formatter.hasFillPaint()) {
+ canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom,
+ formatter.getFillPaint());
+ }
+
+ if (formatter.hasLinePaint()) {
+ canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom,
+ formatter.getBorderPaint());
+ }
+ }
+
+ PointLabelFormatter plf =
+ formatter.hasPointLabelFormatter() ? formatter.getPointLabelFormatter() : null;
+
+ PointLabeler pointLabeler =
+ formatter != null ? formatter.getPointLabeler() : null;
+
+ if (plf != null && plf.hasTextPaint() && pointLabeler != null) {
+ canvas.drawText(pointLabeler.getLabel(bar.series, bar.i),
+ rect.centerX() + plf.hOffset, bar.yPix + plf.vOffset,
+ plf.getTextPaint());
+ }
}
- public class Bar {
+ /**
+ *
+ * @param
+ */
+ public static class Bar {
+
public final XYSeries series;
- private final FormatterType formatter;
- public final int seriesIndex;
- public final double yVal, xVal;
- public final int intX, intY;
- public final double pixX, pixY;
- protected BarGroup barGroup;
-
- public Bar(XYSeries series, FormatterType formatter, int seriesIndex, RectF plotArea) {
- this.series = series;
+ public final FormatterType formatter;
+ public final int i;
+ public final int seriesOrder;
+ public final float xPix;
+ public final float yPix;
+ protected BarGroup barGroup;
+
+ // TODO: factor out plot param
+ public Bar(XYPlot plot, XYSeries series, FormatterType formatter, int seriesOrder, int i, RectF plotArea) {
+ this.series = series;
this.formatter = formatter;
- this.seriesIndex = seriesIndex;
-
- this.xVal = series.getX(seriesIndex).doubleValue();
- this.pixX = getPlot().getBounds().getxRegion()
- .transform(xVal, plotArea.left, plotArea.right, false);
- this.intX = (int) Math.round(pixX);
-
- if (series.getY(seriesIndex) != null) {
- this.yVal = series.getY(seriesIndex).doubleValue();
- this.pixY = getPlot().getBounds().yRegion
- .transform(yVal, plotArea.top, plotArea.bottom, true);
- this.intY = (int) Math.round(pixY);
- } else {
- this.yVal = 0;
- this.pixY = plotArea.bottom;
- this.intY = (int) pixY;
- }
- }
-
- public FormatterType getFormatter() {
- FormatterType f = BarRenderer.this.getFormatter(seriesIndex, series);
- return f != null ? f : formatter;
+ this.i = i;
+ this.seriesOrder = seriesOrder;
+
+ final double xVal = series.getX(i).doubleValue();
+ xPix = (float) plot.getBounds().getxRegion()
+ .transform(xVal, plotArea.left, plotArea.right, false);
+
+ if (series.getY(i) != null) {
+ final double yVal = series.getY(i).doubleValue();
+ this.yPix = (float) plot.getBounds().yRegion
+ .transform(yVal, plotArea.top, plotArea.bottom, true);
+ } else {
+ this.yPix = 0;
+ }
+ }
+
+ public Number getY() {
+ return series.getY(i);
}
}
-
- private class BarGroup {
- public ArrayList bars;
- public int intX;
- public int width, leftX, rightX;
- public RectF plotArea;
- public BarGroup prev;
-
- public BarGroup(int intX, RectF plotArea) {
- // Setup the TreeMap with the required comparator
- this.bars = new ArrayList(); // create a comparator that compares series title given the index.
- this.intX = intX;
- this.plotArea = plotArea;
- }
-
- public void addBar(Bar bar) {
- bar.barGroup = this;
- this.bars.add(bar);
- }
+
+ /**
+ * A collection of one or more {@Bar} instances sharing a common iVal.
+ */
+ private static class BarGroup {
+
+ public ArrayList bars;
+ public int i;
+ public float centerPix;
+ public float leftPix;
+ public float rightPix;
+ public RectF plotArea;
+
+ public BarGroup(int i, float centerPix, RectF plotArea) {
+ // Setup the TreeMap with the required comparator
+ this.bars = new ArrayList<>(); // create a comparator that compares series title given the index.
+ this.centerPix = centerPix;
+ this.plotArea = plotArea;
+ this.i = i;
+ }
+
+ public void addBar(Bar bar) {
+ bar.barGroup = this;
+ this.bars.add(bar);
+ }
+
+ protected float getWidth() {
+ return rightPix - leftPix;
+ }
}
+ /**
+ * Used to determine the order in which bars of the same group will be drawn.
+ */
@SuppressWarnings("WeakerAccess")
- public class BarComparator implements Comparator{
+ public static class BarComparator implements Comparator {
+
+ private final BarOrientation barOrientation;
+ private final float rangeOriginPx;
+
+ public BarComparator(BarOrientation barOrientation, float rangeOriginPx) {
+ this.rangeOriginPx = rangeOriginPx;
+ this.barOrientation = barOrientation;
+ }
@Override
public int compare(Bar bar1, Bar bar2) {
- switch (style) {
- case OVERLAID:
- return Integer.valueOf(bar1.intY).compareTo(bar2.intY);
- case SIDE_BY_SIDE:
- return bar1.series.getTitle().compareToIgnoreCase(bar2.series.getTitle());
- case STACKED:
- return bar1.series.getTitle().compareToIgnoreCase(bar2.series.getTitle());
- default:
- return 0;
- }
+ switch (barOrientation) {
+ case OVERLAID:
+ if(bar1.yPix > rangeOriginPx && bar2.yPix > rangeOriginPx) {
+ return Float.valueOf(bar2.yPix).compareTo(bar1.yPix);
+ } else {
+ return Float.valueOf(bar1.yPix).compareTo(bar2.yPix);
+ }
+ case IN_ORDER:
+ case SIDE_BY_SIDE:
+ case STACKED:
+ default:
+ return Integer.valueOf(bar1.seriesOrder).compareTo(bar2.seriesOrder);
+ }
}
- }
-
+ }
+
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java
index b351358b..3a794b5c 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java
@@ -16,11 +16,12 @@
package com.androidplot.xy;
-import android.content.*;
-import android.graphics.*;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Paint;
-import com.androidplot.ui.*;
-import com.androidplot.util.*;
+import com.androidplot.ui.SeriesRenderer;
+import com.androidplot.util.PixelUtils;
/**
* Format for drawing a value using {@link BubbleRenderer}.
@@ -51,7 +52,7 @@ public class BubbleFormatter extends XYSeriesFormatter {
setPointLabeler(new PointLabeler() {
@Override
public String getLabel(BubbleSeries series, int index) {
- return series.getZ(index) + "";
+ return String.valueOf(series.getZ(index));
}
});
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java
index 1e0ba9c5..dcebf6cb 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java
@@ -3,7 +3,6 @@
import android.graphics.*;
import com.androidplot.Region;
-import com.androidplot.exception.*;
import com.androidplot.ui.*;
import com.androidplot.util.*;
@@ -45,7 +44,7 @@ public BubbleRenderer(XYPlot plot) {
@Override
protected void onRender(Canvas canvas, RectF plotArea, BubbleSeries series,
- FormatterType formatter, RenderStack stack) throws PlotRenderException {
+ FormatterType formatter, RenderStack stack) {
Region magnitudeBounds = calculateBounds();
for(int i = 0; i < series.size(); i++) {
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java b/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java
index 31e272f6..aea93ecd 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java
@@ -20,7 +20,7 @@
import java.util.List;
/**
- * A primitive implementation of Catmull-Rom interpolation, based on the information found at:
+ * An implementation of Catmull-Rom interpolation, based on the information found at:
* http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/19283471#19283471
*/
public class CatmullRomInterpolator implements Interpolator {
@@ -196,7 +196,7 @@ public List interpolate(XYSeries series, Params params) {
* @return the list of coordinates that define the CatmullRom curve
* between the points defined by index+1 and index+2.
*/
- public List interpolate(XYSeries series, int index, Params params) {
+ protected List interpolate(XYSeries series, int index, Params params) {
List result = new ArrayList<>();
double[] x = new double[4];
double[] y = new double[4];
@@ -251,7 +251,7 @@ public List interpolate(XYSeries series, int index, Params params) {
* position between p1 and p2 to interpolate the value.
* @return
*/
- public static double interpolate(double[] p, double[] time, double t) {
+ protected static double interpolate(double[] p, double[] time, double t) {
double L01 = p[0] * (time[1] - t) / (time[1] - time[0]) + p[1] * (t - time[0]) / (time[1] - time[0]);
double L12 = p[1] * (time[2] - t) / (time[2] - time[1]) + p[2] * (t - time[1]) / (time[2] - time[1]);
double L23 = p[2] * (time[3] - t) / (time[3] - time[2]) + p[3] * (t - time[2]) / (time[3] - time[2]);
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java
index 6a646649..df070325 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java
@@ -16,12 +16,17 @@
package com.androidplot.xy;
-import android.graphics.*;
-import com.androidplot.exception.PlotRenderException;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import androidx.annotation.NonNull;
+
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesRenderer;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
/**
* A faster implementation of of {@link LineAndPointRenderer}. For performance reasons, has these constraints:
@@ -32,6 +37,11 @@
*/
public class FastLineAndPointRenderer extends XYSeriesRenderer {
+ /**
+ * A line drawn by {@link Canvas#drawLines(float[], int, int, Paint)} must be defined by at
+ * least four points {@code x0, y0, x1, y1}
+ */
+ private static final int MINIMUM_NUMBER_OF_POINTS_TO_DEFINE_A_LINE = 4;
private float[] points;
List segmentOffsets = new ArrayList<>();
List segmentLengths = new ArrayList<>();
@@ -40,7 +50,7 @@ public FastLineAndPointRenderer(XYPlot plot) {
}
@Override
- protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) throws PlotRenderException {
+ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) {
segmentOffsets.clear();
segmentLengths.clear();
@@ -51,18 +61,13 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte
points = new float[series.size()*2];
}
-// final Number minX = getPlot().getCalculatedMinX();
-// final Number maxX = getPlot().getCalculatedMaxX();
-// final Number minY = getPlot().getCalculatedMinY();
-// final Number maxY = getPlot().getCalculatedMaxY();
-
int segmentLen = 0;
boolean isLastPointNull = true;
+ PointF resultPoint = new PointF();
for (int i = 0, j = 0; i < series.size(); i++, j+=2) {
Number y = series.getY(i);
Number x = series.getX(i);
- PointF thisPoint;
if (y != null && x != null) {
if(isLastPointNull) {
segmentOffsets.add(j);
@@ -70,9 +75,9 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte
isLastPointNull = false;
}
- thisPoint = getPlot().getBounds().transformScreen(x, y, plotArea);
- points[j] = thisPoint.x;
- points[j+1] = thisPoint.y;
+ getPlot().getBounds().transformScreen(resultPoint, x, y, plotArea);
+ points[j] = resultPoint.x;
+ points[j + 1] = resultPoint.y;
segmentLen+=2;
// if this is the last point, account for it in segment lengths:
@@ -95,10 +100,14 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte
}
}
- protected void drawSegment(Canvas canvas, float[] points, int offset, int len, Formatter formatter) {
+ protected void drawSegment(@NonNull Canvas canvas,
+ @NonNull float[] points,
+ int offset,
+ int len,
+ Formatter formatter) {
if(formatter.linePaint != null) {
// draw lines:
- if (len >= 4) {
+ if (len >= MINIMUM_NUMBER_OF_POINTS_TO_DEFINE_A_LINE) {
// optimization to avoid using 2x storage space to represent the full path:
if ((len & 2) != 0) {
canvas.drawLines(points, offset, len - 2, formatter.linePaint);
@@ -117,10 +126,16 @@ protected void drawSegment(Canvas canvas, float[] points, int offset, int len, F
}
@Override
- protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
+ protected void doDrawLegendIcon(@NonNull Canvas canvas,
+ @NonNull RectF rect,
+ @NonNull Formatter formatter) {
if(formatter.hasLinePaint()) {
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
}
+
+ if(formatter.hasVertexPaint()) {
+ canvas.drawPoint(rect.centerX(), rect.centerY(), formatter.getVertexPaint());
+ }
}
/**
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java
index 4faa4d54..de5d534b 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java
@@ -3,6 +3,10 @@
/**
* An implementation of {@link XYSeries} that defines additional methods to speed up rendering by
* giving a hint to the renderer about the min/max values contained in the series.
+ *
+ * Note that these hints can only be leveraged if the containing XYPlot's constraints completely
+ * contain the FastXYSeries min/max values. If this condition is not met then XYPlot falls back
+ * to manually determining the min/max values of the series that exist within the defined constraints.
*/
public interface FastXYSeries extends XYSeries {
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java
index 240f58dc..36ff371e 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java
@@ -1,8 +1,12 @@
package com.androidplot.xy;
-import com.androidplot.util.*;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
-import java.util.*;
+import com.androidplot.util.FastNumber;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* An efficient implementation of {@link EditableXYSeries} intended for use cases where
@@ -14,7 +18,10 @@
*/
public class FixedSizeEditableXYSeries implements EditableXYSeries {
+ @NonNull
private List xVals = new ArrayList<>();
+
+ @NonNull
private List yVals = new ArrayList<>();
private String title;
@@ -24,13 +31,13 @@ public FixedSizeEditableXYSeries(String title, int size) {
}
@Override
- public void setX(Number x, int index) {
- xVals.set(index, new FastNumber(x));
+ public void setX(@Nullable Number x, int index) {
+ xVals.set(index, FastNumber.orNull(x));
}
@Override
- public void setY(Number y, int index) {
- yVals.set(index, new FastNumber(y));
+ public void setY(@Nullable Number y, int index) {
+ yVals.set(index, FastNumber.orNull(y));
}
/**
@@ -44,7 +51,7 @@ public void resize(int size) {
resize(yVals, size);
}
- protected void resize(List list, int size) {
+ protected void resize(@NonNull List list, int size) {
if (size > list.size()) {
while (list.size() < size) {
list.add(null);
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java
index d2fe4c73..503d624d 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java
@@ -19,7 +19,6 @@
import android.graphics.Canvas;
import android.graphics.RectF;
import android.util.Log;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesBundle;
@@ -42,7 +41,7 @@ public GroupRenderer(XYPlot plot) {
@Override
protected void onRender(Canvas canvas, RectF plotArea, XYSeries series,
- FormatterType formatter, RenderStack stack) throws PlotRenderException {
+ FormatterType formatter, RenderStack stack) {
// get all the data associated with this renderer:
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
index 48d397be..f50d6186 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
@@ -22,13 +22,15 @@
import android.graphics.PointF;
import android.graphics.RectF;
+import com.androidplot.Plot;
+import com.androidplot.PlotListener;
import com.androidplot.Region;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.ui.RenderStack;
import com.androidplot.util.*;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Renders a point as a line with the vertices marked. Requires 2 or more points to
@@ -41,12 +43,26 @@ public class LineAndPointRenderer e
private final Path path = new Path();
+ protected final ConcurrentHashMap> pointsCaches
+ = new ConcurrentHashMap<>(2, 0.75f, 2);
+
public LineAndPointRenderer(XYPlot plot) {
super(plot);
+ plot.addListener(new PlotListener() {
+ @Override
+ public void onBeforeDraw(Plot source, Canvas canvas) {
+ cullPointsCache();
+ }
+
+ @Override
+ public void onAfterDraw(Plot source, Canvas canvas) {
+
+ }
+ });
}
@Override
- public void onRender(Canvas canvas, RectF plotArea, XYSeries series, FormatterType formatter, RenderStack stack) throws PlotRenderException {
+ public void onRender(Canvas canvas, RectF plotArea, XYSeries series, FormatterType formatter, RenderStack stack) {
drawSeries(canvas, plotArea, series, formatter);
}
@@ -77,17 +93,37 @@ protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) {
path.lineTo(thisPoint.x, thisPoint.y);
}
- final ArrayList points = new ArrayList<>();
+ /**
+ * Retrieves or initializes a list for storing calculated screen-coords to render as points.
+ * Also handles automatic resizing and culling of unused caches.
+ * Should only be called once per render cycle.
+ * @param series
+ * @return
+ */
+ protected ArrayList getPointsCache(XYSeries series) {
+ ArrayList pointsCache = pointsCaches.get(series);
+ final int seriesSize = series.size();
+ if(pointsCache == null) {
+ pointsCache = new ArrayList<>(seriesSize);
+ pointsCaches.put(series, pointsCache);
+ }
- // avoids needless new allocations of the points array
- protected void resizePointsArray(int newSize) {
- if(points.size() < newSize) {
- while(points.size() < newSize) {
- points.add(null);
+ if(pointsCache.size() < seriesSize) {
+ while(pointsCache.size() < seriesSize) {
+ pointsCache.add(null);
+ }
+ } else if(pointsCache.size() > seriesSize) {
+ while(pointsCache.size() > seriesSize) {
+ pointsCache.remove(0);
}
- } else if(points.size() > newSize) {
- while(points.size() > newSize) {
- points.remove(0);
+ }
+ return pointsCache;
+ }
+
+ protected void cullPointsCache() {
+ for(XYSeries series : pointsCaches.keySet()) {
+ if(!getPlot().getRegistry().contains(series, LineAndPointFormatter.class)) {
+ pointsCaches.remove(series);
}
}
}
@@ -96,37 +132,27 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn
PointF thisPoint;
PointF lastPoint = null;
PointF firstPoint = null;
- final int seriesSize = series.size();
path.reset();
- resizePointsArray(seriesSize);
+ final List points = getPointsCache(series);
int iStart = 0;
- int iEnd = seriesSize;
+ int iEnd = series.size();
if(SeriesUtils.getXYOrder(series) == OrderedXYSeries.XOrder.ASCENDING) {
final Region iBounds = SeriesUtils.iBounds(series, getPlot().getBounds());
iStart = iBounds.getMin().intValue();
if(iStart > 0) {
iStart--;
}
- iEnd = iBounds.getMax().intValue();
- if(iEnd < seriesSize - 1) {
+ iEnd = iBounds.getMax().intValue() + 1;
+ if(iEnd < series.size() - 1) {
iEnd++;
}
}
- final double minX = getPlot().getBounds().getMinX().doubleValue();
- final double maxX = getPlot().getBounds().getMaxX().doubleValue();
for (int i = iStart; i < iEnd; i++) {
final Number y = series.getY(i);
final Number x = series.getX(i);
PointF iPoint = points.get(i);
- final double dx = x.doubleValue();
- if(i > 0 && i < seriesSize - 1) {
- if (dx < minX || dx > maxX) {
- continue;
- }
- }
-
if (y != null && x != null) {
if(iPoint == null) {
iPoint = new PointF();
@@ -187,7 +213,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn
renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter);
}
}
- renderPoints(canvas, plotArea, series, points, formatter);
+ renderPoints(canvas, plotArea, series, iStart, iEnd, points, formatter);
}
/**
@@ -209,29 +235,28 @@ protected PointF convertPoint(XYCoords coord, RectF plotArea) {
return getPlot().getBounds().transformScreen(coord, plotArea);
}
- protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, List points,
+ protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, int iStart, int iEnd, List points,
LineAndPointFormatter formatter) {
- //PointLabelFormatter plf = formatter.getPointLabelFormatter();
if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) {
- int i = 0;
final Paint vertexPaint = formatter.hasVertexPaint() ? formatter.getVertexPaint() : null;
final boolean hasPointLabelFormatter = formatter.hasPointLabelFormatter();
final PointLabelFormatter plf = hasPointLabelFormatter ? formatter.getPointLabelFormatter() : null;
final PointLabeler pointLabeler = hasPointLabelFormatter ? formatter.getPointLabeler() : null;
- for (PointF p : points) {
+ for(int i = iStart; i < iEnd; i++) {
+ PointF p = points.get(i);
+ if(p != null) {
- // if vertexPaint is available, draw vertex:
- if (vertexPaint != null) {
- canvas.drawPoint(p.x, p.y, vertexPaint);
- }
+ // if vertexPaint is available, draw vertex:
+ if (vertexPaint != null) {
+ canvas.drawPoint(p.x, p.y, vertexPaint);
+ }
- // if textPaint and pointLabeler are available, draw point's text label:
- if (pointLabeler != null) {
- //final PointLabelFormatter plf = formatter.getPointLabelFormatter();
- canvas.drawText(pointLabeler.getLabel(series, i),
- p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint());
+ // if textPaint and pointLabeler are available, draw point's text label:
+ if (pointLabeler != null) {
+ canvas.drawText(pointLabeler.getLabel(series, i),
+ p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint());
+ }
}
- i++;
}
}
}
@@ -283,7 +308,7 @@ protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF first
RectF thisRegionRectF = thisRegionTransformed.asRectF();
if (thisRegionRectF != null) {
try {
- canvas.save(Canvas.ALL_SAVE_FLAG);
+ canvas.save();
canvas.clipPath(path);
canvas.drawRect(thisRegionRectF, regionFormatter.getPaint());
} finally {
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java
new file mode 100644
index 00000000..03760b39
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java
@@ -0,0 +1,136 @@
+package com.androidplot.xy;
+
+import com.androidplot.Region;
+import com.androidplot.util.SeriesUtils;
+
+/**
+ * Wrapper implementation of {@link XYSeries} that wraps another XYSeries, normalizing values in the range of 0 to 1.
+ * Note that it's possible to push normed values outside of the standard 0, 1 range by applying
+ * a sufficiently large offset.
+ */
+public class NormedXYSeries implements XYSeries {
+
+ private XYSeries rawData;
+
+ private Region minMaxX;
+ private Region minMaxY;
+
+ private Region transformX;
+ private Region transformY;
+
+ public static class Norm {
+
+ final Region minMax;
+ final double offset;
+ final boolean useOffsetCompression;
+
+ public Norm(Region minMax) {
+ this(minMax, 0, false);
+ }
+
+ /**
+ *
+ * @param minMax Boundary to use when calculating the norm coefficient. Set to null to let
+ * Androidplot auto calculate the bounds. (Very inefficient)
+ * @param offset An extra offset to apply, generally within the range of -1 and 1.
+ * This value is useful for adjusting the positioning of a series relative to another normalized series.
+ * @param useOffsetCompression If true, the offset value will result in further scaling down
+ * of the series data in order to ensure that all points within the specified bounds remain
+ * visible on the screen. If set to true, the specified offset MUST be > -1 and < 1. Will be
+ * ignored if bounds != null.
+ */
+ public Norm(Region minMax, double offset, boolean useOffsetCompression) {
+ this.minMax = minMax;
+ this.offset = offset;
+ this.useOffsetCompression = useOffsetCompression;
+
+ if (useOffsetCompression && (offset <= -1 || offset >= 1)) {
+ throw new IllegalArgumentException(
+ "When useOffsetCompression is true, offset must be > -1 and < 1.");
+ }
+ }
+ }
+
+ /**
+ * Normalizes yVals only, auto calculating min/max.
+ * @param rawData
+ */
+ public NormedXYSeries(XYSeries rawData) {
+ this(rawData, null, new Norm(null, 0, false));
+ }
+
+ /**
+ *
+ * @param rawData The XYSeries to be normalized.
+ * @param x Normalization to apply to xVals. Set to null to disable normalization on the x axis.
+ * @param y Normalization to apply to yVals. Set to null to disable normalization on the y axis.
+ */
+ public NormedXYSeries(XYSeries rawData, Norm x, Norm y) {
+ this.rawData = rawData;
+ normalize(x, y);
+ }
+
+ protected void normalize(Norm x, Norm y) {
+ if( x != null) {
+ this.minMaxX = x.minMax != null ? x.minMax : SeriesUtils.minMaxX(rawData);
+ this.transformX = calculateTransform(x);
+ }
+
+ if( y != null) {
+ this.minMaxY = y.minMax != null ? y.minMax : SeriesUtils.minMaxY(rawData);
+ this.transformY = calculateTransform(y);
+ }
+ }
+
+ protected Region calculateTransform(Norm norm) {
+ if(norm.useOffsetCompression) {
+ return new Region(
+ norm.offset > 0 ? norm.offset : 0,
+ norm.offset < 0 ? 1 + norm.offset : 1);
+ } else {
+ return new Region(0 + norm.offset, 1 + norm.offset);
+ }
+ }
+
+ @Override
+ public String getTitle() {
+ return rawData.getTitle();
+ }
+
+ @Override
+ public int size() {
+ return rawData.size();
+ }
+
+ public Number denormalizeXVal(Number xVal) {
+ if(xVal != null) {
+ return transformX.transform(xVal.doubleValue(), minMaxX);
+ }
+ return null;
+ }
+
+ public Number denormalizeYVal(Number yVal) {
+ if(yVal != null) {
+ return transformY.transform(yVal.doubleValue(), minMaxY);
+ }
+ return null;
+ }
+
+ @Override
+ public Number getX(int index) {
+ final Number xVal = rawData.getX(index);
+ if(xVal != null && transformX != null) {
+ return minMaxX.transform(xVal.doubleValue(), transformX);
+ }
+ return xVal;
+ }
+
+ @Override
+ public Number getY(int index) {
+ final Number yVal = rawData.getY(index);
+ if(yVal != null && transformY != null) {
+ return minMaxY.transform(yVal.doubleValue(), transformY);
+ }
+ return yVal;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java
index 659e38ea..820e4330 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java
@@ -9,13 +9,13 @@ public interface OrderedXYSeries extends XYSeries {
enum XOrder {
/**
* XVals are in strict ascending order such that:
- * x(i) > x(i+1) == true
+ * x(i) < x(i+1) == true
*/
ASCENDING,
/**
* XVals are in strict descending order such that:
- * x(i) < x(i+1) == true
+ * x(i) > x(i+1) == true
*/
DESCENDING,
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java
index 441edb6f..9cd80847 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java
@@ -2,11 +2,13 @@
import android.graphics.RectF;
import android.graphics.PointF;
+import androidx.annotation.NonNull;
import android.view.*;
import com.androidplot.*;
import com.androidplot.util.*;
+import java.io.Serializable;
import java.util.*;
/**
@@ -25,6 +27,8 @@ public class PanZoom implements View.OnTouchListener {
private XYPlot plot;
private Pan pan;
private Zoom zoom;
+
+ private ZoomLimit zoomLimit;
private boolean isEnabled = true;
private DragState dragState = DragState.NONE;
@@ -33,6 +37,7 @@ public class PanZoom implements View.OnTouchListener {
// rectangle created by the space between two fingers
protected RectF fingersRect;
private View.OnTouchListener delegate;
+ private State state = new State();
// Definition of the touch states
protected enum DragState {
@@ -77,25 +82,125 @@ public enum Zoom {
SCALE
}
- protected PanZoom(XYPlot plot, Pan pan, Zoom zoom) {
+ /**
+ * Limits imposed on the zoom.
+ */
+ public enum ZoomLimit {
+ /**
+ * Do not zoom outside the plots outer bounds, if they are defined.
+ */
+ OUTER,
+
+ /**
+ * Additionally to the outer bounds if plot.StepModel defines a value based increment
+ * make sure at least one tick is visible by not zooming in further.
+ */
+ MIN_TICKS
+ }
+
+ // TODO: consider making this immutable / threadsafe
+ public static class State implements Serializable {
+ private Number domainLowerBoundary;
+ private Number domainUpperBoundary;
+ private Number rangeLowerBoundary;
+ private Number rangeUpperBoundary;
+ private BoundaryMode domainBoundaryMode;
+ private BoundaryMode rangeBoundaryMode;
+
+ public void setDomainBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
+ this.domainLowerBoundary = lowerBoundary;
+ this.domainUpperBoundary = upperBoundary;
+ this.domainBoundaryMode = mode;
+ }
+
+ public void setRangeBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
+ this.rangeLowerBoundary = lowerBoundary;
+ this.rangeUpperBoundary = upperBoundary;
+ this.rangeBoundaryMode = mode;
+ }
+
+ public void applyDomainBoundaries(@NonNull XYPlot plot) {
+ plot.setDomainBoundaries(domainLowerBoundary, domainUpperBoundary, domainBoundaryMode);
+ }
+
+ public void applyRangeBoundaries(@NonNull XYPlot plot) {
+ plot.setRangeBoundaries(rangeLowerBoundary, rangeUpperBoundary, rangeBoundaryMode);
+ }
+
+ public void apply(@NonNull XYPlot plot) {
+ applyDomainBoundaries(plot);
+ applyRangeBoundaries(plot);
+ }
+ }
+
+ protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom) {
this.plot = plot;
this.pan = pan;
this.zoom = zoom;
+ this.zoomLimit = ZoomLimit.OUTER;
+ }
+
+ // additional constructor not to break api
+ protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom, ZoomLimit limit) {
+ this.plot = plot;
+ this.pan = pan;
+ this.zoom = zoom;
+ this.zoomLimit = limit;
+ }
+
+ public State getState() {
+ return this.state;
+ }
+
+ public void setState(@NonNull State state) {
+ this.state = state;
+ state.apply(plot);
+ }
+
+ protected void adjustRangeBoundary(Number lower, Number upper, BoundaryMode mode) {
+ state.setRangeBoundaries(lower, upper, mode);
+ state.applyRangeBoundaries(plot);
+ }
+
+ protected void adjustDomainBoundary(Number lower, Number upper, BoundaryMode mode) {
+ state.setDomainBoundaries(lower, upper, mode);
+ state.applyDomainBoundaries(plot);
}
/**
* Convenience method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using
* a default behavior of {@link Pan#BOTH} and {@link Zoom#SCALE}.
- * Use {@link PanZoom#attach(XYPlot, Pan, Zoom)} for finer grain control of this behavior.
+ * Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior.
* @param plot
* @return
*/
- public static PanZoom attach(XYPlot plot) {
+ public static PanZoom attach(@NonNull XYPlot plot) {
return attach(plot, Pan.BOTH, Zoom.SCALE);
}
- public static PanZoom attach(XYPlot plot, Pan pan, Zoom zoom) {
- PanZoom pz = new PanZoom(plot, pan, zoom);
+ /**
+ * Old method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using
+ * the default behavior of {@link ZoomLimit#OUTER}.
+ * Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior.
+ * @param plot
+ * @param pan
+ * @param zoom
+ * @return
+ */
+ public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom) {
+ return attach(plot,pan,zoom, ZoomLimit.OUTER);
+ }
+
+ /**
+ * New method for enabling pan/zoom behavior on an instance of {@link XYPlot}.
+ * @param plot
+ * @param pan
+ * @param zoom
+ * @param limit
+ * @return
+ */
+ public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom, @NonNull ZoomLimit limit) {
+ PanZoom pz = new PanZoom(plot, pan, zoom, limit);
plot.setOnTouchListener(pz);
return pz;
}
@@ -186,14 +291,15 @@ protected void pan(final MotionEvent motionEvent) {
final PointF oldFirstFinger = firstFingerPos; //save old position of finger
firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position
- Region newBounds = new Region();
if (EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) {
+ Region newBounds = new Region();
calculatePan(oldFirstFinger, newBounds, true);
- plot.setDomainBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
+ adjustDomainBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
}
if (EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) {
+ Region newBounds = new Region();
calculatePan(oldFirstFinger, newBounds, false);
- plot.setRangeBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
+ adjustRangeBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
}
plot.redraw();
@@ -242,10 +348,9 @@ protected void calculatePan(final PointF oldFirstFinger, Region bounds, final bo
}
protected boolean isValidScale(float scale) {
- if (Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) {
- return false;
- }
- return true;
+ return !Float.isInfinite(scale)
+ && !Float.isNaN(scale)
+ && (!(scale > -0.001) || !(scale < 0.001));
}
protected void zoom(final MotionEvent motionEvent) {
@@ -300,14 +405,14 @@ protected void zoom(final MotionEvent motionEvent) {
Zoom.STRETCH_BOTH,
Zoom.SCALE).contains(zoom)) {
calculateZoom(newRect, scaleX, true);
- plot.setDomainBoundaries(newRect.left, newRect.right, BoundaryMode.FIXED);
+ adjustDomainBoundary(newRect.left, newRect.right, BoundaryMode.FIXED);
}
if (EnumSet.of(
Zoom.STRETCH_VERTICAL,
Zoom.STRETCH_BOTH,
Zoom.SCALE).contains(zoom)) {
calculateZoom(newRect, scaleY, false);
- plot.setRangeBoundaries(newRect.top, newRect.bottom, BoundaryMode.FIXED);
+ adjustRangeBoundary(newRect.top, newRect.bottom, BoundaryMode.FIXED);
}
plot.redraw();
}
@@ -331,13 +436,21 @@ protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) {
}
final float midPoint = calcMax - (span / 2.0f);
- final float offset = span * scale / 2.0f;
+ float offset = span * scale / 2.0f;
+ final RectRegion limits = plot.getOuterLimits();
if (isHorizontal ) {
- final RectRegion limits = plot.getOuterLimits();
+ // zoom limited and increment by value StepMode?
+ if (zoomLimit == ZoomLimit.MIN_TICKS) {
+ // make sure we do not zoom in too far (there should be at least one grid line visible)
+ if (plot.getDomainStepValue() > (scale*span)) {
+ offset = (float)(plot.getDomainStepValue() / 2.0f);
+ }
+ }
+
+ newRect.left = midPoint - offset;
+ newRect.right = midPoint + offset;
if(limits.isFullyDefined()) {
- newRect.left = midPoint - offset;
- newRect.right = midPoint + offset;
if (newRect.left < limits.getMinX().floatValue()) {
newRect.left = limits.getMinX().floatValue();
}
@@ -346,10 +459,17 @@ protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) {
}
}
} else {
- final RectRegion limits = plot.getOuterLimits();
+ // zoom limited and increment by value StepMode?
+ if (zoomLimit == ZoomLimit.MIN_TICKS) {
+ // make sure we do not zoom in too far (there should be at least one grid line visible)
+ if (plot.getRangeStepValue() > (scale*span)) {
+ offset = (float)(plot.getRangeStepValue() / 2.0f);
+ }
+ }
+
+ newRect.top = midPoint - offset;
+ newRect.bottom = midPoint + offset;
if(limits.isFullyDefined()) {
- newRect.top = midPoint - offset;
- newRect.bottom = midPoint + offset;
if (newRect.top < limits.getMinY().floatValue()) {
newRect.top = limits.getMinY().floatValue();
}
@@ -376,6 +496,14 @@ public void setZoom(Zoom zoom) {
this.zoom = zoom;
}
+ public ZoomLimit getZoomLimit() {
+ return zoomLimit;
+ }
+
+ public void setZoomLimit(ZoomLimit zoomLimit) {
+ this.zoomLimit = zoomLimit;
+ }
+
public View.OnTouchListener getDelegate() {
return delegate;
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
index c2182691..0bb8988d 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
@@ -20,6 +20,7 @@
import android.graphics.RectF;
import com.androidplot.Region;
+
import java.util.ArrayList;
import java.util.List;
@@ -244,7 +245,7 @@ public Number getHeight() {
* @param y
* @return
*/
- private Number distanceBetween(Number x, Number y) {
+ private static Number distanceBetween(Number x, Number y) {
return Math.abs(x.doubleValue() - y.doubleValue());
}
@@ -345,38 +346,12 @@ public boolean contains(Number x, Number y) {
return getxRegion().contains(x) && getyRegion().contains(y);
}
- /**
- * Checks to see whether the specified line. Note that this implementation will return
- * true even if the line is completely enclosed by this RectRegion.
- * WARNING: this implementation has problems. See associated unit test for details.
- * @param x1 x-coord of the line beginning
- * @param y1 y-coord of the line beginning
- * @param x2 x-coord of the line end
- * @param y2 y-coord of the line end
- * @return True if this RectRegion overlaps any part of the specified line.
- */
- public boolean intersectsWithLine(Number x1, Number y1, Number x2, Number y2) {
- if(contains(x1, y1) || contains(x2, y2)) {
- return true;
- }
-
- // if true, it means that these points exist on different sides of the rect's edges
- final boolean x1MinRelation = x1.doubleValue() < getMinX().doubleValue();
- final boolean x2MinRelation = x2.doubleValue() < getMinX().doubleValue();
- final boolean xMinRelation = x1MinRelation &! x2MinRelation;
-
- final boolean x1MaxRelation = x1.doubleValue() < getMaxX().doubleValue();
- final boolean x2MaxRelation = x2.doubleValue() < getMaxX().doubleValue();
- final boolean xMaxRelation = x1MaxRelation &! x2MaxRelation;
-
- final boolean y1MinRelation = y1.doubleValue() < getMinY().doubleValue();
- final boolean y2MinRelation = y2.doubleValue() < getMinY().doubleValue();
- final boolean yMinRelation = y1MinRelation &! y2MinRelation;
-
- final boolean y1MaxRelation = y1.doubleValue() < getMaxY().doubleValue();
- final boolean y2MaxRelation = y2.doubleValue() < getMaxY().doubleValue();
- final boolean yMaxRelation = y1MaxRelation &! y2MaxRelation;
-
- return ((xMinRelation | xMaxRelation) || getxRegion().contains(x1) & (yMinRelation | yMaxRelation) || getyRegion().contains(y1));
+ @Override
+ public String toString() {
+ return "RectRegion{" +
+ "xRegion=" + xRegion +
+ ", yRegion=" + yRegion +
+ ", label='" + label + '\'' +
+ '}';
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java
index b10903b1..f795536b 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java
@@ -89,7 +89,7 @@ public void run() {
lastResamplingException = ex;
}
}
- });
+ }, "Androidplot XY Series Sampler");
getZoomLevels().add(thisSeries);
threads.add(thread);
thread.start();
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java
new file mode 100644
index 00000000..2e194f17
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java
@@ -0,0 +1,67 @@
+package com.androidplot.xy;
+
+/**
+ * Wraps an existing {@link XYSeries} allowing easy scaling of that series' xy values.
+ */
+public class ScalingXYSeries implements XYSeries {
+
+ private double scale;
+ private XYSeries series;
+ private Mode mode;
+
+ public enum Mode {
+ X_ONLY,
+ Y_ONLY,
+ X_AND_Y
+ }
+
+ /**
+ *
+ * @param series The {@link XYSeries} to be scaled
+ * @param scale The initial scale to be applied
+ * @param mode Determines which axis (or both) to which scaling will be applied.
+ */
+ public ScalingXYSeries(XYSeries series, double scale, Mode mode) {
+ this.series = series;
+ this.scale = scale;
+ this.mode = mode;
+ }
+
+ @Override
+ public String getTitle() {
+ return series.getTitle();
+ }
+
+ @Override
+ public int size() {
+ return series.size();
+ }
+
+ @Override
+ public Number getX(int index) {
+ Number x = series.getX(index);
+ if(mode == Mode.X_ONLY || mode == Mode.X_AND_Y) {
+ return x == null ? null : x.doubleValue() * scale;
+ } else {
+ return x;
+ }
+ }
+
+ @Override
+ public Number getY(int index) {
+ Number y = series.getY(index);
+ if(mode == Mode.Y_ONLY || mode == Mode.X_AND_Y) {
+ return y == null ? null : y.doubleValue() * scale;
+ } else {
+ return y;
+ }
+ }
+
+ public double getScale() {
+ return scale;
+ }
+
+ public void setScale(double scale) {
+ this.scale = scale;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java
index 2032f301..b9178123 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java
@@ -17,42 +17,34 @@
package com.androidplot.xy;
import android.graphics.Canvas;
+
import com.androidplot.Plot;
import com.androidplot.PlotListener;
-import com.androidplot.util.*;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* A convenience class used to create instances of XYPlot generated from Lists of Numbers.
*/
-public class SimpleXYSeries implements EditableXYSeries, PlotListener {
-
- private static final String TAG = SimpleXYSeries.class.getName();
-
- @Override
- public void onBeforeDraw(Plot source, Canvas canvas) {
- lock.readLock().lock();
- }
+public class SimpleXYSeries implements EditableXYSeries, OrderedXYSeries, PlotListener {
+ private volatile LinkedList xVals = new LinkedList<>();
+ private volatile LinkedList yVals = new LinkedList<>();
+ private volatile String title = null;
- @Override
- public void onAfterDraw(Plot source, Canvas canvas) {
- lock.readLock().unlock();
- }
+ private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
+ private XOrder xOrder = XOrder.NONE;
public enum ArrayFormat {
Y_VALS_ONLY,
XY_VALS_INTERLEAVED
}
- private volatile LinkedList xVals = new LinkedList<>();
- private volatile LinkedList yVals = new LinkedList<>();
- private volatile String title = null;
-
- private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
-
public SimpleXYSeries(String title) {
this.title = title;
}
@@ -61,11 +53,39 @@ public SimpleXYSeries(ArrayFormat format, String title, Number... model) {
this(asNumberList(model), format, title);
}
+ /**
+ * Retrieve the current x-ordering specified for this series. Default is
+ * {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE}.
+ * @return
+ */
+ @Override
+ public XOrder getXOrder() {
+ return xOrder;
+ }
+
+ /**
+ * If XVals are in strict ascending order, use this method to set
+ * {@link com.androidplot.xy.OrderedXYSeries.XOrder#ASCENDING} to provide an optimization
+ * hint to the renderer.
+ * @param xOrder
+ */
+ public void setXOrder(XOrder xOrder) {
+ this.xOrder = xOrder;
+ }
+
+ @Override
+ public void onBeforeDraw(Plot source, Canvas canvas) {
+ lock.readLock().lock();
+ }
+
+ @Override
+ public void onAfterDraw(Plot source, Canvas canvas) {
+ lock.readLock().unlock();
+ }
+
protected static List asNumberList(Number... model) {
- List numbers = new ArrayList<>();
- for(Number n : model) {
- numbers.add(n);
- }
+ List numbers = new ArrayList<>(model.length);
+ Collections.addAll(numbers, model);
return numbers;
}
@@ -121,7 +141,7 @@ public void setModel(List extends Number> model, ArrayFormat format) {
lock.writeLock().lock();
try {
// empty the current values:
- xVals = null;
+ xVals.clear();
yVals.clear();
// make sure the new model has data:
@@ -133,15 +153,16 @@ public void setModel(List extends Number> model, ArrayFormat format) {
// array containing only y-vals. assume x = index:
case Y_VALS_ONLY:
- for(Number n : model) {
- yVals.add(n);
+ yVals.addAll(model);
+ for(int i = 0; i < yVals.size(); i++) {
+ xVals.add(i);
}
break;
// xy interleaved array:
case XY_VALS_INTERLEAVED:
if (xVals == null) {
- xVals = new LinkedList();
+ xVals = new LinkedList<>();
}
if (model.size() % 2 != 0) {
throw new IndexOutOfBoundsException("Cannot auto-generate series from odd-sized xy List.");
@@ -194,7 +215,6 @@ public void resize(int size) {
try {
lock.writeLock().lock();
if (xVals.size() < size) {
-
for (int i = xVals.size(); i < size; i++) {
xVals.add(null);
yVals.add(null);
@@ -321,7 +341,9 @@ public LinkedList getyVals() {
public void clear() {
lock.writeLock().lock();
try {
- xVals.clear();
+ if (xVals != null) {
+ xVals.clear();
+ }
yVals.clear();
} finally {
lock.writeLock().unlock();
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java b/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java
index d5db2c9d..00f60747 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java
@@ -20,9 +20,11 @@
* INCREMENTAL_VALUE - (default) draw a tick every n values.
* INCREMENTAL_PIXEL - draw a tick every n pixels.
* SUBDIVIDE - draw n number of evenly spaced lines.
+ * INCREMENT_BY_FIT choose increment from a list of possible values
*/
public enum StepMode {
SUBDIVIDE, // default
INCREMENT_BY_VAL,
- INCREMENT_BY_PIXELS
+ INCREMENT_BY_PIXELS,
+ INCREMENT_BY_FIT
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java b/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java
new file mode 100644
index 00000000..d2b12f0b
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java
@@ -0,0 +1,85 @@
+package com.androidplot.xy;
+
+import com.androidplot.Region;
+
+import java.util.Arrays;
+
+/**
+ * Subclass of StepModel that chooses from predefined step values. Depending on the currently
+ * displayed range (by value) choose increment so that the number of lines
+ * is closest to StepModel.value
+ */
+public class StepModelFit extends StepModel {
+
+ private double[] steps; // list of steps to choose from
+ private Region scale; // axis region on display
+
+ public StepModelFit(Region axisRegion, double[] increments, double numLines) {
+ super(StepMode.INCREMENT_BY_FIT, numLines);
+
+ setSteps(increments);
+ setScale(axisRegion);
+ }
+
+ public double[] getSteps() {
+ return steps;
+ }
+
+ public void setSteps(double[] steps) {
+
+ // sanity checks: no null, 0 or negative
+ if (steps == null || steps.length == 0)
+ return;
+
+ for (double step : steps) {
+ if (step <= 0.0d)
+ return;
+ }
+
+ this.steps = steps;
+ }
+
+ public Region getScale() {
+ return scale;
+ }
+
+ public void setScale(Region scale) {
+ this.scale = scale;
+ }
+
+ // does not return StepModel.value instead calculates best fit
+ @Override
+ public double getValue() {
+
+ // no possible increments where supplied
+ // or no region defined
+ if (steps == null || scale == null || !scale.isDefined())
+ return super.getValue();
+
+ double curStep = steps[0];
+ double oldDistance = Math.abs((scale.length().doubleValue() / curStep)-super.getValue() );
+
+ // determine which step size comes closest to the desired number of steps
+ // since steps[] is a small array brute force search is ok
+ for (double step : steps) {
+
+ double newDistance = Math.abs((scale.length().doubleValue() / step)-super.getValue() );
+
+ // closer than previous stepping?
+ if (newDistance < oldDistance){
+ curStep = step;
+ oldDistance = newDistance;
+ }
+ }
+ return curStep;
+ }
+
+ @Override
+ public String toString() {
+ return "StepModelFit{" +
+ "steps=" + Arrays.toString(steps) +
+ ", scale=" + scale +
+ ", current stepping=" + getValue() +
+ '}';
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java
index 04f0449e..f14608d7 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java
@@ -23,7 +23,6 @@
* Renders a point as a line with the vertices marked. Requires 2 or more points to
* be rendered.
*/
-
public class StepRenderer extends LineAndPointRenderer {
public StepRenderer(XYPlot plot) {
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java b/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java
index 30e9aacd..de0436b2 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java
@@ -16,10 +16,14 @@
package com.androidplot.xy;
+import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
+import android.graphics.RectF;
+
import com.androidplot.ui.PositionMetric;
import com.androidplot.ui.TextOrientation;
+import com.androidplot.util.FontUtils;
/**
* Encapsulates a single axis line marker drawn onto an XYPlot at a specified value.
@@ -27,6 +31,8 @@
*/
public abstract class ValueMarker {
+ private static final int MARKER_LABEL_SPACING = 2;
+
public String getText() {
return text;
}
@@ -135,4 +141,37 @@ public PositionMetricType getTextPosition() {
public void setTextPosition(PositionMetricType textPosition) {
this.textPosition = textPosition;
}
+
+ /**
+ * Renders the text associated with user defined markers
+ *
+ * @param canvas
+ * @param text
+ * @param gridRect
+ * @param x
+ * @param y
+ */
+ protected void drawMarkerText(Canvas canvas, String text, RectF gridRect,
+ float x, float y) {
+ if (getText() != null) {
+ x += MARKER_LABEL_SPACING;
+ y -= MARKER_LABEL_SPACING;
+ RectF textRect = new RectF(FontUtils.getStringDimensions(text, getTextPaint()
+ ));
+ textRect.offsetTo(x, y - textRect.height());
+
+ if (textRect.right > gridRect.right) {
+ textRect.offset(-(textRect.right - gridRect.right), 0);
+ }
+
+ if (textRect.top < gridRect.top) {
+ textRect.offset(0, gridRect.top - textRect.top);
+ }
+
+ canvas.drawText(text, textRect.left, textRect.bottom, getTextPaint()
+ );
+ }
+ }
+
+ public abstract void draw(Canvas canvas, XYPlot plot, RectF gridRect);
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java b/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java
index af02ca64..a4c12438 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java
@@ -16,7 +16,10 @@
package com.androidplot.xy;
+import android.graphics.Canvas;
import android.graphics.Paint;
+import android.graphics.RectF;
+
import com.androidplot.ui.VerticalPositioning;
import com.androidplot.ui.VerticalPosition;
@@ -55,4 +58,19 @@ public XValueMarker(Number value, String text, VerticalPosition textPosition, Pa
public XValueMarker(Number value, String text, VerticalPosition textPosition, int linePaint, int textPaint) {
super(value, text, textPosition, linePaint, textPaint);
}
+
+ @Override
+ public void draw(Canvas canvas, XYPlot plot, RectF gridRect) {
+ if (getValue() != null) {
+ float xPix = (float) plot.getBounds().xRegion
+ .transform(getValue().doubleValue(), gridRect.left, gridRect.right, false);
+ canvas.drawLine(xPix, gridRect.top, xPix, gridRect.bottom, getLinePaint()
+ );
+ float yPix = getTextPosition().getPixelValue(gridRect.height());
+ yPix += gridRect.top;
+ if (getText() != null) {
+ drawMarkerText(canvas, getText(), gridRect, xPix, yPix);
+ }
+ }
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
index ae22a4ea..c4452d33 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
@@ -16,8 +16,12 @@
package com.androidplot.xy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
/**
* Calculates the min/max constraints for an xy plane.
+ *
* @since 0.9.7
*/
public class XYConstraints {
@@ -44,113 +48,148 @@ public XYConstraints() {
this(null, null, null, null);
}
- public XYConstraints(Number minX, Number maxX, Number minY, Number maxY) {
+ public XYConstraints(@Nullable Number minX, @Nullable Number maxX, @Nullable Number minY, @Nullable Number maxY) {
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
+ public boolean contains(@NonNull RectRegion rectRegion) {
+ return contains(rectRegion.getMinY(), rectRegion.getMinY())
+ && contains(rectRegion.getMaxX(), rectRegion.getMaxY());
+ }
+
public boolean contains(Number x, Number y) {
- if(x == null || y == null) {
+ if (x == null || y == null) {
// this is essentially an invisible point:
return false;
- } else {
- final double dx = x.doubleValue();
-
- if(minX != null && dx < minX.doubleValue()) {
- return false;
- } else if(maxX != null && dx > maxX.doubleValue()) {
- return false;
- } else {
- final double dy = y.doubleValue();
- if(minY != null && dy < minY.doubleValue()) {
- return false;
- } else if(maxY != null && dy > maxY.doubleValue()) {
- return false;
- }
- }
+ }
+
+ if (minX == null && maxX == null && minY == null && maxY == null) {
+ //there are no constraints
return true;
}
+
+ final double dx = x.doubleValue();
+ if (minX != null && dx < minX.doubleValue()) {
+ return false;
+ } else if (maxX != null && dx > maxX.doubleValue()) {
+ return false;
+ }
+
+ final double dy = y.doubleValue();
+ if (minY != null && dy < minY.doubleValue()) {
+ return false;
+ } else if (maxY != null && dy > maxY.doubleValue()) {
+ return false;
+ }
+
+ return true;
}
+ @Nullable
public Number getMinX() {
return minX;
}
+ @Nullable
public Number getMaxX() {
return maxX;
}
+ @Nullable
public Number getMinY() {
return minY;
}
+ @Nullable
public Number getMaxY() {
return maxY;
}
+ public void setMinX(@Nullable Number minX) {
+ this.minX = minX;
+ }
+
+ public void setMaxX(@Nullable Number maxX) {
+ this.maxX = maxX;
+ }
+
+ public void setMinY(@Nullable Number minY) {
+ this.minY = minY;
+ }
+
+ public void setMaxY(@Nullable Number maxY) {
+ this.maxY = maxY;
+ }
+
+ @NonNull
public XYFramingModel getDomainFramingModel() {
return domainFramingModel;
}
- public void setDomainFramingModel(XYFramingModel domainFramingModel) {
+ public void setDomainFramingModel(@NonNull XYFramingModel domainFramingModel) {
this.domainFramingModel = domainFramingModel;
}
+ @NonNull
public XYFramingModel getRangeFramingModel() {
return rangeFramingModel;
}
- public void setRangeFramingModel(XYFramingModel rangeFramingModel) {
+ public void setRangeFramingModel(@NonNull XYFramingModel rangeFramingModel) {
this.rangeFramingModel = rangeFramingModel;
}
+ @NonNull
public BoundaryMode getDomainUpperBoundaryMode() {
return domainUpperBoundaryMode;
}
- public void setDomainUpperBoundaryMode(BoundaryMode domainUpperBoundaryMode) {
+ public void setDomainUpperBoundaryMode(@NonNull BoundaryMode domainUpperBoundaryMode) {
this.domainUpperBoundaryMode = domainUpperBoundaryMode;
}
+ @NonNull
public BoundaryMode getDomainLowerBoundaryMode() {
return domainLowerBoundaryMode;
}
- public void setDomainLowerBoundaryMode(BoundaryMode domainLowerBoundaryMode) {
+ public void setDomainLowerBoundaryMode(@NonNull BoundaryMode domainLowerBoundaryMode) {
this.domainLowerBoundaryMode = domainLowerBoundaryMode;
}
+ @NonNull
public BoundaryMode getRangeUpperBoundaryMode() {
return rangeUpperBoundaryMode;
}
- public void setRangeUpperBoundaryMode(BoundaryMode rangeUpperBoundaryMode) {
+ public void setRangeUpperBoundaryMode(@NonNull BoundaryMode rangeUpperBoundaryMode) {
this.rangeUpperBoundaryMode = rangeUpperBoundaryMode;
}
+ @NonNull
public BoundaryMode getRangeLowerBoundaryMode() {
return rangeLowerBoundaryMode;
}
- public void setRangeLowerBoundaryMode(BoundaryMode rangeLowerBoundaryMode) {
+ public void setRangeLowerBoundaryMode(@NonNull BoundaryMode rangeLowerBoundaryMode) {
this.rangeLowerBoundaryMode = rangeLowerBoundaryMode;
}
- public void setMinX(Number minX) {
- this.minX = minX;
- }
-
- public void setMaxX(Number maxX) {
- this.maxX = maxX;
- }
-
- public void setMinY(Number minY) {
- this.minY = minY;
- }
-
- public void setMaxY(Number maxY) {
- this.maxY = maxY;
+ @Override
+ public String toString() {
+ return "XYConstraints{" + "domainFramingModel=" + domainFramingModel +
+ ", rangeFramingModel=" + rangeFramingModel +
+ ", domainUpperBoundaryMode=" + domainUpperBoundaryMode +
+ ", domainLowerBoundaryMode=" + domainLowerBoundaryMode +
+ ", rangeUpperBoundaryMode=" + rangeUpperBoundaryMode +
+ ", rangeLowerBoundaryMode=" + rangeLowerBoundaryMode +
+ ", minX=" + minX +
+ ", maxX=" + maxX +
+ ", minY=" + minY +
+ ", maxY=" + maxY +
+ '}';
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java
index 63a74f5b..8fa0571f 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java
@@ -16,23 +16,38 @@
package com.androidplot.xy;
-import android.content.res.*;
-import android.graphics.*;
-
-import com.androidplot.*;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.androidplot.R;
import com.androidplot.Region;
-import com.androidplot.exception.PlotRenderException;
-import com.androidplot.ui.*;
+import com.androidplot.ui.Insets;
+import com.androidplot.ui.LayoutManager;
+import com.androidplot.ui.RenderStack;
+import com.androidplot.ui.Size;
import com.androidplot.ui.widget.Widget;
-import com.androidplot.util.*;
+import com.androidplot.util.AttrUtils;
+import com.androidplot.util.FontUtils;
+import com.androidplot.util.PixelUtils;
+import com.androidplot.util.RectFUtils;
import java.text.DecimalFormat;
import java.text.Format;
-import java.util.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Map;
/**
- * Displays graphical data (lines, points, etc.) annotated with domain and range tick markers.
- * The inner area of the graph upon which grid lines and points are rendered is called the "grid" area.
+ * Displays graphical data (lines, points, etc.) annotated with domain and range tick markers. The
+ * inner area of the graph upon which grid lines and points are rendered is called the "grid" area.
*/
public class XYGraphWidget extends Widget {
@@ -47,8 +62,6 @@ public class XYGraphWidget extends Widget {
private static final float DEFAULT_LINE_LABEL_TEXT_SIZE_PX = PixelUtils.spToPix(15);
- private static final int MARKER_LABEL_SPACING = TWO;
-
/**
* Line interval per range label
*/
@@ -112,8 +125,8 @@ public class XYGraphWidget extends Widget {
private Paint domainOriginLinePaint;
private Paint rangeOriginLinePaint;
- private float domainCursorPosition;
- private float rangeCursorPosition;
+ private Float domainCursorPosition;
+ private Float rangeCursorPosition;
private boolean drawMarkersEnabled = true;
private boolean drawGridOnTop;
@@ -121,21 +134,26 @@ public class XYGraphWidget extends Widget {
/**
* Set of edges for which line labels should be displayed
*/
- private Set lineLabelEdges = new HashSet<>();
+ private EnumSet lineLabelEdges = EnumSet.noneOf(Edge.class);
private RenderStack extends XYSeries, ? extends XYSeriesFormatter> renderStack;
private CursorLabelFormatter cursorLabelFormatter;
- private HashMap lineLabelStyles = getDefaultLineLabelStyles();
- private HashMap lineLabelRenderers = getDefaultLineLabelRenderers();
+ private Map lineLabelStyles = getDefaultLineLabelStyles();
+ private Map lineLabelRenderers = getDefaultLineLabelRenderers();
public static class LineLabelRenderer {
- public void drawLabel(Canvas canvas, LineLabelStyle style, Number val, float x, float y, boolean isOrigin) {
+ public void drawLabel(Canvas canvas,
+ LineLabelStyle style,
+ Number val,
+ float x,
+ float y,
+ boolean isOrigin) {
final int canvasState = canvas.save();
try {
- final String txt = style.format.format(val.doubleValue());
+ final String txt = style.format.format(val);
canvas.rotate(style.getRotation(), x, y);
drawLabel(canvas, txt, style.getPaint(), x, y, isOrigin);
} finally {
@@ -143,7 +161,12 @@ public void drawLabel(Canvas canvas, LineLabelStyle style, Number val, float x,
}
}
- protected void drawLabel(Canvas canvas, String text, Paint paint, float x, float y, boolean isOrigin) {
+ protected void drawLabel(Canvas canvas,
+ String text,
+ Paint paint,
+ float x,
+ float y,
+ boolean isOrigin) {
canvas.drawText(text, x, y, paint);
}
}
@@ -159,6 +182,7 @@ public static class LineLabelStyle {
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(DEFAULT_LINE_LABEL_TEXT_SIZE_PX);
}
+
public Format getFormat() {
return format;
}
@@ -187,17 +211,16 @@ public void setPaint(Paint paint) {
public interface CursorLabelFormatter {
/**
- *
* @return The Paint to be used to draw the cursor text label.
*/
Paint getTextPaint();
/**
- *
- * @return Null if no background should be drawn,
- * the Paint used to draw the background otherwise.
+ * @return Null if no background should be drawn, the Paint used to draw the background
+ * otherwise.
*/
Paint getBackgroundPaint();
+
String getLabelText(Number x, Number y);
}
@@ -256,18 +279,21 @@ public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) {
/**
* Apply xml attrs
+ *
* @param attrs
*/
public void processAttrs(TypedArray attrs) {
setDrawGridOnTop(attrs.getBoolean(R.styleable.xy_XYPlot_drawGridOnTop, isDrawGridOnTop()));
int tlp = attrs.getInt(R.styleable.xy_XYPlot_lineLabels, 0);
- if(tlp != 0) {
+ if (tlp != 0) {
setLineLabelEdges(tlp);
}
- setGridClippingEnabled(attrs.getBoolean(R.styleable.xy_XYPlot_gridClippingEnabled,
- isGridClippingEnabled()));
+ setGridClippingEnabled(attrs.getBoolean(
+ R.styleable.xy_XYPlot_gridClippingEnabled,
+ isGridClippingEnabled()
+ ));
final LineLabelStyle lineLabelStyleTop = getLineLabelStyle(Edge.TOP);
final LineLabelStyle lineLabelStyleBottom = getLineLabelStyle(Edge.BOTTOM);
@@ -276,19 +302,23 @@ public void processAttrs(TypedArray attrs) {
lineLabelStyleTop.setRotation(attrs.getFloat(
R.styleable.xy_XYPlot_lineLabelRotationTop,
- lineLabelStyleTop.getRotation()));
+ lineLabelStyleTop.getRotation()
+ ));
lineLabelStyleBottom.setRotation(attrs.getFloat(
R.styleable.xy_XYPlot_lineLabelRotationBottom,
- lineLabelStyleBottom.getRotation()));
+ lineLabelStyleBottom.getRotation()
+ ));
lineLabelStyleLeft.setRotation(attrs.getFloat(
R.styleable.xy_XYPlot_lineLabelRotationLeft,
- lineLabelStyleLeft.getRotation()));
+ lineLabelStyleLeft.getRotation()
+ ));
lineLabelStyleRight.setRotation(attrs.getFloat(
R.styleable.xy_XYPlot_lineLabelRotationRight,
- lineLabelStyleRight.getRotation()));
+ lineLabelStyleRight.getRotation()
+ ));
setLineExtensionTop(attrs.getDimension(
R.styleable.xy_XYPlot_lineExtensionTop, getLineExtensionTop()));
@@ -302,34 +332,40 @@ public void processAttrs(TypedArray attrs) {
AttrUtils.configureTextPaint(attrs, lineLabelStyleTop.getPaint(),
R.styleable.xy_XYPlot_lineLabelTextColorTop,
R.styleable.xy_XYPlot_lineLabelTextSizeTop,
- R.styleable.xy_XYPlot_lineLabelAlignTop);
+ R.styleable.xy_XYPlot_lineLabelAlignTop
+ );
AttrUtils.configureTextPaint(attrs, lineLabelStyleBottom.getPaint(),
R.styleable.xy_XYPlot_lineLabelTextColorBottom,
R.styleable.xy_XYPlot_lineLabelTextSizeBottom,
- R.styleable.xy_XYPlot_lineLabelAlignBottom);
+ R.styleable.xy_XYPlot_lineLabelAlignBottom
+ );
AttrUtils.configureTextPaint(attrs, lineLabelStyleLeft.getPaint(),
R.styleable.xy_XYPlot_lineLabelTextColorLeft,
R.styleable.xy_XYPlot_lineLabelTextSizeLeft,
- R.styleable.xy_XYPlot_lineLabelAlignLeft);
+ R.styleable.xy_XYPlot_lineLabelAlignLeft
+ );
AttrUtils.configureTextPaint(attrs, lineLabelStyleRight.getPaint(),
R.styleable.xy_XYPlot_lineLabelTextColorRight,
R.styleable.xy_XYPlot_lineLabelTextSizeRight,
- R.styleable.xy_XYPlot_lineLabelAlignRight);
+ R.styleable.xy_XYPlot_lineLabelAlignRight
+ );
AttrUtils.configureInsets(attrs, getGridInsets(),
R.styleable.xy_XYPlot_gridInsetTop,
R.styleable.xy_XYPlot_gridInsetBottom,
R.styleable.xy_XYPlot_gridInsetLeft,
- R.styleable.xy_XYPlot_gridInsetRight);
+ R.styleable.xy_XYPlot_gridInsetRight
+ );
AttrUtils.configureInsets(attrs, getLineLabelInsets(),
R.styleable.xy_XYPlot_lineLabelInsetTop,
R.styleable.xy_XYPlot_lineLabelInsetBottom,
R.styleable.xy_XYPlot_lineLabelInsetLeft,
- R.styleable.xy_XYPlot_lineLabelInsetRight);
+ R.styleable.xy_XYPlot_lineLabelInsetRight
+ );
// graph size & position
AttrUtils.configureWidget(attrs, this,
@@ -337,7 +373,8 @@ public void processAttrs(TypedArray attrs) {
R.styleable.xy_XYPlot_graphWidthMode, R.styleable.xy_XYPlot_graphWidth,
R.styleable.xy_XYPlot_graphHorizontalPositioning, R.styleable.xy_XYPlot_graphHorizontalPosition,
R.styleable.xy_XYPlot_graphVerticalPositioning, R.styleable.xy_XYPlot_graphVerticalPosition,
- R.styleable.xy_XYPlot_graphAnchor, R.styleable.xy_XYPlot_graphVisible);
+ R.styleable.xy_XYPlot_graphAnchor, R.styleable.xy_XYPlot_graphVisible
+ );
// domainLabel size & position
AttrUtils.configureWidget(attrs, this,
@@ -345,7 +382,8 @@ public void processAttrs(TypedArray attrs) {
R.styleable.xy_XYPlot_domainTitleWidthMode, R.styleable.xy_XYPlot_domainTitleWidth,
R.styleable.xy_XYPlot_domainTitleHorizontalPositioning, R.styleable.xy_XYPlot_domainTitleHorizontalPosition,
R.styleable.xy_XYPlot_domainTitleVerticalPositioning, R.styleable.xy_XYPlot_domainTitleVerticalPosition,
- R.styleable.xy_XYPlot_domainTitleAnchor, R.styleable.xy_XYPlot_domainTitleVisible);
+ R.styleable.xy_XYPlot_domainTitleAnchor, R.styleable.xy_XYPlot_domainTitleVisible
+ );
// rangeLabel size & position
AttrUtils.configureWidget(attrs, this,
@@ -353,81 +391,100 @@ public void processAttrs(TypedArray attrs) {
R.styleable.xy_XYPlot_rangeTitleWidthMode, R.styleable.xy_XYPlot_rangeTitleWidth,
R.styleable.xy_XYPlot_rangeTitleHorizontalPositioning, R.styleable.xy_XYPlot_rangeTitleHorizontalPosition,
R.styleable.xy_XYPlot_rangeTitleVerticalPositioning, R.styleable.xy_XYPlot_rangeTitleVerticalPosition,
- R.styleable.xy_XYPlot_rangeTitleAnchor, R.styleable.xy_XYPlot_rangeTitleVisible);
+ R.styleable.xy_XYPlot_rangeTitleAnchor, R.styleable.xy_XYPlot_rangeTitleVisible
+ );
+
+ // rotation
+ AttrUtils.configureWidgetRotation(attrs, this, R.styleable.xy_XYPlot_graphRotation);
- // graphWidget
+ // padding & margin
AttrUtils.configureBoxModelable(attrs, this,
R.styleable.xy_XYPlot_graphMarginTop, R.styleable.xy_XYPlot_graphMarginBottom,
R.styleable.xy_XYPlot_graphMarginLeft, R.styleable.xy_XYPlot_graphMarginRight,
R.styleable.xy_XYPlot_graphPaddingTop, R.styleable.xy_XYPlot_graphPaddingBottom,
- R.styleable.xy_XYPlot_graphPaddingLeft, R.styleable.xy_XYPlot_graphPaddingRight);
+ R.styleable.xy_XYPlot_graphPaddingLeft, R.styleable.xy_XYPlot_graphPaddingRight
+ );
// domainOriginLinePaint
AttrUtils.configureLinePaint(attrs, getDomainOriginLinePaint(),
R.styleable.xy_XYPlot_domainOriginLineColor,
- R.styleable.xy_XYPlot_domainOriginLineThickness);
+ R.styleable.xy_XYPlot_domainOriginLineThickness
+ );
// rangeOriginLinePaint
AttrUtils.configureLinePaint(attrs, getRangeOriginLinePaint(),
R.styleable.xy_XYPlot_rangeOriginLineColor,
- R.styleable.xy_XYPlot_rangeOriginLineThickness);
+ R.styleable.xy_XYPlot_rangeOriginLineThickness
+ );
AttrUtils.configureLinePaint(attrs, getDomainGridLinePaint(),
R.styleable.xy_XYPlot_domainLineColor,
- R.styleable.xy_XYPlot_domainLineThickness);
+ R.styleable.xy_XYPlot_domainLineThickness
+ );
AttrUtils.configureLinePaint(attrs, getRangeGridLinePaint(),
R.styleable.xy_XYPlot_rangeLineColor,
- R.styleable.xy_XYPlot_rangeLineThickness);
+ R.styleable.xy_XYPlot_rangeLineThickness
+ );
AttrUtils.setColor(attrs, getBackgroundPaint(),
- R.styleable.xy_XYPlot_graphBackgroundColor);
+ R.styleable.xy_XYPlot_graphBackgroundColor
+ );
AttrUtils.setColor(attrs, getGridBackgroundPaint(),
- R.styleable.xy_XYPlot_gridBackgroundColor);
+ R.styleable.xy_XYPlot_gridBackgroundColor
+ );
}
/**
- * Convenience method. Wraps getYVal(float)
+ * Convenience method. Wraps screenToSeriesY(float)
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
* @param point
* @return
*/
- public Number getYVal(PointF point) {
- return getYVal(point.y);
+ protected XYCoords screenToSeries(PointF point) {
+ if (!plot.getBounds().isFullyDefined()) {
+ return null;
+ }
+ return new RectRegion(gridRect)
+ .transform(point.x, point.y, plot.getBounds(), false, true);
}
/**
- * Converts a y pixel to a y value.
+ * Convenience method. Wraps screenToSeriesX(float)
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
- * @param yPix
+ * @param point
* @return
*/
- public Number getYVal(float yPix) {
- if (!plot.getBounds().getyRegion().isDefined()) {
- return null;
- }
- return new Region(gridRect.top, gridRect.bottom)
- .transform(yPix, plot.getBounds().getyRegion(), true);
+ protected Number screenToSeriesX(PointF point) {
+ return screenToSeriesX(point.x);
}
/**
- * Convenience method. Wraps getXVal(float)
+ * Convenience method. Wraps screenToSeriesY(float)
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
* @param point
* @return
*/
- public Number getXVal(PointF point) {
- return getXVal(point.x);
+ protected Number screenToSeriesY(PointF point) {
+ return screenToSeriesY(point.y);
}
/**
* Converts an x pixel into an x value.
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
* @param xPix
* @return
*/
- public Number getXVal(float xPix) {
+ protected Number screenToSeriesX(float xPix) {
if (!plot.getBounds().xRegion.isDefined()) {
return null;
}
@@ -435,17 +492,54 @@ public Number getXVal(float xPix) {
.transform(xPix, plot.getBounds().getxRegion());
}
- @Override
- protected void doOnDraw(Canvas canvas, RectF widgetRect)
- throws PlotRenderException {
+ /**
+ * Converts a y pixel to a y value.
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
+ *
+ * @param yPix
+ * @return
+ */
+ protected Number screenToSeriesY(float yPix) {
+ if (!plot.getBounds().getyRegion().isDefined()) {
+ return null;
+ }
+ return new Region(gridRect.top, gridRect.bottom)
+ .transform(yPix, plot.getBounds().getyRegion(), true);
+ }
- if(gridRect == null) {
- gridRect = RectFUtils.applyInsets(widgetRect, gridInsets);
+ protected PointF seriesToScreen(XYCoords xy) {
+ if (!plot.getBounds().isFullyDefined()) {
+ return null;
}
+ return plot.getBounds().transform(xy, gridRect, false, true);
+ }
+
+ protected float seriesToScreenX(Number x) {
+ return (float) plot.getBounds().getxRegion().
+ transform(x.doubleValue(), gridRect.left, gridRect.right, false);
+ }
+
+ protected float seriesToScreenY(Number y) {
+ return (float) plot.getBounds().getyRegion().
+ transform(y.doubleValue(), gridRect.bottom, gridRect.top, true);
+ }
+
+ @Override
+ protected void onResize(@Nullable RectF oldRect, @NonNull RectF newRect) {
+ recalculateSizes(newRect);
+ }
- if(labelRect == null) {
- labelRect = RectFUtils.applyInsets(widgetRect, lineLabelInsets);
+ protected void recalculateSizes(@Nullable RectF rect) {
+ if(rect == null) {
+ rect = getWidgetDimensions().paddedRect;
}
+ gridRect = RectFUtils.applyInsets(rect, gridInsets);
+ labelRect = RectFUtils.applyInsets(rect, lineLabelInsets);
+ }
+
+ @Override
+ protected void doOnDraw(Canvas canvas, RectF widgetRect) {
// don't draw if we have no space to draw into
if (gridRect.height() > ZERO && gridRect.width() > ZERO) {
@@ -454,7 +548,7 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect)
&& bounds.getMaxX() != null
&& bounds.getMinY() != null
&& bounds.getMaxY() != null) {
- if(drawGridOnTop) {
+ if (drawGridOnTop) {
drawData(canvas);
drawGrid(canvas);
} else {
@@ -470,101 +564,104 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect)
}
protected void drawDomainLine(Canvas canvas, float xPix, Number xVal,
- Paint linePaint, boolean isOrigin) {
+ Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) {
// lines
if (linePaint != null) {
- canvas.drawLine(xPix, gridRect.top - lineExtensionTop,
- xPix, gridRect.bottom + lineExtensionBottom, linePaint);
+ canvas.drawLine(xPix, gridRect.top - lineExtensionTop,
+ xPix, gridRect.bottom + lineExtensionBottom, linePaint
+ );
}
// labels
- drawLineLabel(canvas, Edge.TOP, xVal, xPix, labelRect.top, isOrigin);
- drawLineLabel(canvas, Edge.BOTTOM, xVal, xPix, labelRect.bottom, isOrigin);
+ if(shouldDrawLabel) {
+ if (isLineLabelEnabled(Edge.TOP)) {
+ drawLineLabel(canvas, Edge.TOP, xVal, xPix, labelRect.top, isOrigin);
+ }
+
+ if (isLineLabelEnabled(Edge.BOTTOM)) {
+ drawLineLabel(canvas, Edge.BOTTOM, xVal, xPix, labelRect.bottom, isOrigin);
+ }
+ }
}
protected void drawRangeLine(Canvas canvas, float yPix, Number yVal,
- Paint linePaint, boolean isOrigin) {
+ Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) {
// lines
if (linePaint != null) {
canvas.drawLine(gridRect.left - lineExtensionLeft, yPix,
- gridRect.right + lineExtensionRight, yPix, linePaint);
+ gridRect.right + lineExtensionRight, yPix, linePaint
+ );
}
- // labels
- drawLineLabel(canvas, Edge.LEFT, yVal, labelRect.left, yPix, isOrigin);
- drawLineLabel(canvas, Edge.RIGHT, yVal, labelRect.right, yPix, isOrigin);
+ if(shouldDrawLabel) {
+ // labels
+ if (isLineLabelEnabled(Edge.LEFT)) {
+ drawLineLabel(canvas, Edge.LEFT, yVal, labelRect.left, yPix, isOrigin);
+ }
+ if (isLineLabelEnabled(Edge.RIGHT)) {
+ drawLineLabel(canvas, Edge.RIGHT, yVal, labelRect.right, yPix, isOrigin);
+ }
+ }
}
- protected void drawLineLabel(Canvas canvas, Edge edge, Number val, float x, float y, boolean isOrigin) {
- if(isLineLabelEnabled(edge)) {
- getLineLabelRenderer(edge).drawLabel(canvas, getLineLabelStyle(edge), val, x, y, isOrigin);
- }
+ protected void drawLineLabel(Canvas canvas,
+ Edge edge,
+ Number val,
+ float x,
+ float y,
+ boolean isOrigin) {
+ getLineLabelRenderer(edge).drawLabel(canvas, getLineLabelStyle(edge), val, x, y, isOrigin);
}
/**
- * Draws the drid and domain/range labels for the plot.
+ * Draws the grid and domain/range labels for the plot.
*
* @param canvas
*/
protected void drawGrid(Canvas canvas) {
- if(!drawGridOnTop) {
+ if (!drawGridOnTop) {
drawGridBackground(canvas);
}
Number domainOrigin = plot.getDomainOrigin();
- double domainOriginPix;
+ final double domainOriginPix;
if (domainOrigin != null) {
domainOriginPix = plot.getBounds().getxRegion().transform(
plot.getDomainOrigin().doubleValue(), gridRect.left, gridRect.right, false);
} else {
// if no domain origin is set, use the leftmost value visible on the grid:
domainOriginPix = gridRect.left;
- domainOrigin=plot.getBounds().getMinX();
+ domainOrigin = plot.getBounds().getMinX();
}
Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect);
- // draw domain origin:
- if (domainOriginPix >= gridRect.left
- && domainOriginPix <= gridRect.right) {
- drawDomainLine(canvas, (float) domainOriginPix,
- domainOrigin, domainOriginLinePaint, true);
- }
-
- // draw lines LEFT of origin:
- double xPix = domainOriginPix - domainStep.getStepPix();
- for (int i = ONE; xPix >= gridRect.left - FUDGE; xPix = domainOriginPix
- - (i * domainStep.getStepPix())) {
- double xVal = domainOrigin.doubleValue() - i
- * domainStep.getStepVal();
-
- if (xPix <= gridRect.right) {
- final boolean isDomainTick = i% getLinesPerDomainLabel() == ZERO;
- final Paint lp = isDomainTick ? domainGridLinePaint : domainSubGridLinePaint;
- drawDomainLine(canvas, (float) xPix, xVal, lp, false);
- }
- i++;
- }
-
- // draw lines RIGHT of origin:
- xPix = domainOriginPix + domainStep.getStepPix();
- for (int i = ONE; xPix <= gridRect.right + FUDGE; xPix = domainOriginPix
- + (i * domainStep.getStepPix())) {
- double xVal = domainOrigin.doubleValue() + i
- * domainStep.getStepVal();
-
- if (xPix >= gridRect.left) {
- final boolean isDomainTick = i% getLinesPerDomainLabel() == ZERO;
- final Paint lp = isDomainTick ? domainGridLinePaint : domainSubGridLinePaint;
- drawDomainLine(canvas, (float) xPix, xVal, lp, false);
+ // Draw Domain Lines:
+
+ final double domainStepPix = domainStep.getStepPix();
+ final double iMin = (gridRect.left - domainOriginPix - FUDGE) / domainStepPix;
+ final double iMax = (gridRect.right - domainOriginPix + FUDGE) / domainStepPix;
+
+ for (int i = (int) Math.ceil(iMin); i <= iMax; i++) {
+ double xVal = domainOrigin.doubleValue() + i * domainStep.getStepVal();
+ double xPix = domainOriginPix + i * domainStepPix;
+ boolean isMajorTick = i % getLinesPerDomainLabel() == ZERO;
+ boolean isOrigin = i == 0;
+ Paint linePaint;
+ if (isOrigin) {
+ linePaint = domainOriginLinePaint;
+ } else if (isMajorTick) {
+ linePaint = domainGridLinePaint;
+ } else {
+ linePaint = domainSubGridLinePaint;
}
- i++;
+ drawDomainLine(canvas, (float) xPix, xVal, linePaint, isOrigin, isMajorTick);
}
Number rangeOrigin = plot.getRangeOrigin();
- double rangeOriginPix;
+ final double rangeOriginPix;
if (rangeOrigin != null) {
rangeOriginPix = plot.getBounds().getyRegion().transform(
rangeOrigin.doubleValue(), gridRect.top, gridRect.bottom, true);
@@ -576,106 +673,42 @@ protected void drawGrid(Canvas canvas) {
Step rangeStep = XYStepCalculator.getStep(plot, Axis.RANGE, gridRect);
- // draw range origin:
- if (rangeOriginPix >= gridRect.top && rangeOriginPix <= gridRect.bottom) {
- drawRangeLine(canvas, (float) rangeOriginPix,
- rangeOrigin, rangeOriginLinePaint, true);
- }
+ // Draw Range Lines:
final double rangeStepPix = rangeStep.getStepPix();
-
- // draw lines ABOVE origin:
- double yPix = rangeOriginPix - rangeStep.getStepPix();
- for (int i = ONE; yPix >= gridRect.top - FUDGE; yPix = rangeOriginPix - (i * rangeStepPix)) {
- double yVal = rangeOrigin.doubleValue() + i
- * rangeStep.getStepVal();
-
- if (yPix <= gridRect.bottom) {
- final boolean isRangeTick = i% getLinesPerRangeLabel() == ZERO;
- final Paint lp = isRangeTick ? rangeGridLinePaint : rangeSubGridLinePaint;
- drawRangeLine(canvas, (float)yPix, yVal, lp, false);
- }
- i++;
- }
-
- // draw lines BENEATH origin:
- yPix = rangeOriginPix + rangeStep.getStepPix();
- for (int i = ONE; yPix <= gridRect.bottom + FUDGE; yPix = rangeOriginPix + (i * rangeStepPix)) {
- double yVal = rangeOrigin.doubleValue() - i
- * rangeStep.getStepVal();
- if (yPix >= gridRect.top) {
- final boolean isRangeTick = i% getLinesPerRangeLabel() == ZERO;
- final Paint lp = isRangeTick ? rangeGridLinePaint : rangeSubGridLinePaint;
- drawRangeLine(canvas, (float)yPix, yVal, lp, false);
+ final double kMin = (gridRect.top - rangeOriginPix - FUDGE) / rangeStepPix;
+ final double kMax = (gridRect.bottom - rangeOriginPix + FUDGE) / rangeStepPix;
+
+ for (int k = (int) Math.ceil(kMin); k <= kMax; k++) {
+ // Android vertical coordinates (zero at the top of the screen) are the opposite
+ // direction of default range values (lowest on bottom of screen) so we subtract when
+ // calculating yVal
+ double yVal = rangeOrigin.doubleValue() - k * rangeStep.getStepVal();
+ double yPix = rangeOriginPix + k * rangeStepPix;
+ boolean isMajorTick = k % getLinesPerRangeLabel() == ZERO;
+ boolean isOrigin = k == 0;
+ Paint linePaint;
+ if (isOrigin) {
+ linePaint = rangeOriginLinePaint;
+ } else if (isMajorTick) {
+ linePaint = rangeGridLinePaint;
+ } else {
+ linePaint = rangeSubGridLinePaint;
}
- i++;
+ drawRangeLine(canvas, (float) yPix, yVal, linePaint, isOrigin, isMajorTick);
}
}
- /**
- * Renders the text associated with user defined markers
- *
- * @param canvas
- * @param text
- * @param marker
- * @param x
- * @param y
- */
- private void drawMarkerText(Canvas canvas, String text, ValueMarker marker,
- float x, float y) {
- x += MARKER_LABEL_SPACING;
- y -= MARKER_LABEL_SPACING;
- RectF textRect = new RectF(FontUtils.getStringDimensions(text,
- marker.getTextPaint()));
- textRect.offsetTo(x, y - textRect.height());
-
- if (textRect.right > gridRect.right) {
- textRect.offset(-(textRect.right - gridRect.right), ZERO);
- }
-
- if (textRect.top < gridRect.top) {
- textRect.offset(0, gridRect.top - textRect.top);
- }
-
- canvas.drawText(text, textRect.left, textRect.bottom,
- marker.getTextPaint());
- }
-
protected void drawMarkers(Canvas canvas) {
- if(plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) {
+ if (plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) {
for (YValueMarker marker : plot.getYValueMarkers()) {
- if (marker.getValue() != null) {
- float yPix = (float) plot.getBounds().yRegion
- .transform(marker.getValue()
- .doubleValue(), gridRect.top, gridRect.bottom, true);
- canvas.drawLine(gridRect.left, yPix,
- gridRect.right, yPix, marker.getLinePaint());
-
- float xPix = marker.getTextPosition().getPixelValue(
- gridRect.width());
- xPix += gridRect.left;
-
- if (marker.getText() != null) {
- drawMarkerText(canvas, marker.getText(), marker, xPix, yPix);
- }
- }
+ marker.draw(canvas, plot, gridRect);
}
}
- if(plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) {
+ if (plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) {
for (XValueMarker marker : plot.getXValueMarkers()) {
- if (marker.getValue() != null) {
- float xPix = (float) plot.getBounds().xRegion
- .transform(marker.getValue()
- .doubleValue(), gridRect.left, gridRect.right, false);
- canvas.drawLine(xPix, gridRect.top, xPix, gridRect.bottom,
- marker.getLinePaint());
- float yPix = marker.getTextPosition().getPixelValue(gridRect.height());
- yPix += gridRect.top;
- if (marker.getText() != null) {
- drawMarkerText(canvas, marker.getText(), marker, xPix, yPix);
- }
- }
+ marker.draw(canvas, plot, gridRect);
}
}
}
@@ -684,25 +717,29 @@ protected void drawCursors(Canvas canvas) {
boolean hasDomainCursor = false;
// draw the domain cursor:
if (domainCursorPaint != null
+ && domainCursorPosition != null
&& domainCursorPosition <= gridRect.right
&& domainCursorPosition >= gridRect.left) {
hasDomainCursor = true;
canvas.drawLine(domainCursorPosition, gridRect.top,
domainCursorPosition, gridRect.bottom,
- domainCursorPaint);
+ domainCursorPaint
+ );
}
boolean hasRangeCursor = false;
// draw the range cursor:
if (rangeCursorPaint != null
+ && rangeCursorPosition != null
&& rangeCursorPosition >= gridRect.top
&& rangeCursorPosition <= gridRect.bottom) {
hasRangeCursor = true;
canvas.drawLine(gridRect.left, rangeCursorPosition,
- gridRect.right, rangeCursorPosition, rangeCursorPaint);
+ gridRect.right, rangeCursorPosition, rangeCursorPaint
+ );
}
- if(getCursorLabelFormatter() != null && hasRangeCursor && hasDomainCursor) {
+ if (getCursorLabelFormatter() != null && hasRangeCursor && hasDomainCursor) {
drawCursorLabel(canvas);
}
}
@@ -720,8 +757,10 @@ protected void drawCursorLabel(Canvas canvas) {
// if we are too close to the right edge of the plot, we will move
// the label to the left side of our cursor:
if (cursorRect.right >= gridRect.right) {
- cursorRect.offsetTo(domainCursorPosition - cursorRect.width(),
- cursorRect.top);
+ cursorRect.offsetTo(
+ domainCursorPosition - cursorRect.width(),
+ cursorRect.top
+ );
}
// same thing for the top edge of the plot:
@@ -735,11 +774,12 @@ protected void drawCursorLabel(Canvas canvas) {
}
canvas.drawText(label, cursorRect.left, cursorRect.bottom,
- getCursorLabelFormatter().getTextPaint());
+ getCursorLabelFormatter().getTextPaint()
+ );
}
protected void drawGridBackground(Canvas canvas) {
- if(gridBackgroundPaint != null) {
+ if (gridBackgroundPaint != null) {
canvas.drawRect(gridRect, gridBackgroundPaint);
}
}
@@ -748,22 +788,21 @@ protected void drawGridBackground(Canvas canvas) {
* Draws lines and points for each element in the series.
*
* @param canvas
- * @throws PlotRenderException
*/
- protected void drawData(Canvas canvas) throws PlotRenderException {
+ protected void drawData(Canvas canvas) {
if (drawGridOnTop) {
drawGridBackground(canvas);
}
try {
- if(isGridClippingEnabled) {
- canvas.save(Canvas.ALL_SAVE_FLAG);
+ if (isGridClippingEnabled) {
+ canvas.save();
canvas.clipRect(gridRect, android.graphics.Region.Op.INTERSECT);
}
renderStack.sync();
- for(RenderStack.StackElement thisElement : renderStack.getElements()) {
- if(thisElement.isEnabled()) {
+ for (RenderStack.StackElement thisElement : renderStack.getElements()) {
+ if (thisElement.isEnabled()) {
Class extends XYSeriesRenderer> rendererClass =
thisElement.get().getFormatter().getRendererClass();
plot.getRenderer(rendererClass).render(
@@ -772,7 +811,7 @@ protected void drawData(Canvas canvas) throws PlotRenderException {
}
} finally {
- if(isGridClippingEnabled) {
+ if (isGridClippingEnabled) {
canvas.restore();
}
}
@@ -799,6 +838,7 @@ public Paint getDomainGridLinePaint() {
/**
* Set the paint used to draw the domain grid line.
+ *
* @param gridLinePaint
*/
public void setDomainGridLinePaint(Paint gridLinePaint) {
@@ -821,6 +861,7 @@ public Paint getDomainSubGridLinePaint() {
/**
* Set the paint used to draw the domain grid line.
+ *
* @param gridLinePaint
*/
public void setDomainSubGridLinePaint(Paint gridLinePaint) {
@@ -829,6 +870,7 @@ public void setDomainSubGridLinePaint(Paint gridLinePaint) {
/**
* Set the Paint used to draw the range grid line.
+ *
* @param gridLinePaint
*/
public void setRangeGridLinePaint(Paint gridLinePaint) {
@@ -844,6 +886,7 @@ public Paint getRangeSubGridLinePaint() {
/**
* Set the Paint used to draw the range grid line.
+ *
* @param gridLinePaint
*/
public void setRangeSubGridLinePaint(Paint gridLinePaint) {
@@ -882,36 +925,57 @@ public void setRangeOriginLinePaint(Paint rangeOriginLinePaint) {
this.rangeOriginLinePaint = rangeOriginLinePaint;
}
- public void setCursorPosition(float x, float y) {
+ /**
+ * Set domain and range cursor position using screen coordinates
+ *
+ * @param x
+ * @param y
+ */
+ public void setCursorPosition(Float x, Float y) {
setDomainCursorPosition(x);
setRangeCursorPosition(y);
}
+ /**
+ * Set domain and range cursor position using screen coordinates
+ *
+ * @param point
+ */
public void setCursorPosition(PointF point) {
setCursorPosition(point.x, point.y);
}
- public float getDomainCursorPosition() {
+ public Float getDomainCursorPosition() {
return domainCursorPosition;
}
public Number getDomainCursorVal() {
- return getXVal(getDomainCursorPosition());
+ return screenToSeriesX(getDomainCursorPosition());
}
- public void setDomainCursorPosition(float domainCursorPosition) {
+ /**
+ * Set domain cursor position using screen coordinates
+ *
+ * @param domainCursorPosition
+ */
+ public void setDomainCursorPosition(Float domainCursorPosition) {
this.domainCursorPosition = domainCursorPosition;
}
- public float getRangeCursorPosition() {
+ public Float getRangeCursorPosition() {
return rangeCursorPosition;
}
public Number getRangeCursorVal() {
- return getYVal(getRangeCursorPosition());
+ return screenToSeriesY(getRangeCursorPosition());
}
- public void setRangeCursorPosition(float rangeCursorPosition) {
+ /**
+ * Set range cursor position using screen coordinates
+ *
+ * @param rangeCursorPosition
+ */
+ public void setRangeCursorPosition(Float rangeCursorPosition) {
this.rangeCursorPosition = rangeCursorPosition;
}
@@ -936,9 +1000,8 @@ public Paint getDomainCursorPaint() {
}
/**
- *
- * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line.
- * Set to null (default) to disable.
+ * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line. Set to null
+ * (default) to disable.
*/
public void setDomainCursorPaint(Paint domainCursorPaint) {
this.domainCursorPaint = domainCursorPaint;
@@ -949,9 +1012,8 @@ public Paint getRangeCursorPaint() {
}
/**
- *
- * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line.
- * Set to null (default) to disable.
+ * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line. Set to null
+ * (default) to disable.
*/
public void setRangeCursorPaint(Paint rangeCursorPaint) {
this.rangeCursorPaint = rangeCursorPaint;
@@ -989,8 +1051,8 @@ public void setLineExtensionRight(float lineExtensionRight) {
this.lineExtensionRight = lineExtensionRight;
}
- protected HashMap getDefaultLineLabelStyles() {
- HashMap defaults = new HashMap<>();
+ protected Map getDefaultLineLabelStyles() {
+ EnumMap defaults = new EnumMap<>(Edge.class);
defaults.put(Edge.TOP, new LineLabelStyle());
defaults.put(Edge.BOTTOM, new LineLabelStyle());
defaults.put(Edge.LEFT, new LineLabelStyle());
@@ -998,8 +1060,8 @@ protected HashMap getDefaultLineLabelStyles() {
return defaults;
}
- protected HashMap getDefaultLineLabelRenderers() {
- HashMap defaults = new HashMap<>();
+ protected Map getDefaultLineLabelRenderers() {
+ EnumMap defaults = new EnumMap<>(Edge.class);
defaults.put(Edge.TOP, new LineLabelRenderer());
defaults.put(Edge.BOTTOM, new LineLabelRenderer());
defaults.put(Edge.LEFT, new LineLabelRenderer());
@@ -1041,6 +1103,7 @@ public Insets getGridInsets() {
public void setGridInsets(Insets gridInsets) {
this.gridInsets = gridInsets;
+ recalculateSizes(null);
}
/**
@@ -1052,6 +1115,7 @@ public Insets getLineLabelInsets() {
public void setLineLabelInsets(Insets lineLabelInsets) {
this.lineLabelInsets = lineLabelInsets;
+ recalculateSizes(null);
}
public RectF getGridRect() {
@@ -1083,22 +1147,20 @@ public boolean isLineLabelEnabled(Edge position) {
}
public void setLineLabelEdges(Edge... positions) {
- Set positionSet = new HashSet<>();
- if(positions != null) {
- for(Edge position : positions) {
- positionSet.add(position);
- }
+ EnumSet positionSet = EnumSet.noneOf(Edge.class);
+ if (positions != null) {
+ Collections.addAll(positionSet, positions);
}
- setLineLabelEdges(positionSet);
+ this.lineLabelEdges = positionSet;
}
- public void setLineLabelEdges(Set positions) {
- this.lineLabelEdges = positions;
+ public void setLineLabelEdges(Collection positions) {
+ this.lineLabelEdges = EnumSet.copyOf(positions);
}
protected void setLineLabelEdges(int bitfield) {
- for(Edge tp : Edge.values()) {
- if((tp.value & bitfield) == tp.value) {
+ for (Edge tp : Edge.values()) {
+ if ((tp.value & bitfield) == tp.value) {
lineLabelEdges.add(tp);
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java
new file mode 100644
index 00000000..51c67159
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java
@@ -0,0 +1,28 @@
+package com.androidplot.xy;
+
+import androidx.annotation.NonNull;
+
+import com.androidplot.ui.widget.LegendItem;
+
+public class XYLegendItem implements LegendItem {
+
+ public enum Type {
+ SERIES,
+ REGION
+ }
+
+ public final Type type;
+ public final Object item;
+ private final String text;
+
+ public XYLegendItem(@NonNull Type cellType, @NonNull Object item, @NonNull String text) {
+ this.type = cellType;
+ this.item = item;
+ this.text = text;
+ }
+
+ @Override
+ public String getTitle() {
+ return this.text;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java
index e5b981c5..680fd4b2 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java
@@ -17,233 +17,80 @@
package com.androidplot.xy;
import android.graphics.*;
+import androidx.annotation.NonNull;
+
import com.androidplot.ui.LayoutManager;
import com.androidplot.ui.SeriesBundle;
import com.androidplot.ui.Size;
import com.androidplot.ui.TableModel;
-import com.androidplot.ui.widget.Widget;
-import com.androidplot.util.FontUtils;
+import com.androidplot.ui.widget.LegendWidget;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map.Entry;
/**
* Displays a legend for each series added to the owning {@link XYPlot}.
*/
-public class XYLegendWidget extends Widget {
-
- /**
- * This class is of no use outside of XYLegendWidget. It's just used to alphabetically sort
- * Region legend entries.
- */
- private static class RegionEntryComparator implements Comparator> {
- @Override
- public int compare(Map.Entry o1, Map.Entry o2) {
- return o1.getValue().compareTo(o2.getValue());
- }
- }
-
- private enum CellType {
- SERIES,
- REGION
- }
+public class XYLegendWidget extends LegendWidget {
private XYPlot plot;
- //private float iconWidth = 12;
- private Paint textPaint;
- private Paint iconBorderPaint;
- private TableModel tableModel;
- private boolean drawIconBackgroundEnabled = true;
- private boolean drawIconBorderEnabled = true;
-
- private Size iconSize;
- private static final RegionEntryComparator regionEntryComparator = new RegionEntryComparator();
- //private RectF iconRect = new RectF(0, 0, ICON_WIDTH_DEFAULT, ICON_HEIGHT_DEFAULT);
-
- {
- textPaint = new Paint();
- textPaint.setColor(Color.LTGRAY);
- textPaint.setAntiAlias(true);
-
- iconBorderPaint = new Paint();
- iconBorderPaint.setStyle(Paint.Style.STROKE);
- //regionEntryComparator = new RegionEntryComparator();
- }
public XYLegendWidget(LayoutManager layoutManager, XYPlot plot,
Size widgetSize,
TableModel tableModel,
Size iconSize) {
- super(layoutManager, widgetSize);
+ super(tableModel, layoutManager, widgetSize, iconSize);
this.plot = plot;
- setTableModel(tableModel);
- this.iconSize = iconSize;
- }
-
- public synchronized void setTableModel(TableModel tableModel) {
- this.tableModel = tableModel;
- }
- private RectF getIconRect(RectF cellRect) {
- float cellRectCenterY = cellRect.top + (cellRect.height()/2);
- RectF iconRect = iconSize.getRectF(cellRect);
-
- // center the icon rect vertically
- float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2);
- iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY);
- return iconRect;
- }
-
- private static float getRectCenterY(RectF cellRect) {
- return cellRect.top + (cellRect.height()/2);
- }
-
- private void beginDrawingCell(Canvas canvas, RectF iconRect) {
-
- Paint bgPaint = plot.getGraph().getGridBackgroundPaint();
- if(drawIconBackgroundEnabled && bgPaint != null) {
- canvas.drawRect(iconRect, bgPaint);
- }
- }
-
- private void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, String text) {
-
- Paint bgPaint = plot.getGraph().getGridBackgroundPaint();
- if(drawIconBorderEnabled && bgPaint != null) {
- iconBorderPaint.setColor(bgPaint.getColor());
- canvas.drawRect(iconRect, iconBorderPaint);
- }
-
- float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2);
-
- if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) {
- canvas.drawText(text, iconRect.left - 2, centeredTextOriginY, textPaint);
- } else {
- canvas.drawText(text, iconRect.right + 2, centeredTextOriginY, textPaint);
- }
+ // Set a default comparator that sorts by type and then alphabetically
+ setLegendItemComparator(new Comparator() {
+ @Override
+ public int compare(XYLegendItem o1, XYLegendItem o2) {
+ if(o1.type == o2.type) {
+ return o1.getTitle().compareTo(o2.getTitle());
+ } else {
+ return(o1.type.compareTo(o2.type));
+ }
+ }
+ });
}
protected void drawRegionLegendIcon(Canvas canvas, RectF rect, XYRegionFormatter formatter) {
- canvas.drawRect(rect, formatter.getPaint());
- }
-
- private void drawRegionLegendCell(Canvas canvas, XYRegionFormatter formatter, RectF cellRect, String text) {
- RectF iconRect = getIconRect(cellRect);
- beginDrawingCell(canvas, iconRect);
-
- drawRegionLegendIcon(
- canvas,
- iconRect,
- formatter
- );
- finishDrawingCell(canvas, cellRect, iconRect, text);
+ canvas.drawRect(rect, formatter.getPaint());
}
- private void drawSeriesLegendCell(Canvas canvas, XYSeriesRenderer renderer, XYSeriesFormatter formatter, RectF cellRect, String seriesTitle) {
- RectF iconRect = getIconRect(cellRect);
- beginDrawingCell(canvas, iconRect);
-
- renderer.drawSeriesLegendIcon(
- canvas,
- iconRect,
- formatter);
- finishDrawingCell(canvas, cellRect, iconRect, seriesTitle);
+ @Override
+ protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull XYLegendItem XYLegendItem) {
+ switch (XYLegendItem.type) {
+ case REGION:
+ drawRegionLegendIcon(canvas, iconRect, (XYRegionFormatter) XYLegendItem.item);
+ break;
+ case SERIES:
+ final XYSeriesFormatter formatter = (XYSeriesFormatter) XYLegendItem.item;
+ plot.getRenderer(formatter.getRendererClass()).drawSeriesLegendIcon(canvas, iconRect, formatter);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unexpected item type: " + XYLegendItem.type);
+ }
}
-// protected List> getLegendEnabledSeriesAndFormatterList() {
-// List> sfList = new ArrayList<>();
-// ListIterator> it = plot.getSeriesRegistry().listIterator();
-// while(it.hasNext()) {
-// SeriesAndFormatter thisSf = it.next();
-// if(thisSf.getFormatter().isLegendIconEnabled()) {
-// sfList.add(thisSf);
-// }
-// }
-// return sfList;
-// }
-
@Override
- protected synchronized void doOnDraw(Canvas canvas, RectF widgetRect) {
- if(plot.isEmpty()) {
- return;
+ protected List getLegendItems() {
+ final ArrayList items = new ArrayList<>();
+ for (SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) {
+ items.add(new XYLegendItem(XYLegendItem.Type.SERIES, sfPair.getFormatter(), sfPair.getSeries().getTitle()));
}
- // Keep an alphabetically sorted list of regions:
- TreeSet> sortedRegions = new TreeSet>(new RegionEntryComparator());
-
- // Calculate the number of cells needed to draw the Legend:
- int seriesCount = plot.getRegistry().size();
-
- for(XYSeriesRenderer renderer : plot.getRendererList()) {
+ for (XYSeriesRenderer renderer : plot.getRendererList()) {
Hashtable urf = renderer.getUniqueRegionFormatters();
- sortedRegions.addAll(urf.entrySet());
- }
-
- seriesCount += sortedRegions.size();
-
- // Create an iterator specially created to draw the number of cells we calculated:
- Iterator it = tableModel.getIterator(widgetRect, seriesCount);
-
- RectF cellRect;
-
- // draw each series legend item:
- for(SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) {
- //for(SeriesAndFormatter sfPair : plot.getSeriesRegistry()) {
- cellRect = it.next();
- XYSeriesFormatter format = sfPair.getFormatter();
- drawSeriesLegendCell(canvas, plot.getRenderer(sfPair.getFormatter().getRendererClass()),
- format, cellRect, sfPair.getSeries().getTitle());
- }
-
- // draw each region legend item:
- for(Map.Entry entry : sortedRegions) {
- if(!it.hasNext()) {
- break;
+ for (Entry entry : urf.entrySet()) {
+ items.add(new XYLegendItem(XYLegendItem.Type.REGION, entry.getKey(), entry.getValue()));
}
- cellRect = it.next();
- XYRegionFormatter formatter = entry.getKey();
- drawRegionLegendCell(canvas, formatter, cellRect, entry.getValue());
}
- }
-
-
- public Paint getTextPaint() {
- return textPaint;
- }
-
- public void setTextPaint(Paint textPaint) {
- this.textPaint = textPaint;
- }
-
- public boolean isDrawIconBackgroundEnabled() {
- return drawIconBackgroundEnabled;
- }
-
- public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) {
- this.drawIconBackgroundEnabled = drawIconBackgroundEnabled;
- }
-
- public boolean isDrawIconBorderEnabled() {
- return drawIconBorderEnabled;
- }
-
- public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) {
- this.drawIconBorderEnabled = drawIconBorderEnabled;
- }
-
- public TableModel getTableModel() {
- return tableModel;
- }
-
- public Size getIconSize() {
- return iconSize;
- }
- /**
- * Set the size of each legend's icon. Note that when using relative sizing,
- * the size is calculated against the countaining cell's size, not the plot's size.
- * @param iconSize
- */
- public void setIconSize(Size iconSize) {
- this.iconSize = iconSize;
+ return items;
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java
index 5563f29b..e519b5a0 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java
@@ -22,11 +22,18 @@
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
+import androidx.annotation.NonNull;
import android.util.AttributeSet;
-import com.androidplot.*;
-import com.androidplot.ui.*;
+import com.androidplot.Plot;
+import com.androidplot.R;
+import com.androidplot.ui.Anchor;
+import com.androidplot.ui.DynamicTableModel;
+import com.androidplot.ui.HorizontalPositioning;
+import com.androidplot.ui.Size;
+import com.androidplot.ui.SizeMode;
import com.androidplot.ui.TextOrientation;
+import com.androidplot.ui.VerticalPositioning;
import com.androidplot.ui.widget.TextLabelWidget;
import com.androidplot.util.AttrUtils;
import com.androidplot.util.PixelUtils;
@@ -41,9 +48,6 @@
*/
public class XYPlot extends Plot {
- private static final int DEFAULT_LEGEND_WIDGET_H_DP = 10;
- private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 7;
-
private static final int DEFAULT_GRAPH_WIDGET_H_DP = 18;
private static final int DEFAULT_GRAPH_WIDGET_W_DP = 10;
@@ -53,8 +57,11 @@ public class XYPlot extends Plot min.doubleValue()))
? value
: min);
@@ -518,7 +602,7 @@ private Number applyUserMinMax(Number value, Number min, Number max) {
*
* @param origin
*/
- public void centerOnDomainOrigin(Number origin) {
+ public void centerOnDomainOrigin(@NonNull Number origin) {
centerOnDomainOrigin(origin, null, BoundaryMode.AUTO);
}
@@ -530,9 +614,9 @@ public void centerOnDomainOrigin(Number origin) {
* @param extent
* @param mode
*/
- public void centerOnDomainOrigin(Number origin, Number extent, BoundaryMode mode) {
+ public void centerOnDomainOrigin(@NonNull Number origin, Number extent, BoundaryMode mode) {
if (origin == null) {
- throw new NullPointerException("Origin param cannot be null.");
+ throw new IllegalArgumentException("Origin param cannot be null.");
}
constraints.setDomainFramingModel(XYFramingModel.ORIGIN);
setUserDomainOrigin(origin);
@@ -549,7 +633,7 @@ public void centerOnDomainOrigin(Number origin, Number extent, BoundaryMode mode
*
* @param origin
*/
- public void centerOnRangeOrigin(Number origin) {
+ public void centerOnRangeOrigin(@NonNull Number origin) {
centerOnRangeOrigin(origin, null, BoundaryMode.AUTO);
}
@@ -562,9 +646,9 @@ public void centerOnRangeOrigin(Number origin) {
* @param mode
*/
@SuppressWarnings("SameParameterValue")
- public void centerOnRangeOrigin(Number origin, Number extent, BoundaryMode mode) {
+ public void centerOnRangeOrigin(@NonNull Number origin, Number extent, BoundaryMode mode) {
if (origin == null) {
- throw new NullPointerException("Origin param cannot be null.");
+ throw new IllegalArgumentException("Origin param cannot be null.");
}
constraints.setRangeFramingModel(XYFramingModel.ORIGIN);
setUserRangeOrigin(origin);
@@ -600,7 +684,7 @@ protected Number[] getOriginMinMax(BoundaryMode mode, Number origin, Number exte
* @param y
* @return
*/
- private double distance(double x, double y) {
+ private static double distance(double x, double y) {
if (x > y) {
return x - y;
} else {
@@ -610,15 +694,15 @@ private double distance(double x, double y) {
public void updateDomainMinMaxForOriginModel() {
double origin = userDomainOrigin.doubleValue();
- double maxXDelta = distance(bounds.getMaxX().doubleValue(), origin);
- double minXDelta = distance(bounds.getMinX().doubleValue(), origin);
- double delta = maxXDelta > minXDelta ? maxXDelta : minXDelta;
- double dlb = origin - delta;
- double dub = origin + delta;
+ double maxDelta = distance(bounds.getMaxX().doubleValue(), origin);
+ double minDelta = distance(bounds.getMinX().doubleValue(), origin);
+ double delta = maxDelta > minDelta ? maxDelta : minDelta;
+ double lowerBoundary = origin - delta;
+ double upperBoundary = origin + delta;
switch (domainOriginBoundaryMode) {
case AUTO:
- bounds.setMinX(dlb);
- bounds.setMaxX(dub);
+ bounds.setMinX(lowerBoundary);
+ bounds.setMaxX(upperBoundary);
break;
// if fixed, then the value already exists within "user" vals.
@@ -626,28 +710,28 @@ public void updateDomainMinMaxForOriginModel() {
break;
case GROW: {
- if (prevMinX == null || dlb < prevMinX.doubleValue()) {
- bounds.setMinX(dlb);
+ if (prevMinX == null || lowerBoundary < prevMinX.doubleValue()) {
+ bounds.setMinX(lowerBoundary);
} else {
bounds.setMinX(prevMinX);
}
- if (prevMaxX == null || dub > prevMaxX.doubleValue()) {
- bounds.setMaxX(dub);
+ if (prevMaxX == null || upperBoundary > prevMaxX.doubleValue()) {
+ bounds.setMaxX(upperBoundary);
} else {
bounds.setMaxX(prevMaxX);
}
}
break;
case SHRINK:
- if (prevMinX == null || dlb > prevMinX.doubleValue()) {
- bounds.setMinX(dlb);
+ if (prevMinX == null || lowerBoundary > prevMinX.doubleValue()) {
+ bounds.setMinX(lowerBoundary);
} else {
bounds.setMinX(prevMinX);
}
- if (prevMaxX == null || dub < prevMaxX.doubleValue()) {
- bounds.setMaxX(dub);
+ if (prevMaxX == null || upperBoundary < prevMaxX.doubleValue()) {
+ bounds.setMaxX(upperBoundary);
} else {
bounds.setMaxX(prevMaxX);
}
@@ -661,14 +745,14 @@ public void updateRangeMinMaxForOriginModel() {
switch (rangeOriginBoundaryMode) {
case AUTO:
double origin = userRangeOrigin.doubleValue();
- double maxYDelta = distance(bounds.getMaxY().doubleValue(), origin);
- double minYDelta = distance(bounds.getMinY().doubleValue(), origin);
- if (maxYDelta > minYDelta) {
- bounds.setMinY(origin - maxYDelta);
- bounds.setMaxY(origin + maxYDelta);
+ double maxDelta = distance(bounds.getMaxY().doubleValue(), origin);
+ double minDelta = distance(bounds.getMinY().doubleValue(), origin);
+ if (maxDelta > minDelta) {
+ bounds.setMinY(origin - maxDelta);
+ bounds.setMaxY(origin + maxDelta);
} else {
- bounds.setMinY(origin - minYDelta);
- bounds.setMaxY(origin + minYDelta);
+ bounds.setMinY(origin - minDelta);
+ bounds.setMaxY(origin + minDelta);
}
break;
case FIXED:
@@ -959,12 +1043,12 @@ public synchronized void setUserRangeOrigin(Number origin) {
}
@SuppressWarnings("SameParameterValue")
- protected void setDomainFramingModel(XYFramingModel model) {
+ protected void setDomainFramingModel(@NonNull XYFramingModel model) {
constraints.setDomainFramingModel(model);
}
@SuppressWarnings("SameParameterValue")
- protected void setRangeFramingModel(XYFramingModel model) {
+ protected void setRangeFramingModel(@NonNull XYFramingModel model) {
constraints.setRangeFramingModel(model);
}
@@ -1012,9 +1096,7 @@ public YValueMarker removeMarker(YValueMarker marker) {
* @return
*/
public int removeMarkers() {
- int removed = removeXMarkers();
- removed += removeYMarkers();
- return removed;
+ return removeXMarkers() + removeYMarkers();
}
/**
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java
index b0a3186f..0492c6c2 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java
@@ -43,7 +43,11 @@ public class XYRegionFormatter {
public XYRegionFormatter(Context ctx, int xmlCfgId) {
// prevent configuration of classes derived from this one:
if (getClass().equals(XYRegionFormatter.class)) {
- Fig.configure(ctx, this, xmlCfgId);
+ try {
+ Fig.configure(ctx, this, xmlCfgId);
+ } catch (FigException e) {
+ throw new RuntimeException(e);
+ }
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java
index 0c18d2e9..27924ab1 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java
@@ -16,7 +16,7 @@
package com.androidplot.xy;
-import android.content.*;
+import android.content.Context;
import com.androidplot.ui.Formatter;
import com.androidplot.util.LayerHash;
@@ -28,7 +28,7 @@ public abstract class XYSeriesFormatter
-
+
+
-
+
@@ -48,36 +60,35 @@
-
+
-
-
-
+
+
+
-
+
-
+
-
+
-
+
@@ -186,6 +197,15 @@
+
+
+
+
+
+
+
+
+
@@ -258,180 +278,857 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
+
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
-
-
+
+
+
+
+
+Enable line labels on one or more edge of the graph. For example, to enable labels on the left
+and bottom edges:
+```
+ap:lineLabels="left|bottom"
+```
+-->
+
+Text alignment of line labels drawn on top edge of the plot. This alignment is applied relative
+to the line label insets defined by `lineLabelInsetTop`.
+-->
+
+
-
+
+Text alignment of line labels drawn on right edge of the plot. This alignment is applied relative
+to the line label insets defined by `lineLabelInsetRight`.
+-->
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
+
\ No newline at end of file
diff --git a/androidplot-core/src/test/java/com/androidplot/PlotTest.java b/androidplot-core/src/test/java/com/androidplot/PlotTest.java
index 6c013ea6..9e4753bf 100644
--- a/androidplot-core/src/test/java/com/androidplot/PlotTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/PlotTest.java
@@ -20,11 +20,11 @@
import android.graphics.*;
import android.util.*;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.test.*;
import com.androidplot.ui.*;
import com.halfhp.fig.*;
import org.junit.Test;
+import org.mockito.Mock;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.HashMap;
@@ -33,138 +33,20 @@
import static junit.framework.Assert.assertNotSame;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class PlotTest extends AndroidplotTest {
- static class MockPlotListener implements PlotListener {
-
- public void onBeforeDraw(Plot source, Canvas canvas) {}
-
- public void onAfterDraw(Plot source, Canvas canvas) {}
- }
-
- static class MockSeries implements Series {
-
- public String getTitle() {
- return null;
- }
-
- }
-
- static class MockSeries2 implements Series {
-
- public String getTitle() {
- return null;
- }
- }
-
- static class MockSeries3 implements Series {
-
- public String getTitle() {
- return null;
- }
- }
-
- static class MockRenderer1 extends SeriesRenderer {
-
- public MockRenderer1(Plot plot) {
- super(plot);
- }
-
- @Override
- public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) throws PlotRenderException {
-
- }
-
- @Override
- public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
-
- }
- }
- static class MockRenderer2 extends SeriesRenderer {
-
- public MockRenderer2(Plot plot) {
- super(plot);
- }
-
- @Override
- public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) throws PlotRenderException {
-
- }
-
- @Override
- public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
-
- }
- }
-
- static class MockFormatter1 extends Formatter {
-
- @Override
- public Class extends SeriesRenderer> getRendererClass() {
- return MockRenderer1.class;
- }
-
- @Override
- public SeriesRenderer doGetRendererInstance(MockPlot plot) {
- return new MockRenderer1(plot);
- }
- }
-
- static class MockFormatter2 extends Formatter {
-
- @Override
- public Class extends SeriesRenderer> getRendererClass() {
- return MockRenderer2.class;
- }
-
- @Override
- public SeriesRenderer doGetRendererInstance(MockPlot plot) {
- return new MockRenderer2(plot);
- }
- }
-
- public static class MockSeriesBundle extends SeriesBundle {
-
- public MockSeriesBundle(MockSeries series, Formatter formatter) {
- super(series, formatter);
- }
- }
-
- public static class MockPlot extends Plot> {
- public MockPlot(String title) {
- super(RuntimeEnvironment.application, title);
- }
-
- @Override
- protected SeriesRegistry getRegistryInstance() {
- return new SeriesRegistry() {
- @Override
- protected MockSeriesBundle newSeriesBundle(
- MockSeries series, Formatter formatter) {
- return new MockSeriesBundle(series, formatter);
- }
- };
- }
-
- @Override
- protected void onPreInit() {
-
- }
-
- @Override
- protected void processAttrs(TypedArray attrs) {
-
- }
- }
+ @Mock
+ SeriesRegistry mockSeriesRegistry;
@Test
- public void testInit_withoutAttrs() throws Exception {
- Plot plot = mock(Plot.class);
+ public void testInit_withoutAttrs() {
+ //Plot plot = mock(Plot.class);
+ Plot plot = spy(new MockPlot("MockPlot"));
plot.init(RuntimeEnvironment.application, null, 0);
verify(plot, times(1)).onPreInit();
@@ -172,15 +54,17 @@ public void testInit_withoutAttrs() throws Exception {
}
@Test
- public void testInit_withAttrs() throws Exception {
- Plot plot = new MockPlot("MockPlot");
+ public void testInit_withAttrs() {
+ Plot plot = spy(new MockPlot("MockPlot"));
AttributeSet attrs = mock(AttributeSet.class);
plot.init(RuntimeEnvironment.application, attrs, 0);
- // TODO: verifications
+
+ verify(plot, times(1)).onPreInit();
+ verify(plot, times(1)).onAfterConfig();
}
@Test
- public void testAddSeries() throws Exception {
+ public void testAddSeries() {
Plot plot = new MockPlot("MockPlot");
MockSeries m1 = new MockSeries();
@@ -213,7 +97,7 @@ public void testAddSeries() throws Exception {
}
@Test
- public void testRemoveSeries() throws Exception {
+ public void testRemoveSeries() {
Plot plot = new MockPlot("MockPlot");
@@ -273,7 +157,7 @@ public void testRemoveSeries() throws Exception {
@Test
- public void testGetFormatter() throws Exception {
+ public void testGetFormatter() {
Plot plot = new MockPlot("MockPlot");
MockSeries m1 = new MockSeries();
@@ -301,7 +185,7 @@ public void testGetFormatter() throws Exception {
}
@Test
- public void testGetRendererList() throws Exception {
+ public void testGetRendererList() {
Plot plot = new MockPlot("MockPlot");
@@ -322,7 +206,7 @@ public void testGetRendererList() throws Exception {
}
@Test
- public void testAddListener() throws Exception {
+ public void testAddListener() {
Plot plot = new MockPlot("MockPlot");
ArrayList listeners = plot.getListeners();
@@ -344,11 +228,11 @@ public void testAddListener() throws Exception {
plot.addListener(pl2);
assertEquals(2, listeners.size());
-
+
}
@Test
- public void testRemoveListener() throws Exception {
+ public void testRemoveListener() {
Plot plot = new MockPlot("MockPlot");
ArrayList listeners = plot.getListeners();
@@ -397,4 +281,188 @@ public void testConfigure() throws Exception {
assertEquals(Plot.RenderMode.USE_BACKGROUND_THREAD, plot.getRenderMode());
assertEquals(Color.parseColor(param3), plot.getBackgroundPaint().getColor());
}
+
+ @Test
+ public void setTitle_setsTitle() {
+ Plot plot = new MockPlot("foo");
+ plot.setTitle("bar");
+ assertEquals("bar", plot.getTitle().getText());
+ }
+
+ @Test
+ public void clear_unregistersAllPlotListeners() {
+ Plot plot = new MockPlot("MockPlot");
+ plot.addSeries(new MockSeries(), new MockFormatter1());
+ plot.addSeries(new MockSeries(), new MockFormatter1());
+ plot.addSeries(new MockSeries(), new MockFormatter1());
+ assertEquals(3, plot.getListeners().size());
+
+ plot.clear();
+ assertEquals(0, plot.getListeners().size());
+ }
+
+ @Test
+ public void clear_clearsRegistry() {
+ Plot plot = new MockPlot("MockPlot");
+ plot.setRegistry(mockSeriesRegistry);
+
+ plot.clear();
+ verify(mockSeriesRegistry).clear();
+ }
+
+ @Test
+ public void setPlotMargins_updatesMargins() {
+ Plot plot = new MockPlot("MockPlot");
+ plot.setPlotMargins(11, 22, 33, 44);
+
+ assertEquals(11f, plot.getPlotMarginLeft());
+ assertEquals(22f, plot.getPlotMarginTop());
+ assertEquals(33f, plot.getPlotMarginRight());
+ assertEquals(44f, plot.getPlotMarginBottom());
+ }
+
+ @Test
+ public void setPlotPadding_updatesPadding() {
+ Plot plot = new MockPlot("MockPlot");
+ plot.setPlotPadding(11, 22, 33, 44);
+
+ assertEquals(11f, plot.getPlotPaddingLeft());
+ assertEquals(22f, plot.getPlotPaddingTop());
+ assertEquals(33f, plot.getPlotPaddingRight());
+ assertEquals(44f, plot.getPlotPaddingBottom());
+ }
+
+ static class MockPlotListener implements PlotListener {
+
+ public void onBeforeDraw(Plot source, Canvas canvas) {
+ }
+
+ public void onAfterDraw(Plot source, Canvas canvas) {
+ }
+ }
+
+ static class MockSeries implements Series, PlotListener {
+
+ public String getTitle() {
+ return null;
+ }
+
+ @Override
+ public void onBeforeDraw(Plot source, Canvas canvas) {
+
+ }
+
+ @Override
+ public void onAfterDraw(Plot source, Canvas canvas) {
+
+ }
+ }
+
+ static class MockSeries2 implements Series {
+
+ public String getTitle() {
+ return null;
+ }
+ }
+
+ static class MockSeries3 implements Series {
+
+ public String getTitle() {
+ return null;
+ }
+ }
+
+ static class MockRenderer1 extends SeriesRenderer {
+
+ public MockRenderer1(Plot plot) {
+ super(plot);
+ }
+
+ @Override
+ public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) {
+
+ }
+
+ @Override
+ public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
+
+ }
+ }
+
+ static class MockRenderer2 extends SeriesRenderer {
+
+ public MockRenderer2(Plot plot) {
+ super(plot);
+ }
+
+ @Override
+ public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) {
+
+ }
+
+ @Override
+ public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
+
+ }
+ }
+
+ static class MockFormatter1 extends Formatter {
+
+ @Override
+ public Class extends SeriesRenderer> getRendererClass() {
+ return MockRenderer1.class;
+ }
+
+ @Override
+ public SeriesRenderer doGetRendererInstance(MockPlot plot) {
+ return new MockRenderer1(plot);
+ }
+ }
+
+ static class MockFormatter2 extends Formatter {
+
+ @Override
+ public Class extends SeriesRenderer> getRendererClass() {
+ return MockRenderer2.class;
+ }
+
+ @Override
+ public SeriesRenderer doGetRendererInstance(MockPlot plot) {
+ return new MockRenderer2(plot);
+ }
+ }
+
+ public static class MockSeriesBundle extends SeriesBundle {
+
+ public MockSeriesBundle(MockSeries series, Formatter formatter) {
+ super(series, formatter);
+ }
+ }
+
+ public static class MockPlot extends Plot> {
+ public MockPlot(String title) {
+ super(RuntimeEnvironment.application, title);
+ }
+
+ @Override
+ protected SeriesRegistry getRegistryInstance() {
+ return new SeriesRegistry() {
+ @Override
+ protected MockSeriesBundle newSeriesBundle(
+ MockSeries series, Formatter formatter) {
+ return new MockSeriesBundle(series, formatter);
+ }
+ };
+ }
+
+ @Override
+ protected void onPreInit() {
+
+ }
+
+ @Override
+ protected void processAttrs(TypedArray attrs) {
+
+ }
+ }
}
diff --git a/androidplot-core/src/test/java/com/androidplot/RegionTest.java b/androidplot-core/src/test/java/com/androidplot/RegionTest.java
index 1dbc6dc9..c38ded3c 100644
--- a/androidplot-core/src/test/java/com/androidplot/RegionTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/RegionTest.java
@@ -20,7 +20,7 @@
import org.junit.Before;
import org.junit.Test;
-import static junit.framework.Assert.fail;
+import static junit.framework.Assert.assertNotSame;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
diff --git a/androidplot-core/src/test/java/com/androidplot/SeriesRegistryTest.java b/androidplot-core/src/test/java/com/androidplot/SeriesRegistryTest.java
new file mode 100644
index 00000000..7352193f
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/SeriesRegistryTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * 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.androidplot;
+
+import com.androidplot.test.AndroidplotTest;
+import com.androidplot.ui.Formatter;
+import com.androidplot.xy.BarFormatter;
+import com.androidplot.xy.LineAndPointFormatter;
+import com.androidplot.xy.SimpleXYSeries;
+import com.androidplot.xy.XYSeriesRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class SeriesRegistryTest extends AndroidplotTest {
+
+ SeriesRegistry seriesRegistry;
+
+ @Before
+ public void setUp() throws Exception {
+ seriesRegistry = new XYSeriesRegistry();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ @Test
+ public void testAdd() {
+ assertEquals(0, seriesRegistry.size());
+ seriesRegistry.add(new SimpleXYSeries("s1"), new LineAndPointFormatter());
+ assertEquals(1, seriesRegistry.size());
+ }
+
+ @Test
+ public void testAdd_failsOnNullArgument() throws Exception {
+ try {
+ seriesRegistry.add(null, null);
+ fail("IllegalArgumentException expected.");
+ } catch(IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ seriesRegistry.add(new SimpleXYSeries("s1"), null);
+ fail("IllegalArgumentException expected.");
+ } catch(IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ seriesRegistry.add(null, new LineAndPointFormatter());
+ fail("IllegalArgumentException expected.");
+ } catch(IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testGet() {
+ Series s1 = new SimpleXYSeries("s1");
+ Formatter f1 = new LineAndPointFormatter();
+ Series s2 = new SimpleXYSeries("s2");
+ Formatter f2 = new LineAndPointFormatter();
+ Formatter f3 = new LineAndPointFormatter();
+ seriesRegistry.add(s1, f1);
+ seriesRegistry.add(s1, f3);
+ seriesRegistry.add(s2, f2);
+
+
+ assertEquals(2, seriesRegistry.get(s1).size());
+ assertEquals(1, seriesRegistry.get(s2).size());
+ }
+
+ @Test
+ public void testRemove() {
+ Series series = new SimpleXYSeries("s1");
+ seriesRegistry.add(series, new LineAndPointFormatter());
+ assertEquals(1, seriesRegistry.size());
+
+ seriesRegistry.remove(new SimpleXYSeries("s2"));
+ assertEquals(1, seriesRegistry.size());
+
+ seriesRegistry.remove(series);
+ assertEquals(0, seriesRegistry.size());
+ }
+
+ @Test
+ public void testClear() {
+ seriesRegistry.add(new SimpleXYSeries("s1"), new LineAndPointFormatter());
+ seriesRegistry.add(new SimpleXYSeries("s2"), new LineAndPointFormatter());
+ assertEquals(2, seriesRegistry.size());
+
+ seriesRegistry.clear();
+ assertEquals(0, seriesRegistry.size());
+
+ }
+
+ @Test
+ public void testContains() {
+ Series s1 = new SimpleXYSeries("s1");
+ Series s2 = new SimpleXYSeries("s1");
+ Series s3 = new SimpleXYSeries("s1");
+
+ seriesRegistry.add(s1, new LineAndPointFormatter());
+ seriesRegistry.add(s2, new LineAndPointFormatter());
+
+ assertTrue(seriesRegistry.contains(s1, LineAndPointFormatter.class));
+ assertFalse(seriesRegistry.contains(s1, BarFormatter.class));
+ assertTrue(seriesRegistry.contains(s2, LineAndPointFormatter.class));
+ assertFalse(seriesRegistry.contains(s3, LineAndPointFormatter.class));
+
+ }
+
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java
index e787afaa..e504582a 100644
--- a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java
@@ -27,6 +27,8 @@
import org.junit.*;
import org.mockito.*;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.fail;
import static org.mockito.Mockito.*;
public class PieRendererTest extends AndroidplotTest {
@@ -47,17 +49,18 @@ public class PieRendererTest extends AndroidplotTest {
@Mock
RenderStack renderStack;
+ Canvas canvas;
+
@Before
public void setUp() throws Exception {
pieChart = spy(new PieChart(getContext(), "My Pie"));
+ canvas = spy(new Canvas());
}
@Test
public void testDrawSegment_withoutTextPaintDoesntDrawLabel() throws Exception {
PieRenderer pieRenderer = spy(new PieRenderer(pieChart));
Segment segment = spy(new Segment("My Segment", 100));
- Canvas canvas = new Canvas();
-
SegmentFormatter formatterWithoutTextPaint = new SegmentFormatter(Color.GREEN);
formatterWithoutTextPaint.setLabelPaint(null);
@@ -96,7 +99,6 @@ public void testDrawSegment_withoutTextPaintDoesntDrawLabel() throws Exception {
@Test
public void testOnRender() throws Exception {
Segment segment = spy(new Segment("My Segment", 100));
- Canvas canvas = new Canvas();
SegmentFormatter formatter = spy(
new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN));
PieRenderer pr = formatter.getRendererInstance(pieChart);
@@ -106,4 +108,127 @@ public void testOnRender() throws Exception {
pieChart.addSegment(segment, formatter);
renderer.onRender(canvas, plotArea, segment, formatter, renderStack);
}
+
+ @Test
+ public void getContainingSegment_returnsCorrectSegment() throws Exception {
+ Segment segment1 = spy(new Segment("s1", 25));
+ Segment segment2 = spy(new Segment("s2", 25));
+ Segment segment3 = spy(new Segment("s3", 25));
+ Segment segment4 = spy(new Segment("s4", 25));
+ SegmentFormatter formatter = spy(
+ new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN));
+ PieRenderer renderer = formatter.getRendererInstance(pieChart);
+
+ pieChart.addSegment(segment1, formatter);
+ pieChart.addSegment(segment2, formatter);
+ pieChart.addSegment(segment3, formatter);
+ pieChart.addSegment(segment4, formatter);
+
+ // southeast
+ assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 100)));
+
+ // southwest
+ assertEquals(segment2, renderer.getContainingSegment(new PointF(0, 100)));
+
+ // northwest
+ assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0)));
+
+ // northeast
+ assertEquals(segment4, renderer.getContainingSegment(new PointF(100, 0)));
+
+ renderer.setStartDegs(90);
+ // southeast
+ assertEquals(segment2, renderer.getContainingSegment(new PointF(100, 100)));
+
+ // southwest
+ assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 100)));
+
+ // northwest
+ assertEquals(segment4, renderer.getContainingSegment(new PointF(0, 0)));
+
+ // northeast
+ assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0)));
+ }
+
+ @Test
+ public void getContainingSegment_handlesSegmentsLargerThanHalfPie() throws Exception {
+ Segment segment1 = spy(new Segment("s1", 25));
+ Segment segment2 = spy(new Segment("s2", 24));
+ Segment segment3 = spy(new Segment("s3", 51));
+ SegmentFormatter formatter = spy(
+ new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN));
+ PieRenderer renderer = formatter.getRendererInstance(pieChart);
+
+ pieChart.addSegment(segment1, formatter);
+ pieChart.addSegment(segment2, formatter);
+ pieChart.addSegment(segment3, formatter);
+
+ // southeast
+ assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 100)));
+
+ // southwest
+ assertEquals(segment2, renderer.getContainingSegment(new PointF(0, 100)));
+
+ // northwest
+ assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0)));
+
+ // northeast
+ assertEquals(segment3, renderer.getContainingSegment(new PointF(100, 0)));
+
+ renderer.setStartDegs(90);
+ // southeast
+ assertEquals(segment2, renderer.getContainingSegment(new PointF(100, 100)));
+
+ // southwest
+ assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 100)));
+
+ // northwest
+ assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0)));
+
+ // northeast
+ assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0)));
+ }
+
+ @Test
+ public void testDegsToScreenDegs() throws Exception {
+ assertEquals(0f, PieRenderer.degsToScreenDegs(0));
+ assertEquals(359f, PieRenderer.degsToScreenDegs(1));
+ assertEquals(271f, PieRenderer.degsToScreenDegs(89));
+ assertEquals(270f, PieRenderer.degsToScreenDegs(90));
+ assertEquals(269f, PieRenderer.degsToScreenDegs(91));
+ assertEquals(181f, PieRenderer.degsToScreenDegs(179));
+ assertEquals(180f, PieRenderer.degsToScreenDegs(180));
+ assertEquals(179f, PieRenderer.degsToScreenDegs(181));
+ assertEquals(91f, PieRenderer.degsToScreenDegs(269));
+ assertEquals(90f, PieRenderer.degsToScreenDegs(270));
+ assertEquals(89f, PieRenderer.degsToScreenDegs(271));
+ assertEquals(1f, PieRenderer.degsToScreenDegs(359));
+ assertEquals(0f, PieRenderer.degsToScreenDegs(360));
+
+ }
+
+ @Test
+ public void testSetDonutSize() throws Exception {
+
+ Segment segment1 = spy(new Segment("s1", 25));
+ Segment segment2 = spy(new Segment("s2", 25));
+ Segment segment3 = spy(new Segment("s3", 25));
+ Segment segment4 = spy(new Segment("s4", 25));
+
+ SegmentFormatter formatter = spy(
+ new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN));
+ PieRenderer renderer = formatter.getRendererInstance(pieChart);
+
+ pieChart.addSegment(segment1, formatter);
+ pieChart.addSegment(segment2, formatter);
+ pieChart.addSegment(segment3, formatter);
+ pieChart.addSegment(segment4, formatter);
+
+ renderer.setDonutSize(0.25f, PieRenderer.DonutMode.PERCENT);
+
+ renderer.onRender(canvas, plotArea, segment1, formatter, renderStack);
+
+ // TODO: verify radials are drown at the correct offsets from center
+ //verify(canvas).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), any(Paint.class));
+ }
}
diff --git a/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java
index 93e9143d..1f8f1457 100644
--- a/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java
+++ b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java
@@ -10,14 +10,26 @@
import static org.robolectric.Shadows.shadowOf;
/**
- * Created by halfhp on 10/8/16.
+ * Utilities to help with unit testing
*/
public abstract class TestUtils {
public static XYSeries generateXYSeries(String title, int size) {
+ return generateXYSeries(title, size, 0, 1);
+ }
+
+ /**
+ * Generate a series of random numbers within a min/max range
+ * @param title
+ * @param size
+ * @param min
+ * @param max
+ * @return
+ */
+ public static XYSeries generateXYSeries(String title, int size, double min, double max) {
SimpleXYSeries series = new SimpleXYSeries(title);
for(int i = 0; i < size; i++) {
- series.addLast(i, Math.random());
+ series.addLast(i, Math.random() * max - min);
}
return series;
}
@@ -44,11 +56,11 @@ public static XYSeries generateXYSeriesWithNulls(String title, int size) {
*/
@SuppressLint("NewApi")
public static MotionEvent newPointerDownEvent(int finger1x, int finger1y, int finger2x, int finger2y) {
- MotionEvent me = MotionEvent.obtain(0, 0, 0, 0, 0, 0);
+ MotionEvent me = MotionEvent.obtain(0, 0, MotionEvent.ACTION_POINTER_DOWN, finger1x, finger1y, 0);
ShadowMotionEvent sme = shadowOf(me);
- sme.setAction(MotionEvent.ACTION_POINTER_DOWN);
+ //sme.setAction(MotionEvent.ACTION_POINTER_DOWN);
sme.setPointerIds(0, 1);
- sme.setLocation(finger1x, finger1y);
+ //sme.setLocation(finger1x, finger1y);
sme.setPointer2(finger2x, finger2y);
return me;
}
diff --git a/androidplot-core/src/test/java/com/androidplot/ui/RenderStackTest.java b/androidplot-core/src/test/java/com/androidplot/ui/RenderStackTest.java
new file mode 100644
index 00000000..59bab380
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/ui/RenderStackTest.java
@@ -0,0 +1,52 @@
+package com.androidplot.ui;
+
+import com.androidplot.Plot;
+import com.androidplot.SeriesRegistry;
+import com.androidplot.test.AndroidplotTest;
+import com.androidplot.xy.LineAndPointFormatter;
+import com.androidplot.xy.LineAndPointRenderer;
+import com.androidplot.xy.XYSeries;
+import com.google.common.collect.Lists;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class RenderStackTest extends AndroidplotTest {
+
+ @Mock Plot plot;
+ @Mock SeriesRegistry seriesRegistry;
+
+ @Before
+ public void before() {
+ when(plot.getRegistry()).thenReturn(seriesRegistry);
+ }
+
+ @Test
+ public void disable_disablesElements_ofSpecifiedRendererOnly() {
+ final SeriesBundle s1 = new SeriesBundle(mock(XYSeries.class),
+ new LineAndPointFormatter());
+ final SeriesBundle s2 = new SeriesBundle(mock(XYSeries.class),
+ new LineAndPointFormatter());
+ when(seriesRegistry.getSeriesAndFormatterList()).thenReturn(Lists.newArrayList(s1, s2));
+ RenderStack renderStack = new RenderStack<>(plot);
+
+ renderStack.sync();
+ assertEquals(2, renderStack.getElements().size());
+ for(RenderStack.StackElement element : renderStack.getElements()) {
+ assertTrue(element.isEnabled());
+ }
+
+ renderStack.disable(LineAndPointRenderer.class);
+ assertEquals(2, renderStack.getElements().size());
+ for(RenderStack.StackElement element : renderStack.getElements()) {
+ assertFalse(element.isEnabled());
+ }
+ }
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/ui/widget/TextLabelWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/ui/widget/TextLabelWidgetTest.java
new file mode 100644
index 00000000..bdb48f92
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/ui/widget/TextLabelWidgetTest.java
@@ -0,0 +1,82 @@
+package com.androidplot.ui.widget;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.androidplot.test.AndroidplotTest;
+import com.androidplot.ui.LayoutManager;
+import com.androidplot.ui.Size;
+import com.androidplot.ui.TextOrientation;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class TextLabelWidgetTest extends AndroidplotTest {
+
+ @Mock
+ LayoutManager layoutManager;
+
+ @Mock
+ Size size;
+
+ @Mock
+ Canvas canvas;
+
+ @Mock
+ RectF rectF;
+
+ private TextLabelWidget textLabelWidget;
+
+ @Before
+ public void before() {
+ textLabelWidget = spy(new TextLabelWidget(layoutManager, size));
+ }
+
+ @Test
+ public void onMetricsChanged_invokesPack_ifAutopackEnabled() {
+ textLabelWidget.setAutoPackEnabled(true);
+ textLabelWidget.onMetricsChanged(size, size);
+ verify(textLabelWidget, times(2)).pack();
+ }
+
+ @Test
+ public void onMetricsChanged_doesNotInvokePack_ifAutopackDisabled() {
+ textLabelWidget.setAutoPackEnabled(false);
+ textLabelWidget.onMetricsChanged(size, size);
+ verify(textLabelWidget, never()).pack();
+ }
+
+ @Test
+ public void doOnDraw_rotatesThenDraws_ifVerticalAscending() {
+ textLabelWidget.setText("this is a test");
+ textLabelWidget.setOrientation(TextOrientation.VERTICAL_ASCENDING);
+ textLabelWidget.doOnDraw(canvas, rectF);
+
+ InOrder inOrder = inOrder(canvas);
+ inOrder.verify(canvas).rotate(-90);
+ inOrder.verify(canvas).drawText(eq(textLabelWidget.getText()), anyFloat(), anyFloat(), any(Paint.class));
+ }
+
+ @Test
+ public void doOnDraw_rotatesThenDraws_ifVerticalDescending() {
+ textLabelWidget.setText("this is a test");
+ textLabelWidget.setOrientation(TextOrientation.VERTICAL_DESCENDING);
+ textLabelWidget.doOnDraw(canvas, rectF);
+
+ InOrder inOrder = inOrder(canvas);
+ inOrder.verify(canvas).rotate(90);
+ inOrder.verify(canvas).drawText(eq(textLabelWidget.getText()), anyFloat(), anyFloat(), any(Paint.class));
+ }
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/ui/widget/WidgetTest.java b/androidplot-core/src/test/java/com/androidplot/ui/widget/WidgetTest.java
new file mode 100644
index 00000000..cae6c056
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/ui/widget/WidgetTest.java
@@ -0,0 +1,113 @@
+package com.androidplot.ui.widget;
+
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import androidx.annotation.NonNull;
+
+import com.androidplot.test.AndroidplotTest;
+import com.androidplot.ui.Anchor;
+import com.androidplot.ui.HorizontalPositioning;
+import com.androidplot.ui.LayoutManager;
+import com.androidplot.ui.PositionMetrics;
+import com.androidplot.ui.Size;
+import com.androidplot.ui.SizeMetric;
+import com.androidplot.ui.SizeMode;
+import com.androidplot.ui.VerticalPositioning;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class WidgetTest extends AndroidplotTest {
+
+ @Mock
+ LayoutManager layoutManager;
+
+ @Mock
+ Size size;
+
+ @Mock
+ Canvas canvas;
+
+ Widget widget;
+
+ PositionMetrics positionMetrics;
+
+ @Before
+ public void before() {
+
+ positionMetrics = new PositionMetrics(
+ 0, HorizontalPositioning.ABSOLUTE_FROM_LEFT,
+ 0, VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.LEFT_TOP);
+ when(size.getHeight()).thenReturn(new SizeMetric(100, SizeMode.ABSOLUTE));
+ when(size.getWidth()).thenReturn(new SizeMetric(100, SizeMode.ABSOLUTE));
+ widget = spy(new TestWidget(layoutManager, size));
+ widget.setPositionMetrics(positionMetrics);
+ widget.refreshLayout();
+ }
+
+ @Test
+ public void setGetAnchor_setsAndGetsAnchor() {
+ widget.setAnchor(Anchor.LEFT_TOP);
+ assertEquals(Anchor.LEFT_TOP, widget.getAnchor());
+
+ widget.setAnchor(Anchor.RIGHT_BOTTOM);
+ assertEquals(Anchor.RIGHT_BOTTOM, widget.getAnchor());
+ }
+
+ @Test
+ public void position_withoutAnchor_updatesPositionWithExistingAnchor() {
+ final Anchor existingAnchor = widget.getAnchor();
+ widget.position(1, HorizontalPositioning.ABSOLUTE_FROM_RIGHT,
+ 1, VerticalPositioning.ABSOLUTE_FROM_BOTTOM);
+
+ assertEquals(existingAnchor, widget.getAnchor());
+ assertEquals(1F, widget.getPositionMetrics().getYPositionMetric().getValue());
+ assertEquals(1F, widget.getPositionMetrics().getXPositionMetric().getValue());
+ }
+
+ @Test
+ public void position_withAnchor_updatesPositionWithNewAnchor() {
+ final Anchor newAnchor = Anchor.CENTER;
+ widget.position(1, HorizontalPositioning.ABSOLUTE_FROM_RIGHT,
+ 1, VerticalPositioning.ABSOLUTE_FROM_BOTTOM, newAnchor);
+
+ assertEquals(newAnchor, widget.getAnchor());
+ assertEquals(1F, widget.getPositionMetrics().getYPositionMetric().getValue());
+ assertEquals(1F, widget.getPositionMetrics().getXPositionMetric().getValue());
+ }
+
+ @Test
+ public void draw_sizeChanged_invokesOnResizeBeforeDoOnDraw() {
+ InOrder inOrder = Mockito.inOrder(widget);
+
+ widget.draw(canvas);
+
+ inOrder.verify(widget).onResize(isNull(), any(RectF.class));
+ inOrder.verify(widget).doOnDraw(eq(canvas), any(RectF.class));
+ verify(widget).onResize(isNull(), any(RectF.class));
+ }
+
+ static class TestWidget extends Widget {
+
+ public TestWidget(@NonNull LayoutManager layoutManager, @NonNull Size size) {
+ super(layoutManager, size);
+ }
+
+ @Override
+ protected void doOnDraw(Canvas canvas, RectF widgetRect) {
+ // nothing to do
+ }
+ }
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java b/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java
new file mode 100644
index 00000000..8bca2a38
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java
@@ -0,0 +1,136 @@
+package com.androidplot.util;
+
+import android.annotation.SuppressLint;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@SuppressWarnings({"UnnecessaryBoxing", "ObjectEqualsNull", "EqualsBetweenInconvertibleTypes", "EqualsWithItself", "NumberEquality"})
+@SuppressLint("UseValueOf")
+public class FastNumberTest {
+
+ private static final Number[] NUMBERS = new Number[]{
+ new Integer(0),
+ new Integer(1),
+ new Integer(-1),
+ new Integer(Integer.MAX_VALUE),
+ new Integer(Integer.MIN_VALUE),
+
+ new Long(0),
+ new Long(1),
+ new Long(-1),
+ new Long(Long.MAX_VALUE),
+ new Long(Long.MIN_VALUE),
+
+ new Double(0),
+ new Double(1),
+ new Double(-1),
+ new Double(87654321.12345d),
+ new Double(0.0002f),
+ new Double(Double.MAX_VALUE),
+ new Double(Double.MIN_VALUE),
+
+ new Float(0),
+ new Float(1.0f),
+ new Float(-1.0f),
+ new Float(8765.4321f),
+ new Float(1098.576f),
+ new Float(Float.MIN_VALUE),
+ new Float(Float.MAX_VALUE),
+ };
+
+ private static final Number[] NUMBERS_CLONE = new Number[]{
+ new Integer(0),
+ new Integer(1),
+ new Integer(-1),
+ new Integer(Integer.MAX_VALUE),
+ new Integer(Integer.MIN_VALUE),
+
+ new Long(0),
+ new Long(1),
+ new Long(-1),
+ new Long(Long.MAX_VALUE),
+ new Long(Long.MIN_VALUE),
+
+ new Double(0),
+ new Double(1),
+ new Double(-1),
+ new Double(87654321.12345d),
+ new Double(0.0002f),
+ new Double(Double.MAX_VALUE),
+ new Double(Double.MIN_VALUE),
+
+ new Float(0),
+ new Float(1.0f),
+ new Float(-1.0f),
+ new Float(8765.4321f),
+ new Float(1098.576f),
+ new Float(Float.MIN_VALUE),
+ new Float(Float.MAX_VALUE),
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ @Test
+ public void orNull_returnsNull_ifNullNumber() {
+ //noinspection ConstantConditions
+ assertNull(FastNumber.orNull(null));
+ }
+
+ @Test
+ public void equals_returnsTrue_ifFromSameNumberInstance() {
+ for (Number number : NUMBERS) {
+ assertTrue("Equality test failed on " + number, FastNumber.orNull(number).equals(FastNumber.orNull(number)));
+ }
+ }
+
+ @Test
+ public void equals_returnsTrue_ifFromSameNumber() {
+ assertEquals("misconfigured test values", NUMBERS.length, NUMBERS_CLONE.length);
+ for (int i = 0; i < NUMBERS.length; i++) {
+ assertEquals("misconfigured test values", NUMBERS[i], NUMBERS_CLONE[i]);
+ assertTrue(FastNumber.orNull(NUMBERS[i]).equals(FastNumber.orNull(NUMBERS_CLONE[i])));
+ }
+ }
+
+ @Test
+ public void equals_returnsFalse_ifNumberIsDifferent() {
+ for (int i = 0; i < NUMBERS.length; i++) {
+ for (int j = 0; j < NUMBERS.length; j++) {
+ if (j == i) {
+ continue;
+ }
+ assertNotEquals("duplicate test values at index " + i + " and " + j, NUMBERS[i], NUMBERS[j]);
+ assertFalse(FastNumber.orNull(NUMBERS[i]).equals(FastNumber.orNull(NUMBERS[j])));
+ }
+ }
+ }
+
+ @Test
+ public void hashCode_isEqual_ifInstanceIsEqual() {
+ for (Number number : NUMBERS) {
+ for (Number number2 : NUMBERS) {
+ FastNumber fastNumber1 = FastNumber.orNull(number);
+ FastNumber fastNumber2 = FastNumber.orNull(number2);
+ if (fastNumber1.equals(fastNumber2)) {
+ assertEquals(fastNumber1.hashCode(), fastNumber2.hashCode());
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java b/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java
index 3d479495..3dbd6970 100644
--- a/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java
@@ -35,33 +35,32 @@ public void setUp() throws Exception {
layerHash.addToBottom(obj2, obj2);
layerHash.addToBottom(obj3, obj3);
- assertEquals(obj3, layerHash.getKeysAsList().get(0));
- assertEquals(obj2, layerHash.getKeysAsList().get(1));
- assertEquals(obj1, layerHash.getKeysAsList().get(2));
+ assertEquals(obj1, layerHash.getTop());
+ assertEquals(obj3, layerHash.getBottom());
}
// "top" == last element in the list:
@Test
- public void testMoveUp() throws Exception {
+ public void moveUp() {
layerHash.moveUp(obj3);
- assertEquals(obj2, layerHash.getKeysAsList().get(0));
- assertEquals(obj3, layerHash.getKeysAsList().get(1));
- assertEquals(obj1, layerHash.getKeysAsList().get(2));
+ assertEquals(obj1, layerHash.getTop());
+ assertEquals(obj3, layerHash.getBeneath(obj1));
+ assertEquals(obj2, layerHash.getBeneath(obj3));
layerHash.moveUp(obj3);
- assertEquals(obj2, layerHash.getKeysAsList().get(0));
- assertEquals(obj1, layerHash.getKeysAsList().get(1));
- assertEquals(obj3, layerHash.getKeysAsList().get(2));
+ assertEquals(obj3, layerHash.getTop());
+ assertEquals(obj1, layerHash.getBeneath(obj3));
+ assertEquals(obj2, layerHash.getBeneath(obj1));
layerHash.moveUp(obj3);
- assertEquals(obj2, layerHash.getKeysAsList().get(0));
- assertEquals(obj1, layerHash.getKeysAsList().get(1));
- assertEquals(obj3, layerHash.getKeysAsList().get(2));
+ assertEquals(obj3, layerHash.getTop());
+ assertEquals(obj1, layerHash.getBeneath(obj3));
+ assertEquals(obj2, layerHash.getBeneath(obj1));
}
// "bottom" == first element in the list:
@Test
- public void testMoveDown() throws Exception {
+ public void moveDown() {
layerHash.moveDown(obj1);
assertEquals(obj3, layerHash.getKeysAsList().get(0));
assertEquals(obj1, layerHash.getKeysAsList().get(1));
@@ -79,7 +78,7 @@ public void testMoveDown() throws Exception {
}
@Test
- public void testMoveAbove() throws Exception {
+ public void moveAbove() {
layerHash.moveAbove(obj2, obj1);
assertEquals(obj3, layerHash.getKeysAsList().get(0));
assertEquals(obj1, layerHash.getKeysAsList().get(1));
@@ -87,11 +86,45 @@ public void testMoveAbove() throws Exception {
}
@Test
- public void testMoveBeneath() throws Exception {
+ public void moveBeneath() {
layerHash.moveBeneath(obj1, obj2);
assertEquals(obj3, layerHash.getKeysAsList().get(0));
assertEquals(obj1, layerHash.getKeysAsList().get(1));
assertEquals(obj2, layerHash.getKeysAsList().get(2));
}
+ @Test
+ public void addToTop() {
+ Object obj = new Object();
+ layerHash.addToTop(obj, obj);
+ assertEquals(obj, layerHash.getTop());
+ assertEquals(obj3, layerHash.getKeysAsList().get(0));
+ assertEquals(obj2, layerHash.getKeysAsList().get(1));
+ assertEquals(obj1, layerHash.getKeysAsList().get(2));
+ assertEquals(obj, layerHash.getKeysAsList().get(3));
+ }
+
+ @Test
+ public void moveToTop() {
+ layerHash.moveToTop(obj3);
+ assertEquals(obj3, layerHash.getTop());
+ assertEquals(obj1, layerHash.getBeneath(obj3));
+ assertEquals(obj2, layerHash.getBeneath(obj1));
+ }
+
+ @Test
+ public void moveToBottom() {
+ layerHash.moveToBottom(obj1);
+ assertEquals(obj1, layerHash.getBottom());
+ assertEquals(obj3, layerHash.getAbove(obj1));
+ assertEquals(obj2, layerHash.getAbove(obj3));
+ }
+
+ @Test
+ public void remove() {
+ layerHash.remove(obj2);
+ assertEquals(2, layerHash.size());
+ assertEquals(obj1, layerHash.getAbove(obj3));
+ }
+
}
diff --git a/androidplot-core/src/test/java/com/androidplot/util/PlotStatisticsTest.java b/androidplot-core/src/test/java/com/androidplot/util/PlotStatisticsTest.java
new file mode 100644
index 00000000..4454c69c
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/util/PlotStatisticsTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * 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.androidplot.util;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import com.androidplot.Plot;
+import com.androidplot.test.AndroidplotTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class PlotStatisticsTest extends AndroidplotTest {
+
+ @Mock
+ Canvas canvas;
+
+ @Mock
+ Paint paint;
+
+ @Mock
+ Plot plot;
+
+ @InjectMocks
+ PlotStatistics ps = new PlotStatistics(1, true);
+
+ @Before
+ public void before() {
+ when(plot.getDisplayDimensions()).thenReturn(new DisplayDimensions());
+ }
+
+ @Test
+ public void annotatePlot_annotates_ifEnabled() {
+ ps.onAfterDraw(plot, canvas);
+ verify(canvas).drawText(anyString(), anyFloat(), anyFloat(), any(Paint.class));
+ }
+
+ @Test
+ public void annotatePlot_doesNotAnnotate_ifDisabled() {
+ ps.setEnabled(false);
+ ps.onAfterDraw(plot, canvas);
+ verify(canvas, never()).drawText(anyString(), anyFloat(), anyFloat(), any(Paint.class));
+ }
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java
index 83a7eb81..df9014cd 100644
--- a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java
@@ -27,6 +27,7 @@
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.fail;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class SeriesUtilsTest {
@@ -50,7 +51,7 @@ public void tearDown() throws Exception {
}
@Test
- public void testSeriesMinMax() {
+ public void minMax_onSimpleXYSeries_calculatesExpectedRegion() {
SimpleXYSeries series = new SimpleXYSeries(LINEAR, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null);
RectRegion minMax = SeriesUtils.minMax(series);
assertEquals(0, minMax.getMinX().doubleValue(), 0);
@@ -102,7 +103,68 @@ public void testSeriesMinMax() {
}
@Test
- public void testListMinMax() {
+ public void minMax_onFastXYSeries_usesSeriesMinMax() {
+ FastXYSeries series = mock(FastXYSeries.class);
+ SeriesUtils.minMax(series);
+ verify(series).minMax();
+ }
+
+ @Test
+ public void minMax_calculatesExpectedRegion_onFastXYSeriesWithLayoutConstraints() {
+ FastXYSeries series = new FastXYSeries() {
+
+ // create a couple arrays of y-values to plot:
+ final ArrayList times = new ArrayList<>();
+ final ArrayList values = new ArrayList<>();
+
+ {
+ for (int i = 0; i < 10; i++) {
+ times.add(i);
+ values.add(i);
+ }
+ }
+
+ @Override
+ public int size() {
+ return times.size();
+ }
+
+ @Override
+ public Number getX(int index) {
+ return times.get(index);
+ }
+
+ @Override
+ public Number getY(int index) {
+ return values.get(index);
+ }
+
+ @Override
+ public String getTitle() {
+ return "Isaac's crazy thing";
+ }
+
+ @Override
+ public RectRegion minMax() {
+ return new RectRegion(0, 10, 0, 10);
+ }
+ };
+
+ final XYConstraints constraints = new XYConstraints();
+ constraints.setDomainLowerBoundaryMode(BoundaryMode.FIXED);
+ constraints.setDomainUpperBoundaryMode(BoundaryMode.FIXED);
+ constraints.setMinX(5);
+ constraints.setMaxX(9);
+
+ final RectRegion result = SeriesUtils.minMax(constraints, series);
+ assertEquals(5d, result.getMinX().doubleValue());
+ assertEquals(9d, result.getMaxX().doubleValue());
+ assertEquals(5d, result.getMinY().doubleValue());
+ assertEquals(9d, result.getMaxY().doubleValue());
+ }
+
+ @Test
+ public void minMax_onSeriesList_producesAggregateResult() {
Region minMax = SeriesUtils.minMax(LINEAR);
assertEquals(1, minMax.getMin().doubleValue(), 0);
assertEquals(8, minMax.getMax().doubleValue(), 0);
@@ -133,7 +195,7 @@ public void testListMinMax() {
}
@Test
- public void testGetNullRegion() {
+ public void getNullRegion_producesExpectedResult() {
XYSeries s1 = new SimpleXYSeries(
SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "s1",
0, 0, // 0
@@ -168,7 +230,7 @@ public void testGetNullRegion() {
}
@Test
- public void testIboundsMin() {
+ public void iBoundsMin_findsMin() {
XYSeries s1 = new SimpleXYSeries(
Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
@@ -195,7 +257,7 @@ public void testIboundsMin() {
}
@Test
- public void testIboundsMax() {
+ public void iBoundsMax_findsMax() {
XYSeries s1 = new SimpleXYSeries(
Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
@@ -222,7 +284,7 @@ public void testIboundsMax() {
}
@Test
- public void testIbounds() {
+ public void iBounds_findsMinMax() {
FastXYSeries series = mock(FastXYSeries.class);
when(series.size()).thenReturn(3);
when(series.getX(0)).thenReturn(0);
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java
index 647882c5..b1600828 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java
@@ -24,7 +24,6 @@
import org.junit.*;
import org.mockito.*;
-import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
/**
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java
new file mode 100644
index 00000000..08cc1480
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * 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.androidplot.xy;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.androidplot.test.AndroidplotTest;
+import com.androidplot.ui.RenderStack;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.List;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class BarRendererTest extends AndroidplotTest {
+
+ XYPlot xyPlot;
+
+ Canvas canvas;
+
+ RectF plotArea = new RectF(0, 0, 100, 100);
+
+ BarFormatter barFormatter;
+
+ @Mock
+ RenderStack renderStack;
+
+ @Captor
+ ArgumentCaptor barCaptor;
+
+ @Captor
+ ArgumentCaptor rectCaptor;
+
+ @Before
+ public void setUp() {
+ canvas = spy(new Canvas());
+ xyPlot = spy(new XYPlot(getContext(), "My Plot"));
+ barFormatter = spy(new BarFormatter(Color.RED, Color.RED));
+ }
+
+ @Test
+ public void onRender_handlesNullValues() {
+ XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", null, 5, null);
+ XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", null, 5, null);
+
+ BarRenderer renderer = setupRendererForTesting(s1, s2);
+
+ xyPlot.setRangeBoundaries(0, 10, BoundaryMode.FIXED);
+ xyPlot.calculateMinMaxVals();
+ renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack);
+
+ verify(renderer, times(6))
+ .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture());
+ }
+
+ @Test
+ public void onRender_stacked() {
+ XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 2, 5, 7);
+ XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 8, 5, 3);
+
+ BarRenderer renderer = setupRendererForTesting(s1, s2);
+
+ renderer.setBarOrientation(BarRenderer.BarOrientation.STACKED);
+
+ xyPlot.setRangeBoundaries(0, 10, BoundaryMode.FIXED);
+ xyPlot.calculateMinMaxVals();
+ renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack);
+
+ // make sure the expected number of bars draws were attempted:
+ verify(renderer, times(s1.size() + s2.size())).
+ drawBar(eq(canvas), any(BarRenderer.Bar.class), any(RectF.class));
+
+ // s1[0]
+ verifyBarHeight(80, 100, barFormatter, 1);
+
+ // s2[0]
+ verifyBarHeight(0, 80, barFormatter, 1);
+
+ // s1[1]
+ verifyBarHeight(50, 100, barFormatter, 1);
+
+ // s2[1]
+ verifyBarHeight(0, 50, barFormatter, 1);
+
+ // s1[2]
+ verifyBarHeight(30, 100, barFormatter, 1);
+
+ // s2[2]
+ verifyBarHeight(0, 30, barFormatter, 1);
+ }
+
+ @Test
+ public void onRender_sideBySide() {
+ XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10);
+ XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10);
+
+ BarRenderer renderer = setupRendererForTesting(s1, s2);
+ renderer.setBarOrientation(BarRenderer.BarOrientation.SIDE_BY_SIDE);
+
+ xyPlot.calculateMinMaxVals();
+ renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack);
+
+ // make sure the expected number of bars draws were attempted:
+ verify(renderer, times(s1.size() + s2.size())).
+ drawBar(eq(canvas), any(BarRenderer.Bar.class), any(RectF.class));
+
+ // s1[0] has zero height so should not be drawn:
+ verifyBarHeight(100, 100, barFormatter, 0);
+
+ // s1[1]:
+ verifyBarHeight(50, 100, barFormatter, 1);
+
+ // s1[2] & s2[2]:
+ verifyBarHeight(0, 100, barFormatter, 2);
+
+ // s2[0]:
+ verifyBarHeight(90, 100, barFormatter, 1);
+
+ // s2[1]:
+ verifyBarHeight(25, 100, barFormatter, 1);
+ }
+
+ @Test
+ public void onRender_inOrder_drawsFirstAddedSeriesFirst() {
+ XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 1, 5, 6);
+ XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 2, 4, 7);
+
+ BarRenderer renderer = setupRendererForTesting(s1, s2);
+ renderer.setBarOrientation(BarRenderer.BarOrientation.IN_ORDER);
+
+ xyPlot.setUserRangeOrigin(0);
+ xyPlot.calculateMinMaxVals();
+ renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack);
+
+ verify(renderer, times(6))
+ .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture());
+
+ // list of all bars drawn, in the exact order they were drawn.
+ List bars = barCaptor.getAllValues();
+
+ assertEquals(bars.get(0).getY(), s1.getY(0));
+ assertEquals(bars.get(1).getY(), s2.getY(0));
+
+ assertEquals(bars.get(2).getY(), s1.getY(1));
+ assertEquals(bars.get(3).getY(), s2.getY(1));
+
+ assertEquals(bars.get(4).getY(), s1.getY(2));
+ assertEquals(bars.get(5).getY(), s2.getY(2));
+ }
+
+ @Test
+ public void onRender_overlaid_drawsBarsWithExpectedHeight() {
+ XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10);
+ XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10);
+
+ BarRenderer renderer = setupRendererForTesting(s1, s2);
+ renderer.setBarOrientation(BarRenderer.BarOrientation.OVERLAID);
+
+ xyPlot.calculateMinMaxVals();
+ renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack);
+
+ // make sure the expected number of bars draws were attempted:
+ verify(renderer, times(s1.size() + s2.size())).
+ drawBar(eq(canvas), any(BarRenderer.Bar.class), any(RectF.class));
+
+ // s1[0] has zero height so should not be drawn:
+ verifyBarHeight(100, 100, barFormatter, 0);
+
+ // s1[1]:
+ verifyBarHeight(50, 100, barFormatter, 1);
+
+ // s1[2] & s2[2]:
+ verifyBarHeight(0, 100, barFormatter, 2);
+
+ // s2[0]:
+ verifyBarHeight(90, 100, barFormatter, 1);
+
+ // s2[1]:
+ verifyBarHeight(25, 100, barFormatter, 1);
+ }
+
+ /**
+ * Verify that positive values are drawn in order of highest yVal, while negative values are
+ * drawn in order of lowest yVal.
+ */
+ @Test
+ public void onRender_overlaid_drawsSmallestBarsLast() {
+ XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", -1, -2, 1, 2);
+ XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", -2, -1, 2, 1);
+
+ BarRenderer renderer = setupRendererForTesting(s1, s2);
+ renderer.setBarOrientation(BarRenderer.BarOrientation.OVERLAID);
+
+ xyPlot.setUserRangeOrigin(0);
+ xyPlot.calculateMinMaxVals();
+ renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack);
+
+ verify(renderer, times(8))
+ .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture());
+
+ // list of all bars drawn, in the exact order they were drawn.
+ List bars = barCaptor.getAllValues();
+
+ assertEquals(s2.getY(0), bars.get(0).getY());
+ assertEquals(s1.getY(0), bars.get(1).getY());
+
+ assertEquals(s1.getY(1), bars.get(2).getY());
+ assertEquals(s2.getY(1), bars.get(3).getY());
+ }
+
+ @Test
+ public void onRender_fixedBarWidth_rendersAllBarsWithSameWidth() {
+ XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10);
+ XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10);
+
+ BarRenderer renderer = setupRendererForTesting(s1, s2);
+ final float barWidth = 10;
+ renderer.setBarGroupWidth(BarRenderer.BarGroupWidthMode.FIXED_WIDTH, barWidth);
+
+ xyPlot.calculateMinMaxVals();
+ renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack);
+
+ verify(renderer, times(6))
+ .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture());
+
+ List barRects = rectCaptor.getAllValues();
+
+ for(RectF rect : barRects) {
+ assertEquals(barWidth, rect.width());
+ }
+ }
+
+ @Test
+ public void onRender_fixedGapWidth_rendersFixedGapBetweenAllBars() {
+ XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10);
+ XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10);
+
+ BarRenderer renderer = setupRendererForTesting(s1, s2);
+
+ final float gap = 5;
+ renderer.setBarGroupWidth(BarRenderer.BarGroupWidthMode.FIXED_GAP, gap);
+
+ xyPlot.calculateMinMaxVals();
+ renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack);
+
+ verify(renderer, times(6))
+ .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture());
+
+ List barRects = rectCaptor.getAllValues();
+
+ // verify that the spacing between each bar group is exactly what was set:
+ assertEquals(gap, barRects.get(2).left - barRects.get(0).right);
+ assertEquals(gap, barRects.get(3).left - barRects.get(1).right);
+
+ assertEquals(gap, barRects.get(4).left - barRects.get(2).right);
+ assertEquals(gap, barRects.get(5).left - barRects.get(3).right);
+ }
+
+ private void verifyBarHeight(float top, float bottom, BarFormatter formatter, int times) {
+ final Paint borderPaint = formatter.getBorderPaint();
+ verify(canvas, times(times)).drawRect(
+ anyFloat(),
+ eq(top),
+ anyFloat(),
+ eq(bottom),
+ eq(borderPaint));
+
+ final Paint fillPaint = formatter.getFillPaint();
+ verify(canvas, times(times)).drawRect(
+ anyFloat(),
+ eq(top),
+ anyFloat(),
+ eq(bottom),
+ eq(fillPaint));
+ }
+
+ protected BarRenderer setupRendererForTesting(XYSeries... series) {
+ BarRenderer renderer = spy((BarRenderer)barFormatter.getRendererInstance(xyPlot));
+ renderer.setBarOrientation(BarRenderer.BarOrientation.OVERLAID);
+
+ for(XYSeries s : series) {
+ xyPlot.addSeries(s, barFormatter);
+ }
+
+ doReturn(renderer.getClass()).when(barFormatter).getRendererClass();
+ return renderer;
+ }
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java
index cd0ff2b1..94a0be50 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java
@@ -24,7 +24,6 @@
import org.junit.*;
import org.mockito.*;
-import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
/**
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java
index a7fba775..8eda23b9 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java
@@ -24,7 +24,6 @@
import org.junit.*;
import org.mockito.*;
-import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
public class CandlestickRendererTest extends AndroidplotTest {
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/CatmullRomInterpolatorTest.java b/androidplot-core/src/test/java/com/androidplot/xy/CatmullRomInterpolatorTest.java
new file mode 100644
index 00000000..1d672daf
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/xy/CatmullRomInterpolatorTest.java
@@ -0,0 +1,59 @@
+package com.androidplot.xy;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+public class CatmullRomInterpolatorTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void interpolate_invalidPointsPerSegment_throwsIllegalArgumentException() {
+ final XYSeries series = mock(XYSeries.class);
+ final CatmullRomInterpolator.Params params =
+ new CatmullRomInterpolator.Params(1, CatmullRomInterpolator.Type.Centripetal);
+
+ new CatmullRomInterpolator().interpolate(series, params);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void interpolate_twoElementSeries_throwsIllegalArgumentException() {
+ final XYSeries series = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "test", 1, 2);
+ final CatmullRomInterpolator.Params params =
+ new CatmullRomInterpolator.Params(2, CatmullRomInterpolator.Type.Centripetal);
+
+ new CatmullRomInterpolator().interpolate(series, params);
+ }
+
+ @Test
+ public void interpolate_threeElementSeriesAndThreePointsPerSegment_producesFivePoints() {
+ final XYSeries series = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "test", 1, 2, 3);
+ final CatmullRomInterpolator.Params params =
+ new CatmullRomInterpolator.Params(3, CatmullRomInterpolator.Type.Centripetal);
+ final List interpolated = new CatmullRomInterpolator().interpolate(series, params);
+
+ assertEquals(5, interpolated.size());
+
+ // control points should exactly match input:
+ assertEquals(1, interpolated.get(0).y);
+ assertEquals(2, interpolated.get(2).y);
+ assertEquals(3, interpolated.get(4).y);
+ }
+
+ @Test
+ public void interpolate_threeElementSeriesAndFourPointsPerSegment_producesSavenPoints() {
+ final XYSeries series = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "test", 1, 2, 3);
+ final CatmullRomInterpolator.Params params =
+ new CatmullRomInterpolator.Params(4, CatmullRomInterpolator.Type.Centripetal);
+ final List interpolated = new CatmullRomInterpolator().interpolate(series, params);
+
+ assertEquals(7, interpolated.size());
+
+ // control points should exactly match input:
+ assertEquals(1, interpolated.get(0).y);
+ assertEquals(2, interpolated.get(3).y);
+ assertEquals(3, interpolated.get(6).y);
+ }
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java
index de81e70c..cf083d9e 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java
@@ -24,8 +24,8 @@
import org.junit.runner.*;
import org.mockito.*;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java
index 3269082d..800992f9 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java
@@ -27,7 +27,6 @@
import java.util.*;
import static junit.framework.Assert.assertEquals;
-import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
public class LTTBSamplerTest extends AndroidplotTest {
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java
index e42eef65..44a7b22d 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java
@@ -21,13 +21,11 @@
import com.androidplot.test.*;
import org.junit.*;
-import org.junit.runner.*;
import org.mockito.*;
import java.util.*;
import static junit.framework.Assert.assertEquals;
-import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
public class LineAndPointRendererTest extends AndroidplotTest {
@@ -60,12 +58,10 @@ public void testDrawSeries_withInterpolation() throws Exception {
* @throws Exception
*/
@Test
- public void testRenderPoints() throws Exception {
+ public void testDrawSeries() throws Exception {
// 100x100 plot space:
- RectF plotArea = new RectF(0, 0, 99, 99);
- XYPlot plot = new XYPlot(getContext(), "Test");
-
+ plotArea = new RectF(0, 0, 99, 99);
FastLineAndPointRenderer.Formatter formatter =
new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null);
@@ -73,12 +69,12 @@ public void testRenderPoints() throws Exception {
XYSeries series = new SimpleXYSeries(
SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
- LineAndPointRenderer renderer = Mockito.spy(new LineAndPointRenderer(plot));
+ LineAndPointRenderer renderer = Mockito.spy(new LineAndPointRenderer(xyPlot));
- plot.addSeries(series, formatter);
+ xyPlot.addSeries(series, formatter);
- plot.calculateMinMaxVals();
- renderer.onRender(canvas, plotArea, series, formatter, null);
+ xyPlot.calculateMinMaxVals();
+ renderer.drawSeries(canvas, plotArea, series, formatter);
PointF[] expectedPoints = new PointF[] {
new PointF(0, 99),
@@ -98,6 +94,8 @@ public void testRenderPoints() throws Exception {
eq(canvas),
eq(plotArea),
eq(series),
+ eq(0),
+ eq(expectedPoints.length),
capturedPoints.capture(),
eq(formatter));
@@ -143,4 +141,121 @@ public void testRenderPoints() throws Exception {
assertEquals(expectedPoints[9].x, pList.get(9).x);
assertEquals(expectedPoints[9].y, pList.get(9).y);
}
+
+ @Test
+ public void testDrawSeries_supportsOrderedXYSeries() throws Exception {
+ // 100x100 plot space:
+ //RectF plotArea = new RectF(0, 0, 99, 99);
+ FastLineAndPointRenderer.Formatter formatter =
+ new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null);
+
+ // create a series composed of 3 "segments"; series portions separated by null values:
+ SimpleXYSeries series = new SimpleXYSeries(
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+
+ LineAndPointRenderer renderer = Mockito.spy(new LineAndPointRenderer(xyPlot));
+
+ xyPlot.addSeries(series, formatter);
+
+ xyPlot.calculateMinMaxVals();
+ renderer.drawSeries(canvas, plotArea, series, formatter);
+
+ verify(renderer, times(1)).renderPoints(
+ eq(canvas),
+ eq(plotArea),
+ eq(series),
+ eq(0),
+ eq(series.size()),
+ any(List.class),
+ eq(formatter));
+
+ xyPlot.setDomainBoundaries(5, 6, BoundaryMode.FIXED);
+ series.setXOrder(OrderedXYSeries.XOrder.ASCENDING);
+ xyPlot.calculateMinMaxVals();
+ renderer.drawSeries(canvas, plotArea, series, formatter);
+
+ verify(renderer, times(1)).renderPoints(
+ eq(canvas),
+ eq(plotArea),
+ eq(series),
+ eq(4),
+ eq(8),
+ any(List.class),
+ eq(formatter));
+
+ }
+
+ @Test
+ public void testCullPointsCache() throws Exception {
+ LineAndPointFormatter formatter =
+ new LineAndPointFormatter(0, 0, 0, null);
+
+ // create a series composed of 3 "segments"; series portions separated by null values:
+ SimpleXYSeries series = new SimpleXYSeries(
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+
+ xyPlot.addSeries(series, formatter);
+ LineAndPointRenderer renderer = xyPlot.getRenderer(LineAndPointRenderer.class);
+
+ assertEquals(0, renderer.pointsCaches.size());
+
+ // should generate a new pointCache:
+ renderer.getPointsCache(series);
+ assertEquals(1, renderer.pointsCaches.size());
+
+ // culling should not delete it since it is
+ // registered in the series registry:
+ renderer.getPointsCache(series);
+ assertEquals(1, renderer.pointsCaches.size());
+ renderer.cullPointsCache();
+ assertEquals(1, renderer.pointsCaches.size());
+
+ // unregister the series. this time, culling should remove the series
+ // from the points cache:
+ xyPlot.removeSeries(series);
+ renderer.cullPointsCache();
+ assertEquals(0, renderer.pointsCaches.size());
+ }
+
+ @Test
+ public void renderPath_rendersRegions() {
+ LineAndPointFormatter formatter =
+ new LineAndPointFormatter(0, 0, 0, null);
+
+ XYRegionFormatter r1 = new XYRegionFormatter(Color.RED);
+ formatter.addRegion(new RectRegion(0, 2, 0, 2, "region1"), r1);
+
+ XYRegionFormatter r2 = new XYRegionFormatter(Color.GREEN);
+ formatter.addRegion(new RectRegion(0, 2, 0, 2, "region2"), r2);
+
+ SimpleXYSeries series = new SimpleXYSeries(
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+
+ xyPlot.addSeries(series, formatter);
+ LineAndPointRenderer renderer = xyPlot.getRenderer(LineAndPointRenderer.class);
+
+ //xyPlot.draw(canvas);
+ renderer.renderPath(canvas, plotArea, new Path(), mock(PointF.class), mock(PointF.class), formatter);
+ verify(canvas).drawRect(any(RectF.class), eq(r1.getPaint()));
+ verify(canvas).drawRect(any(RectF.class), eq(r2.getPaint()));
+ }
+
+ @Test
+ public void drawSeries_withPointLabelFormatter_drawsPointLabels() {
+ LineAndPointFormatter formatter =
+ new LineAndPointFormatter(0, 0, 0, null);
+ formatter.setPointLabelFormatter(new PointLabelFormatter(Color.RED));
+ SimpleXYSeries series = new SimpleXYSeries(
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+ xyPlot.addSeries(series, formatter);
+ LineAndPointRenderer renderer = xyPlot.getRenderer(LineAndPointRenderer.class);
+ renderer.drawSeries(canvas, plotArea, series, formatter);
+
+ verify(canvas, times(series.size())).drawText(
+ anyString(),
+ anyFloat(),
+ anyFloat(),
+ eq(formatter.getPointLabelFormatter().getTextPaint()));
+
+ }
}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/NormedXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/NormedXYSeriesTest.java
new file mode 100644
index 00000000..8e8b341d
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/xy/NormedXYSeriesTest.java
@@ -0,0 +1,87 @@
+package com.androidplot.xy;
+
+import com.androidplot.test.AndroidplotTest;
+import com.androidplot.test.TestUtils;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.Mockito.spy;
+
+/**
+ * Tests {@link NormedXYSeries}.
+ */
+public class NormedXYSeriesTest extends AndroidplotTest {
+
+ // account for precision issues inherent in floating point math:
+ private static final double DELTA = 0.0000001;
+
+ @Test
+ public void testConstructor_withNoOffset() {
+ XYSeries rawData = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 2, 4, 6, 8, 10);
+ NormedXYSeries normedData = new NormedXYSeries(rawData,
+ new NormedXYSeries.Norm(null),
+ new NormedXYSeries.Norm(null));
+
+ assertEquals(0d, normedData.getY(0).doubleValue(), DELTA);
+ assertEquals(0.2d, normedData.getY(1).doubleValue(), DELTA);
+ assertEquals(0.4d, normedData.getY(2).doubleValue(), DELTA);
+ assertEquals(0.6d, normedData.getY(3).doubleValue(), DELTA);
+ assertEquals(0.8d, normedData.getY(4).doubleValue(), DELTA);
+ assertEquals(1.0d, normedData.getY(5).doubleValue(), DELTA);
+ }
+
+ @Test
+ public void testConstructor_withNullYVals() {
+ XYSeries rawData = new SimpleXYSeries(
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY,
+ "s1",
+ 0, null, 4, null, 8, 10);
+ NormedXYSeries normedData = new NormedXYSeries(rawData,
+ new NormedXYSeries.Norm(null),
+ new NormedXYSeries.Norm(null));
+
+ assertEquals(0d, normedData.getY(0).doubleValue(), DELTA);
+ assertEquals(0.4d, normedData.getY(2).doubleValue(), DELTA);
+ assertEquals(0.8d, normedData.getY(4).doubleValue(), DELTA);
+ assertEquals(1.0d, normedData.getY(5).doubleValue(), DELTA);
+ }
+
+ @Test
+ public void testConstructor_withPositiveOffsetAndOffsetCompression() {
+ XYSeries rawData = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 2, 4, 6, 8, 10);
+ NormedXYSeries normedData = new NormedXYSeries(rawData,
+ new NormedXYSeries.Norm(null, 0.5, true),
+ new NormedXYSeries.Norm(null, 0.5, true));
+
+ assertEquals(0.5d, normedData.getY(0).doubleValue(), DELTA);
+ assertEquals(0.6d, normedData.getY(1).doubleValue(), DELTA);
+ assertEquals(1.0d, normedData.getY(5).doubleValue(), DELTA);
+ }
+
+ @Test
+ public void testConstructor_withNegativeOffsetAndOffsetCompression() {
+ XYSeries rawData = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 2, 4, 6, 8, 10);
+ NormedXYSeries normedData = new NormedXYSeries(rawData,
+ new NormedXYSeries.Norm(null, -0.5, true),
+ new NormedXYSeries.Norm(null, -0.5, true));
+
+ assertEquals(0d, normedData.getY(0).doubleValue(), DELTA);
+ assertEquals(0.1d, normedData.getY(1).doubleValue(), DELTA);
+ assertEquals(0.2d, normedData.getY(2).doubleValue(), DELTA);
+ assertEquals(0.5d, normedData.getY(5).doubleValue(), DELTA);
+ }
+
+ @Test
+ public void testConstructor_withOffsetAndNoOffsetCompression() {
+ XYSeries rawData = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 2, 4, 6, 8, 10);
+ NormedXYSeries normedData = new NormedXYSeries(rawData,
+ new NormedXYSeries.Norm(null, 0.5, false),
+ new NormedXYSeries.Norm(null, 0.5, false));
+
+ assertEquals(0.5d, normedData.getY(0).doubleValue(), DELTA);
+ assertEquals(0.7d, normedData.getY(1).doubleValue(), DELTA);
+ assertEquals(1.5d, normedData.getY(5).doubleValue(), DELTA);
+ }
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java
index 6bb7fb20..50d3c16c 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java
@@ -178,6 +178,61 @@ public void testZoom() {
}
+ @Test
+ public void testLimitZoom() {
+ double[] inc_domain = new double[]{10,50,100};
+ double[] inc_range = new double[]{20,50};
+
+ xyPlot = spy(new InstrumentedXYPlot(getContext()));
+ xyPlot.setDomainBoundaries(0, 20, BoundaryMode.FIXED);
+ xyPlot.setRangeBoundaries(0, 30, BoundaryMode.FIXED);
+ xyPlot.setDomainStepModel(new StepModelFit(xyPlot.getBounds().getxRegion(), inc_domain, 5));
+ xyPlot.setRangeStepModel(new StepModelFit(xyPlot.getBounds().getyRegion(), inc_range, 5));
+ xyPlot.redraw();
+
+ PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE, PanZoom.ZoomLimit.MIN_TICKS));
+
+ // cap our pan/zoom boundaries:
+ xyPlot.getOuterLimits().set(0, 20, 0, 30);
+
+ panZoom.setFingersRect(new RectF(0, 0, 20, 20));
+
+ InOrder inOrder = inOrder(xyPlot);
+ inOrder.verify(xyPlot).setDomainBoundaries(0, 20, BoundaryMode.FIXED);
+
+ // should NOT result in a 2x zoom on domain centerpoint, but in a zoom to
+ // the minimum spacing 10 and 20 respectively
+ panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 40, 40));
+ inOrder.verify(xyPlot).setDomainBoundaries(5f, 15f, BoundaryMode.FIXED);
+ inOrder.verify(xyPlot).setRangeBoundaries(5f, 25f, BoundaryMode.FIXED);
+ inOrder.verify(xyPlot).redraw();
+
+ // to zoom in beyond min limits
+ panZoom.setZoomLimit(PanZoom.ZoomLimit.OUTER);
+
+ // should result in another 2x zoom on domain centerpoint:
+ panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 80, 80));
+ inOrder.verify(xyPlot).setDomainBoundaries(7.5f, 12.5f, BoundaryMode.FIXED);
+ inOrder.verify(xyPlot).setRangeBoundaries(10f, 20f, BoundaryMode.FIXED);
+ inOrder.verify(xyPlot).redraw();
+
+ // back to limited zoom
+ panZoom.setZoomLimit(PanZoom.ZoomLimit.MIN_TICKS);
+
+ // try to zoom in further, should snap back to min limit:
+ panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 90, 90));
+ inOrder.verify(xyPlot).setDomainBoundaries(5f, 15f, BoundaryMode.FIXED);
+ inOrder.verify(xyPlot).setRangeBoundaries(5f, 25f, BoundaryMode.FIXED);
+ inOrder.verify(xyPlot).redraw();
+
+ // redraw should not be called again
+ inOrder.verify(xyPlot, never()).redraw();
+
+ // make sure no panning took place during these zoom ops:
+ verify(panZoom, never()).pan(any(MotionEvent.class));
+
+ }
+
@Test
public void testFingerDistance() {
PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE));
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java
index 70a9bbe1..c6e11ae7 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java
@@ -21,7 +21,6 @@
import com.androidplot.test.AndroidplotTest;
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
@@ -196,55 +195,4 @@ public void testUnion() throws Exception {
assertEquals(0, r2.getMinY().doubleValue(), 0);
assertEquals(200, r2.getMaxY().doubleValue(), 0);
}
-
- /**
- * Currently ignored as the base implementation does not currently pass.
- */
- @Ignore
- @Test
- public void testOverlapsLine() {
- RectRegion r1 = new RectRegion(0, 100, 0, 100);
-
-// assertFalse(r1.intersectsWithLine(200, 200, 400, 400));
-// assertFalse(r1.intersectsWithLine(-1, -1, -1, 100));
-// assertTrue(r1.intersectsWithLine(0, 0, 100, 100));
-// assertTrue(r1.intersectsWithLine(50, 50, 200, 200));
-
- // lines running parallel to region edges:
- assertTrue(r1.intersectsWithLine(0, 0, 0, 100));
- assertTrue(r1.intersectsWithLine(0, 0, 100, 0));
- assertTrue(r1.intersectsWithLine(0, 100, 0, 0));
- assertTrue(r1.intersectsWithLine(100, 0, 0, 0));
-
- assertTrue(r1.intersectsWithLine(100, 0, 0, 0));
- assertTrue(r1.intersectsWithLine(0, 100, 0, 0));
- assertTrue(r1.intersectsWithLine(0, 0, 100, 0));
- assertTrue(r1.intersectsWithLine(0, 0, 0, 100));
-
- // lines passing through top & bottom edges only:
- assertTrue(r1.intersectsWithLine(50, -1000, 50, 1000));
- assertTrue(r1.intersectsWithLine(50, 1000, 50, -1000));
-
- // lines passing through left & right edges only:
- assertTrue(r1.intersectsWithLine(-1000, 50, 1000, 50));
- assertTrue(r1.intersectsWithLine(1000, 50, -100, 50));
-
- // diagonal passing through bottom-left and top-right corners:
- assertTrue(r1.intersectsWithLine(-100, -100, 200, 200));
-
- // diagonal passing inside upper-left edge
- assertTrue(r1.intersectsWithLine(-20, 80, 20, 120));
- assertTrue(r1.intersectsWithLine(20, 120, -20, 80));
-
- // diagonal passing outside upper-left edge
-
- // diagonal passing inside lower-left edge
- // diagonal passing outside lower-left edge
-
- // diagonal passing inside upper-right edge
- // diagonal passing outside upper-right edge
-
- // diagonal passing inside lower-right edge
- // diagonal passing outside lower-right edge
- }
}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java
index df07b4f5..95cea342 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java
@@ -9,7 +9,7 @@
import static org.mockito.Mockito.verify;
/**
- * Created by halfhp on 10/8/16.
+ * Tests {@link SampledXYSeries}
*/
public class SampledXYSeriesTest extends AndroidplotTest {
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/ScalingXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/ScalingXYSeriesTest.java
new file mode 100644
index 00000000..90756428
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/xy/ScalingXYSeriesTest.java
@@ -0,0 +1,58 @@
+package com.androidplot.xy;
+
+import com.androidplot.test.AndroidplotTest;
+
+import org.junit.Test;
+
+import static junit.framework.Assert.assertEquals;
+
+/**
+ * Tests {@link ScalingXYSeries}.
+ */
+public class ScalingXYSeriesTest extends AndroidplotTest {
+
+ @Test
+ public void testScale_yOnly() {
+ SimpleXYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10);
+ ScalingXYSeries scaled1 = new ScalingXYSeries(s1, 0.5, ScalingXYSeries.Mode.Y_ONLY);
+
+ assertEquals(0d, scaled1.getX(0).doubleValue());
+ assertEquals(0d, scaled1.getY(0).doubleValue());
+
+ assertEquals(1d, scaled1.getX(1).doubleValue());
+ assertEquals(2.5d, scaled1.getY(1).doubleValue());
+
+ assertEquals(2d, scaled1.getX(2).doubleValue());
+ assertEquals(5d, scaled1.getY(2).doubleValue());
+ }
+
+ @Test
+ public void testScale_xOnly() {
+ SimpleXYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10);
+ ScalingXYSeries scaled1 = new ScalingXYSeries(s1, 0.5, ScalingXYSeries.Mode.X_ONLY);
+
+ assertEquals(0d, scaled1.getX(0).doubleValue());
+ assertEquals(0d, scaled1.getY(0).doubleValue());
+
+ assertEquals(0.5d, scaled1.getX(1).doubleValue());
+ assertEquals(5d, scaled1.getY(1).doubleValue());
+
+ assertEquals(1d, scaled1.getX(2).doubleValue());
+ assertEquals(10d, scaled1.getY(2).doubleValue());
+ }
+
+ @Test
+ public void testScale_xAndY() {
+ SimpleXYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10);
+ ScalingXYSeries scaled1 = new ScalingXYSeries(s1, 0.5, ScalingXYSeries.Mode.X_AND_Y);
+
+ assertEquals(0d, scaled1.getX(0).doubleValue());
+ assertEquals(0d, scaled1.getY(0).doubleValue());
+
+ assertEquals(0.5d, scaled1.getX(1).doubleValue());
+ assertEquals(2.5d, scaled1.getY(1).doubleValue());
+
+ assertEquals(1d, scaled1.getX(2).doubleValue());
+ assertEquals(5d, scaled1.getY(2).doubleValue());
+ }
+}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java
index 397e7700..dc21ab7c 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java
@@ -19,13 +19,14 @@
import org.junit.Test;
import java.util.Arrays;
+import java.util.NoSuchElementException;
import static junit.framework.Assert.assertEquals;
public class SimpleXYSeriesTest {
@Test
- public void testYValsOnlyConstructor() throws Exception {
+ public void constructor_yValsOnly() throws Exception {
Number[] yVals = {5, 6, 7, 8, 9};
SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "test");
@@ -43,7 +44,7 @@ public void testYValsOnlyConstructor() throws Exception {
}
@Test
- public void testXYInterleavedConstructor() throws Exception {
+ public void constructor_xyInterleaved() throws Exception {
Number[] yVals = {55, 5, 66, 6, 77, 7, 88, 8, 99, 9};
SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "test");
@@ -61,7 +62,7 @@ public void testXYInterleavedConstructor() throws Exception {
}
@Test
- public void testTwoListConstructor() throws Exception {
+ public void constructor_xAndYLists() throws Exception {
Number[] yVals = {5, 6, 7, 8, 9};
Number[] xVals = {1, 2, 3, 4, 5};
SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(xVals), Arrays.asList(yVals), "test");
@@ -80,7 +81,7 @@ public void testTwoListConstructor() throws Exception {
}
@Test
- public void testPushPopStuff() throws Exception {
+ public void addRemove_modifiesSeries() throws Exception {
Number[] yVals = {5, 6, 7, 8, 9};
Number[] xVals = {1, 2, 3, 4, 5};
SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(xVals), Arrays.asList(yVals), "test");
@@ -107,7 +108,7 @@ public void testPushPopStuff() throws Exception {
}
@Test
- public void testSet() throws Exception {
+ public void set_setsExpectedValue() throws Exception {
Number[] yVals = {5, 6, 7, 8, 9};
Number[] xVals = {1, 2, 3, 4, 5};
SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(xVals), Arrays.asList(yVals), "test");
@@ -128,7 +129,7 @@ public void testSet() throws Exception {
}
@Test
- public void testResize() throws Exception {
+ public void resize_emptySeries_resizesSeries() {
SimpleXYSeries series = new SimpleXYSeries("series");
series.resize(10);
assertEquals(10, series.size());
@@ -146,4 +147,59 @@ public void testResize() throws Exception {
assertEquals(0, series.size());
}
+ @Test
+ public void resize_yValsOnly_resizesSeries() {
+ SimpleXYSeries series = new SimpleXYSeries(
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "", 1, 2, 3);
+ series.resize(10);
+ assertEquals(10, series.size());
+ }
+
+ @Test
+ public void setXY_modifiesSeries() {
+ SimpleXYSeries series = new SimpleXYSeries("series");
+ series.resize(5);
+ series.setXY(100, 200, 0);
+
+ assertEquals(100, series.getX(0));
+ assertEquals(200, series.getY(0));
+
+ }
+
+ @Test
+ public void setX_yValsOnly_changesValue() {
+ SimpleXYSeries series = new SimpleXYSeries(
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "", 1, 2, 3);
+ series.setX(9, 0);
+ assertEquals(9, series.getX(0));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void removeFirst_throwsNoSuchElementException_ifEmpty() {
+ new SimpleXYSeries("series").removeFirst();
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void removeLast_throwsNoSuchElementException_ifEmpty() {
+ new SimpleXYSeries("series").removeLast();
+ }
+
+ @Test
+ public void setTitle_changesTitle() {
+ SimpleXYSeries series = new SimpleXYSeries("series");
+
+ final String newTitle = "newTitle";
+ series.setTitle(newTitle);
+ assertEquals(newTitle, series.getTitle());
+ }
+
+ @Test
+ public void clear_removesEverything() {
+ SimpleXYSeries series = new SimpleXYSeries(
+ Arrays.asList(1, 2, 3, 4, 5),
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "series");
+
+ series.clear();
+ assertEquals(0, series.size());
+ }
}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/StepModelFitTest.java b/androidplot-core/src/test/java/com/androidplot/xy/StepModelFitTest.java
new file mode 100644
index 00000000..5f34fd52
--- /dev/null
+++ b/androidplot-core/src/test/java/com/androidplot/xy/StepModelFitTest.java
@@ -0,0 +1,61 @@
+package com.androidplot.xy;
+
+import com.androidplot.Region;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class StepModelFitTest {
+
+ Region regionSmall = new Region(0,11);
+ Region regionBig = new Region(-111,420);
+ Region regionZero = new Region(0, 0);
+ Region regionUndef = new Region(0, null);
+
+ double[] stpSmall = {1,2,5}, stpBig = {1,10,100}, nonsense = {0};
+
+ @Before
+ public void setUp() throws Exception {
+
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ @Test
+ public void getValue() throws Exception {
+
+ StepModelFit model = new StepModelFit(regionSmall,stpSmall,3);
+
+ assertEquals(5.0, model.getValue(), 0.0);
+ model.setValue(5.0);
+ assertEquals(2.0, model.getValue(), 0.0);
+ model.setValue(7.0);
+ assertEquals(2.0, model.getValue(), 0.0);
+
+ model.setSteps(stpBig);
+ assertEquals(1.0, model.getValue(), 0.0);
+
+ model.setScale(regionBig);
+ assertEquals(100.0, model.getValue(), 0.0);
+ model.setValue(1000.0);
+ assertEquals(1.0, model.getValue(), 0.0);
+
+ // bad parameters
+ model.setSteps(nonsense);
+ assertArrayEquals(stpBig,model.getSteps(), 0.0);
+
+ model.setScale(regionZero);
+ assertEquals(stpBig[0], model.getValue(), 0.0);
+
+ model.setScale(regionUndef);
+ model.setValue(1.1);
+ assertEquals(1.1, model.getValue(), 0.0);
+ }
+
+}
\ No newline at end of file
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java
index f0e0c76e..a9182501 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java
@@ -25,12 +25,14 @@
import org.junit.*;
import org.mockito.*;
+import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyFloat;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyDouble;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@@ -65,7 +67,7 @@ public class XYGraphWidgetTest extends AndroidplotTest {
@Before
- public void setUp() throws Exception {
+ public void setUp() {
size = spy(new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE));
xyPlot = spy(new XYPlot(getContext(), "XYPlot"));
when(xyPlot.getRegistry()).thenReturn(seriesRegistry);
@@ -77,17 +79,12 @@ public void setUp() throws Exception {
xyPlot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1);
graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size));
- graphWidget.setGridRect(new RectF(0, 0, 100, 100));
+ graphWidget.setGridRect(new RectF(0, 0, 10, 100));
graphWidget.setLabelRect(new RectF(0, 0, 100, 100));
}
- @After
- public void tearDown() throws Exception {
-
- }
-
@Test
- public void testProcessAttrs() throws Exception {
+ public void processAttrs_withDefaults_disablesGridClipping() {
XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size));
graphWidget.processAttrs(typedArray);
@@ -95,13 +92,15 @@ public void testProcessAttrs() throws Exception {
}
@Test
- public void testDoOnDraw_drawGridOnTopFalse() throws Exception {
+ public void doOnDraw_drawGridOnTopFalse_drawsGridAfterData() throws Exception {
XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size));
graphWidget.setDrawGridOnTop(false);
doNothing().when(graphWidget).drawGrid(canvas);
doNothing().when(graphWidget).drawData(canvas);
- graphWidget.doOnDraw(canvas, new RectF(0, 0, 100, 100));
+ final RectF canvasRect = new RectF(0, 0, 100, 100);
+ graphWidget.onResize(null, canvasRect);
+ graphWidget.doOnDraw(canvas, canvasRect);
InOrder io = inOrder(graphWidget);
io.verify(graphWidget).drawGrid(canvas);
@@ -109,13 +108,15 @@ public void testDoOnDraw_drawGridOnTopFalse() throws Exception {
}
@Test
- public void testDoOnDraw_drawGridOnTopTrue() throws Exception {
+ public void doOnDraw_drawGridOnTopTrue_drawsGridBeforeData() throws Exception {
XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size));
graphWidget.setDrawGridOnTop(true);
doNothing().when(graphWidget).drawGrid(canvas);
doNothing().when(graphWidget).drawData(canvas);
- graphWidget.doOnDraw(canvas, new RectF(0, 0, 100, 100));
+ final RectF canvasRect = new RectF(0, 0, 100, 100);
+ graphWidget.onResize(null, canvasRect);
+ graphWidget.doOnDraw(canvas, canvasRect);
InOrder io = inOrder(graphWidget);
io.verify(graphWidget).drawData(canvas);
@@ -123,7 +124,7 @@ public void testDoOnDraw_drawGridOnTopTrue() throws Exception {
}
@Test
- public void testDrawMarkers() throws Exception {
+ public void drawMarkers_drawsLineForEachMarker() {
xyPlot.addMarker(new XValueMarker(1, "x"));
xyPlot.addMarker(new YValueMarker(-1, "y"));
@@ -134,42 +135,22 @@ public void testDrawMarkers() throws Exception {
.drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), any(Paint.class));
}
- protected void runDrawGridTest() {
- doNothing().when(graphWidget).
- drawDomainLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean());
-
- doNothing().when(graphWidget).
- drawRangeLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean());
-
- xyPlot.setRangeBoundaries(0, 100, BoundaryMode.FIXED);
- xyPlot.setDomainBoundaries(0, 100, BoundaryMode.FIXED);
-
- graphWidget.drawGrid(canvas);
-
- // expecting a 100x100 grid to be drawn:
- verify(graphWidget, times(100))
- .drawDomainLine(eq(canvas), anyFloat(), anyFloat(), any(Paint.class), eq(false));
-
- verify(graphWidget, times(100))
- .drawRangeLine(eq(canvas), anyFloat(), anyFloat(), any(Paint.class), eq(false));
- }
-
@Test
- public void testDrawGrid_nullOrigin() throws Exception {
+ public void drawGrid_nullOrigin_drawsGrid() {
when(xyPlot.getDomainOrigin()).thenReturn(null);
when(xyPlot.getRangeOrigin()).thenReturn(null);
runDrawGridTest();
}
@Test
- public void testDrawGrid_zeroOrigin() throws Exception {
+ public void drawGrid_zeroOrigin_drawsGrid() {
when(xyPlot.getDomainOrigin()).thenReturn(0);
when(xyPlot.getRangeOrigin()).thenReturn(0);
runDrawGridTest();
}
@Test
- public void testDrawGrid_centeredOrigin() throws Exception {
+ public void drawGrid_centeredOrigin_drawsGrid() {
// set origin to midpoint so we exercise
// code to draw lines on both sides of the origin:
@@ -179,12 +160,14 @@ public void testDrawGrid_centeredOrigin() throws Exception {
}
@Test
- public void testDrawCursors_ifCursorPaintIsSet() throws Exception {
+ public void drawCursors_withCursorPaintAndPosition_drawsCursorLines() {
final Paint domainCursorPaint = new Paint();
graphWidget.setDomainCursorPaint(domainCursorPaint);
final Paint rangeCursorPaint = new Paint();
graphWidget.setRangeCursorPaint(rangeCursorPaint);
+ graphWidget.setRangeCursorPosition(0f);
+ graphWidget.setDomainCursorPosition(0f);
graphWidget.drawCursors(canvas);
// expect one line to be drawn for each cursor:
@@ -193,7 +176,7 @@ public void testDrawCursors_ifCursorPaintIsSet() throws Exception {
}
@Test
- public void testDrawCursors_ifCursorPaintIsNotSet() throws Exception {
+ public void testDrawCursors_noCursorPaintOrPosition_drawsNoCursorLines() {
graphWidget.setDomainCursorPaint(null);
graphWidget.setRangeCursorPaint(null);
@@ -204,7 +187,9 @@ public void testDrawCursors_ifCursorPaintIsNotSet() throws Exception {
}
@Test
- public void testDrawCursorLabel() throws Exception {
+ public void drawCursorLabel_drawsText() {
+ graphWidget.setDomainCursorPosition(0f);
+ graphWidget.setRangeCursorPosition(0f);
XYGraphWidget.CursorLabelFormatter clf = mock(XYGraphWidget.CursorLabelFormatter.class);
when(clf.getTextPaint()).thenReturn(new Paint());
when(clf.getLabelText(any(Number.class), any(Number.class))).thenReturn("bla");
@@ -214,8 +199,7 @@ public void testDrawCursorLabel() throws Exception {
}
@Test
- public void testSetLineLabelEdges() throws Exception {
-
+ public void setLineLabelEdges_setsEdges() {
graphWidget.setLineLabelEdges(XYGraphWidget.Edge.LEFT, XYGraphWidget.Edge.BOTTOM);
assertTrue(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.LEFT));
assertTrue(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.BOTTOM));
@@ -228,7 +212,7 @@ public void testSetLineLabelEdges() throws Exception {
}
@Test
- public void testSetLineLabelEdges_bitfield() throws Exception {
+ public void setLineLabelEdges_bitfield_setsEdges() {
graphWidget.setLineLabelEdges(
XYGraphWidget.Edge.TOP.getValue() | XYGraphWidget.Edge.RIGHT.getValue());
@@ -238,4 +222,108 @@ public void testSetLineLabelEdges_bitfield() throws Exception {
assertFalse(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.BOTTOM));
assertFalse(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.LEFT));
}
+
+ @Test
+ public void screenToSeries_returnsSeriesCoords() {
+ when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100));
+
+ XYCoords coords = graphWidget.screenToSeries(new PointF(0, 0));
+ assertEquals(-100, coords.x.intValue());
+ assertEquals(100, coords.y.intValue());
+
+ coords = graphWidget.screenToSeries(new PointF(10, 100));
+ assertEquals(100, coords.x.intValue());
+ assertEquals(-100, coords.y.intValue());
+
+ coords = graphWidget.screenToSeries(new PointF(5, 50));
+ assertEquals(0, coords.x.intValue());
+ assertEquals(0, coords.y.intValue());
+ }
+
+ @Test
+ public void seriesToScreen_returnsScreenPoint() {
+ when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100));
+
+ PointF point = graphWidget.seriesToScreen(new XYCoords(-100, 100));
+ assertEquals(0f, point.x);
+ assertEquals(0f, point.y);
+
+ point = graphWidget.seriesToScreen(new XYCoords(100, -100));
+ assertEquals(10f, point.x);
+ assertEquals(100f, point.y);
+
+ point = graphWidget.seriesToScreen(new XYCoords(0, 0));
+ assertEquals(5f, point.x);
+ assertEquals(50f, point.y);
+ }
+
+ @Test
+ public void screenToSeriesX_returnsSeriesValue() {
+ when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100));
+
+ assertEquals(-100, graphWidget.screenToSeriesX(new PointF(0, 0)).intValue());
+ assertEquals(100, graphWidget.screenToSeriesX(new PointF(10, 100)).intValue());
+ assertEquals(0, graphWidget.screenToSeriesX(new PointF(5, 50)).intValue());
+ }
+
+ @Test
+ public void screenToSeriesY_returnsSeriesValue() {
+ when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100));
+
+ assertEquals(100, graphWidget.screenToSeriesY(new PointF(0, 0)).intValue());
+ assertEquals(-100, graphWidget.screenToSeriesY(new PointF(100, 100)).intValue());
+ assertEquals(0, graphWidget.screenToSeriesY(new PointF(50, 50)).intValue());
+ }
+
+ @Test
+ public void seriesToScreenX_returnsScreenValue() {
+ when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100));
+
+ assertEquals(0f, graphWidget.seriesToScreenX(-100));
+ assertEquals(10f, graphWidget.seriesToScreenX(100));
+ assertEquals(5f, graphWidget.seriesToScreenX(0));
+ }
+
+ @Test
+ public void seriesToScreenY_returnsScreenValue() {
+ when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100));
+
+ assertEquals(100f, graphWidget.seriesToScreenY(100));
+ assertEquals(0f, graphWidget.seriesToScreenY(-100));
+ assertEquals(50f, graphWidget.seriesToScreenY(0));
+ }
+
+ @Test
+ public void setGridInsets_updatesGridRect() {
+ graphWidget.setGridInsets(new com.androidplot.ui.Insets(0, 0, 0, 0));
+ final RectF oldRect = graphWidget.getGridRect();
+
+ graphWidget.setGridInsets(new com.androidplot.ui.Insets(2, 2, 2, 2));
+ final RectF newRect = graphWidget.getGridRect();
+
+ assertEquals(oldRect.left + 2, newRect.left);
+ assertEquals(oldRect.top + 2, newRect.top);
+ assertEquals(oldRect.right - 2, newRect.right);
+ assertEquals(oldRect.bottom -2, newRect.bottom);
+ }
+
+ private void runDrawGridTest() {
+ doNothing().when(graphWidget).
+ drawDomainLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean(), anyBoolean());
+
+ doNothing().when(graphWidget).
+ drawRangeLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean(), anyBoolean());
+
+ xyPlot.setRangeBoundaries(0, 100, BoundaryMode.FIXED);
+ xyPlot.setDomainBoundaries(0, 100, BoundaryMode.FIXED);
+
+ graphWidget.drawGrid(canvas);
+
+ // expecting a 100x100 grid to be drawn:
+ verify(graphWidget, times(100))
+ .drawDomainLine(eq(canvas), anyFloat(), anyDouble(), any(Paint.class), eq(false), anyBoolean());
+
+ verify(graphWidget, times(100))
+ .drawRangeLine(eq(canvas), anyFloat(), anyDouble(), any(Paint.class), eq(false), anyBoolean());
+ }
}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java
index bbb2bec5..778fb40a 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java
@@ -17,61 +17,100 @@
package com.androidplot.xy;
import android.graphics.*;
-import com.androidplot.Plot;
import com.androidplot.test.AndroidplotTest;
-import org.junit.After;
+import com.androidplot.ui.DynamicTableModel;
+import com.androidplot.ui.LayoutManager;
+import com.androidplot.ui.Size;
+import com.androidplot.ui.SizeMode;
+import com.google.common.collect.Lists;
+
+import org.junit.Before;
import org.junit.Test;
-import org.robolectric.RuntimeEnvironment;
-import java.util.Arrays;
-import static junit.framework.Assert.assertEquals;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
public class XYLegendWidgetTest extends AndroidplotTest {
- static class MockXYPlot extends XYPlot {
+ @Mock LayoutManager layoutManager;
+ @Mock XYPlot xyPlot;
+ @Mock Canvas canvas;
+ @Mock XYRegionFormatter xyRegionFormatter;
+ LineAndPointRenderer lineAndPointRenderer;
- public MockXYPlot() {
- super(RuntimeEnvironment.application, "Test",
- Plot.RenderMode.USE_MAIN_THREAD);
- }
+ Size widgetSize = new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE);
+ Size iconSize = new Size(10, SizeMode.ABSOLUTE, 10, SizeMode.ABSOLUTE);
+ XYSeriesRegistry seriesRegistry;
- public void exposedOnSizeChanged(int w, int h, int oldw, int oldh) {
- this.onSizeChanged(w, h, oldw, oldh);
- }
+ XYLegendWidget legendWidget;
- public void exposedOnDraw(Canvas canvas) {
- this.onDraw(canvas);
- }
- }
+ @Before
+ public void before() {
+ seriesRegistry = new XYSeriesRegistry();
+ legendWidget = spy(new XYLegendWidget(layoutManager, xyPlot, widgetSize,
+ new DynamicTableModel(4, 4), iconSize));
- @After
- public void tearDown() throws Exception {}
+ lineAndPointRenderer = new LineAndPointRenderer(xyPlot);
- @Test
- public void testDoOnDraw() throws Exception {
- MockXYPlot plot = new MockXYPlot();
+ when(xyPlot.getRegistry()).thenReturn(seriesRegistry);
+ when(xyPlot.getRendererList()).thenReturn(Lists.newArrayList(lineAndPointRenderer));
+ when(xyPlot.getRenderer(any(Class.class))).thenReturn(lineAndPointRenderer);
+ }
- SimpleXYSeries s1 = new SimpleXYSeries((Arrays.asList(1, 2, 3)),
- SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1");
+ @Test
+ public void draw_drawsLegendIcons_forEnabledItemsOnly() throws Exception {
+ final XYSeries s1 = mock(XYSeries.class);
+ final XYSeriesFormatter f1 = new LineAndPointFormatter();
+ f1.setLegendIconEnabled(true);
+
+ final XYSeries s2 = mock(XYSeries.class);
+ final XYSeriesFormatter f2 = new LineAndPointFormatter();
+ f2.setLegendIconEnabled(false);
+
+ final RectRegion r1 = new RectRegion(0, 0, 10, 10, "r1");
+ final RectRegion r2 = new RectRegion(0, 0, 20, 20, "r2");
+ f1.addRegion(r1, new XYRegionFormatter(0));
+ f2.addRegion(r2, new XYRegionFormatter(0));
+
+ seriesRegistry.add(s1, f1);
+ seriesRegistry.add(s2, f2);
+ legendWidget.draw(canvas);
+
+ verify(legendWidget, times(2))
+ .drawRegionLegendIcon(any(Canvas.class), any(RectF.class), any(XYRegionFormatter.class));
+ verify(legendWidget, times(3))
+ .drawIcon(any(Canvas.class), any(RectF.class), any(XYLegendItem.class));
+ }
- plot.addSeries(s1, new LineAndPointFormatter(
- Color.RED, Color.GREEN, Color.BLUE, null));
+ @Test
+ public void draw_sortsItemsAlphabeticallyByTitle() throws Exception{
+ final XYLegendItem i1 = new XYLegendItem(XYLegendItem.Type.SERIES,
+ new LineAndPointFormatter(), "zoo");
+ final XYLegendItem i2 = new XYLegendItem(XYLegendItem.Type.SERIES,
+ new LineAndPointFormatter(), "apple");
+ final XYLegendItem i3 = new XYLegendItem(XYLegendItem.Type.SERIES,
+ new LineAndPointFormatter(), "boo");
- assertEquals(1, plot.getRegistry().size());
+ final List legendItems = Lists.newArrayList(i1, i2, i3);
+ doReturn(legendItems).when(legendWidget).getLegendItems();
- plot.exposedOnSizeChanged(100, 100, 100, 100);
- plot.redraw();
- // have to manually invoke this because the invalidate()
- // invoked by redraw() is a stub and will not result in onDraw being called.
- plot.exposedOnDraw(new Canvas());
+ legendWidget.draw(canvas);
- plot.removeSeries(s1);
- assertEquals(0, plot.getRegistry().size());
- plot.addSeries(s1, new BarFormatter(Color.RED, Color.GREEN));
- plot.redraw();
+ InOrder inOrder = Mockito.inOrder(legendWidget);
- // throws NullPointerException before fix
- // for ANDROIDPLOT-166 was applied.
- plot.exposedOnDraw(new Canvas());
+ inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i2));
+ inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i3));
+ inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i1));
}
-
}
diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java
index 7e443b29..2e8c6094 100644
--- a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java
@@ -31,6 +31,7 @@
import java.util.List;
import static junit.framework.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
public class XYPlotTest extends AndroidplotTest {
@@ -38,7 +39,7 @@ public class XYPlotTest extends AndroidplotTest {
List numList1;
List numList2;
- SimpleXYSeries series1;
+ SimpleXYSeries series0To100;
@Before
public void setUp() throws Exception {
@@ -46,7 +47,7 @@ public void setUp() throws Exception {
plot = new XYPlot(getContext(), "test");
numList1 = Arrays.asList(0, 1, 3, 5, 10, 15, 25, 50, 75, 100); // 10 elements
numList2 = Arrays.asList(-100, 0, 1, 3, 5, 10, 15, 25, 50, 75, 100, 200); // 12 elements
- series1 = new SimpleXYSeries(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "");
+ series0To100 = new SimpleXYSeries(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "");
}
@After
@@ -56,7 +57,7 @@ public void tearDown() throws Exception {
@Test
public void testOriginFixedMode() throws Exception {
- plot.addSeries(series1, new LineAndPointFormatter());
+ plot.addSeries(series0To100, new LineAndPointFormatter());
plot.centerOnDomainOrigin(5, 2, BoundaryMode.FIXED);
plot.calculateMinMaxVals();
@@ -67,7 +68,7 @@ public void testOriginFixedMode() throws Exception {
@Test
public void testOriginAutoMode() throws Exception {
- plot.addSeries(series1, new LineAndPointFormatter());
+ plot.addSeries(series0To100, new LineAndPointFormatter());
plot.centerOnDomainOrigin(5);
plot.calculateMinMaxVals();
@@ -84,7 +85,7 @@ public void testOriginAutoMode() throws Exception {
@Test
public void testOriginGrowMode() throws Exception {
- plot.addSeries(series1, new LineAndPointFormatter());
+ plot.addSeries(series0To100, new LineAndPointFormatter());
plot.centerOnDomainOrigin(5, null, BoundaryMode.GROW);
plot.calculateMinMaxVals();
@@ -92,14 +93,14 @@ public void testOriginGrowMode() throws Exception {
assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0);
// introduce a larger domain set. boundaries should change
- series1.setModel(numList2, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
assertEquals(-1.0, plot.getBounds().getMinX().doubleValue(), 0);
assertEquals(11.0, plot.getBounds().getMaxX().doubleValue(), 0);
// revert series model back to the previous set. boundaries should remain the same
- series1.setModel(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
assertEquals(-1.0, plot.getBounds().getMinX().doubleValue(), 0);
@@ -108,7 +109,7 @@ public void testOriginGrowMode() throws Exception {
@Test
public void testOriginShrinkMode() throws Exception {
- plot.addSeries(series1, new LineAndPointFormatter());
+ plot.addSeries(series0To100, new LineAndPointFormatter());
plot.centerOnDomainOrigin(5, null, BoundaryMode.SHRINK);
plot.calculateMinMaxVals();
@@ -116,18 +117,27 @@ public void testOriginShrinkMode() throws Exception {
assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0);
// update with more extreme values...nothing should change in shrink mode:
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0);
assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0);
}
+ @Test(expected = IllegalArgumentException.class)
+ public void centerOnRangeOrigin_throwsIllegalArgumentException_ifNullOrigin() {
+ plot.centerOnRangeOrigin(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void centerOnDomainOrigin_throwsIllegalArgumentException_ifNullOrigin() {
+ plot.centerOnDomainOrigin(null);
+ }
// Ifor not sure about filling in test stubs just going to do my own stuff instead.
@Test
public void testsetDomainBoundaries() throws Exception {
- plot.addSeries(series1, new LineAndPointFormatter());
+ plot.addSeries(series0To100, new LineAndPointFormatter());
plot.calculateMinMaxVals();
// default to auto so run them
@@ -159,7 +169,7 @@ public void testsetDomainBoundaries() throws Exception {
assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0);
// update with more extreme values...
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// after growing
@@ -167,7 +177,7 @@ public void testsetDomainBoundaries() throws Exception {
assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0);
// back to previous
- series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// should not of changed.
@@ -175,7 +185,7 @@ public void testsetDomainBoundaries() throws Exception {
assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0);
// back to big
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.setDomainBoundaries(2, BoundaryMode.SHRINK, 8, BoundaryMode.SHRINK);
plot.calculateMinMaxVals();
@@ -185,7 +195,7 @@ public void testsetDomainBoundaries() throws Exception {
assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0);
// now small
- series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// after shrinking
@@ -193,7 +203,7 @@ public void testsetDomainBoundaries() throws Exception {
assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0);
// back to previous
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// should not of changed.
@@ -210,8 +220,8 @@ public void testsetDomainBoundaries() throws Exception {
}
@Test
- public void testsetRangeBoundaries() throws Exception {
- plot.addSeries(series1, new LineAndPointFormatter());
+ public void setRangeBoundaries_calculatesCorrectMinMaxVals() throws Exception {
+ plot.addSeries(series0To100, new LineAndPointFormatter());
plot.calculateMinMaxVals();
// default to auto so run them
@@ -242,7 +252,7 @@ public void testsetRangeBoundaries() throws Exception {
assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0);
// update with more extreme values...
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// after growing
@@ -250,7 +260,7 @@ public void testsetRangeBoundaries() throws Exception {
assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0);
// back to previous
- series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// should not of changed.
@@ -258,7 +268,7 @@ public void testsetRangeBoundaries() throws Exception {
assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0);
// back to big
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.setRangeBoundaries(2, BoundaryMode.SHRINK, 8, BoundaryMode.SHRINK);
plot.calculateMinMaxVals();
@@ -268,7 +278,7 @@ public void testsetRangeBoundaries() throws Exception {
assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0);
// now small
- series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// after shrinking
@@ -276,7 +286,7 @@ public void testsetRangeBoundaries() throws Exception {
assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0);
// back to previous
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// should not of changed.
@@ -294,7 +304,7 @@ public void testsetRangeBoundaries() throws Exception {
@Test
public void testSetDomainRightMinMax() throws Exception {
- plot.addSeries(series1, new LineAndPointFormatter());
+ plot.addSeries(series0To100, new LineAndPointFormatter());
plot.calculateMinMaxVals();
// default to auto so run them
@@ -302,21 +312,19 @@ public void testSetDomainRightMinMax() throws Exception {
assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0);
plot.getOuterLimits().setMaxX(10);
- //plot.setDomainRightMax(10);
plot.calculateMinMaxVals();
// same values.
assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0);
assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0);
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// on RightMax
assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0);
assertEquals(10, plot.getBounds().getMaxX().doubleValue(), 0);
- //plot.setDomainRightMax(null);
plot.getOuterLimits().setMaxX(null);
plot.calculateMinMaxVals();
@@ -325,7 +333,6 @@ public void testSetDomainRightMinMax() throws Exception {
assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0);
// now the RightMin
- //plot.setDomainRightMin(10);
plot.getInnerLimits().setMaxX(10);
plot.calculateMinMaxVals();
@@ -334,7 +341,7 @@ public void testSetDomainRightMinMax() throws Exception {
assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0);
// small list
- series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// on RightMin
@@ -342,7 +349,6 @@ public void testSetDomainRightMinMax() throws Exception {
assertEquals(10, plot.getBounds().getMaxX().doubleValue(), 0);
// now off again
- //plot.setDomainRightMin(null);
plot.getInnerLimits().setMaxX(null);
plot.calculateMinMaxVals();
@@ -353,7 +359,7 @@ public void testSetDomainRightMinMax() throws Exception {
@Test
public void testSetRangeTopBottomMinMax() throws Exception {
- plot.addSeries(series1, new LineAndPointFormatter());
+ plot.addSeries(series0To100, new LineAndPointFormatter());
plot.calculateMinMaxVals();
// default to auto so run them
@@ -362,15 +368,13 @@ public void testSetRangeTopBottomMinMax() throws Exception {
plot.getOuterLimits().setMaxY(110);
plot.getOuterLimits().setMinY(-50);
- //plot.setRangeTopMax(110);
- //plot.setRangeBottomMin(-50);
plot.calculateMinMaxVals();
// same values.
assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0);
assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0);
- series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// on Limits
@@ -379,8 +383,6 @@ public void testSetRangeTopBottomMinMax() throws Exception {
plot.getOuterLimits().setMaxY(null);
plot.getOuterLimits().setMinY(null);
- //plot.setRangeTopMax(null);
- //plot.setRangeBottomMin(null);
plot.calculateMinMaxVals();
// back to full
@@ -390,8 +392,6 @@ public void testSetRangeTopBottomMinMax() throws Exception {
// now the Min
plot.getInnerLimits().setMaxY(150);
plot.getInnerLimits().setMinY(-60);
- //plot.setRangeTopMin(150);
- //plot.setRangeBottomMax(-60);
plot.calculateMinMaxVals();
// still to full
@@ -399,7 +399,7 @@ public void testSetRangeTopBottomMinMax() throws Exception {
assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0);
// small list
- series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
+ series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY);
plot.calculateMinMaxVals();
// on Limits
@@ -409,8 +409,6 @@ public void testSetRangeTopBottomMinMax() throws Exception {
// now off again
plot.getInnerLimits().setMaxY(null);
plot.getInnerLimits().setMinY(null);
- //plot.setRangeTopMin(null);
- //plot.setRangeBottomMax(null);
plot.calculateMinMaxVals();
// small values.
@@ -419,33 +417,45 @@ public void testSetRangeTopBottomMinMax() throws Exception {
}
@Test
- public void testSetDomainUpperBoundary() throws Exception {
-
+ public void setDomainUpperBoundary_overridesCalculatedBoundary() throws Exception {
+ plot.addSeries(series0To100, new LineAndPointFormatter());
+ plot.setDomainUpperBoundary(350, BoundaryMode.FIXED);
+ plot.calculateMinMaxVals();
+ assertEquals(350, plot.getBounds().getMaxX().intValue());
}
@Test
public void testSetDomainLowerBoundary() throws Exception {
-
+ plot.addSeries(series0To100, new LineAndPointFormatter());
+ plot.setDomainLowerBoundary(-350, BoundaryMode.FIXED);
+ plot.calculateMinMaxVals();
+ assertEquals(-350, plot.getBounds().getMinX().intValue());
}
@Test
public void testSetRangeUpperBoundary() throws Exception {
-
+ plot.addSeries(series0To100, new LineAndPointFormatter());
+ plot.setRangeUpperBoundary(350, BoundaryMode.FIXED);
+ plot.calculateMinMaxVals();
+ assertEquals(350, plot.getBounds().getMaxY().intValue());
}
@Test
public void testSetRangeLowerBoundary() throws Exception {
-
+ plot.addSeries(series0To100, new LineAndPointFormatter());
+ plot.setRangeLowerBoundary(-350, BoundaryMode.FIXED);
+ plot.calculateMinMaxVals();
+ assertEquals(-350, plot.getBounds().getMinY().intValue());
}
@Test
public void testSetDomainOrigin() throws Exception {
-
+ // TODO
}
@Test
public void testSetRangeOrigin() throws Exception {
-
+ // TODO
}
@Test
@@ -463,4 +473,81 @@ public void testConfigure() throws Exception {
assertEquals(Plot.RenderMode.USE_BACKGROUND_THREAD, plot.getRenderMode());
assertEquals(Color.parseColor(param3), plot.getBackgroundPaint().getColor());
}
+
+ @Test
+ public void removeMarker_withXMarker_removesExpectedXMarkerOnly() {
+
+ XValueMarker xMarker1 = new XValueMarker(1, "x1");
+ XValueMarker xMarker2 = new XValueMarker(2, "x2");
+ XValueMarker xMarker3 = new XValueMarker(2, "x2");
+ XValueMarker xMarker4 = new XValueMarker(2, "x2");
+ XValueMarker xMarker5 = new XValueMarker(2, "x2");
+
+ plot.addMarker(xMarker1);
+ plot.addMarker(xMarker2);
+ plot.addMarker(xMarker3);
+ plot.addMarker(xMarker4);
+ plot.addMarker(xMarker5);
+
+ assertEquals(5, plot.getXValueMarkers().size());
+
+ assertEquals(xMarker3, plot.removeMarker(xMarker3));
+ assertEquals(4, plot.getXValueMarkers().size());
+ }
+
+ @Test
+ public void removeMarker_withYMarker_removesExpectedYMarkerOnly() {
+
+ YValueMarker YMarker1 = new YValueMarker(1, "Y1");
+ YValueMarker YMarker2 = new YValueMarker(2, "Y2");
+ YValueMarker YMarker3 = new YValueMarker(2, "Y2");
+ YValueMarker YMarker4 = new YValueMarker(2, "Y2");
+ YValueMarker YMarker5 = new YValueMarker(2, "Y2");
+
+ plot.addMarker(YMarker1);
+ plot.addMarker(YMarker2);
+ plot.addMarker(YMarker3);
+ plot.addMarker(YMarker4);
+ plot.addMarker(YMarker5);
+
+ assertEquals(5, plot.getYValueMarkers().size());
+
+ assertEquals(YMarker3, plot.removeMarker(YMarker3));
+ assertEquals(4, plot.getYValueMarkers().size());
+ }
+
+ @Test
+ public void removeMarkers_removesAllXAndYMarkers() {
+
+ XValueMarker xMarker1 = new XValueMarker(1, "x1");
+ XValueMarker xMarker2 = new XValueMarker(2, "x2");
+ XValueMarker xMarker3 = new XValueMarker(2, "x2");
+ XValueMarker xMarker4 = new XValueMarker(2, "x2");
+ XValueMarker xMarker5 = new XValueMarker(2, "x2");
+
+ plot.addMarker(xMarker1);
+ plot.addMarker(xMarker2);
+ plot.addMarker(xMarker3);
+ plot.addMarker(xMarker4);
+ plot.addMarker(xMarker5);
+
+ YValueMarker YMarker1 = new YValueMarker(1, "Y1");
+ YValueMarker YMarker2 = new YValueMarker(2, "Y2");
+ YValueMarker YMarker3 = new YValueMarker(2, "Y2");
+ YValueMarker YMarker4 = new YValueMarker(2, "Y2");
+ YValueMarker YMarker5 = new YValueMarker(2, "Y2");
+
+ plot.addMarker(YMarker1);
+ plot.addMarker(YMarker2);
+ plot.addMarker(YMarker3);
+ plot.addMarker(YMarker4);
+ plot.addMarker(YMarker5);
+
+ assertEquals(5, plot.getXValueMarkers().size());
+ assertEquals(5, plot.getYValueMarkers().size());
+
+ plot.removeMarkers();
+ assertEquals(0, plot.getXValueMarkers().size());
+ assertEquals(0, plot.getYValueMarkers().size());
+ }
}
diff --git a/build.gradle b/build.gradle
index d74e02dc..4b5e18de 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,44 +14,32 @@
* limitations under the License.
*/
-apply plugin: 'java'
-
-allprojects {
- repositories {
- jcenter()
- }
-}
-
ext {
- theBuildToolsVersion = '24.0.2'
- theCompileSdkVersion = 24
- theTargetSdkVersion = 24
+ theCompileSdkVersion = 34
+ theTargetSdkVersion = 34
theMinSdkVersion = 5
- theVersionName = '1.3.0'
- theVersionCode = 0
+ theVersionName = '1.5.11'
+ theVersionCode = 227
+ gitUrl = 'https://github.com/halfhp/androidplot.git'
}
buildscript {
repositories {
mavenCentral()
- jcenter()
+ maven { url "https://plugins.gradle.org/m2/" }
+ maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' }
+ google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.2.0'
- classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
- classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7'
- classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.5.0'
- classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.6.3'
+ classpath 'com.android.tools.build:gradle:8.5.2'
+ classpath 'com.mxalbert.gradle:jacoco-android:0.2.0'
}
}
allprojects {
repositories {
mavenCentral()
+ google()
}
}
-
-task wrapper(type: Wrapper) {
- gradleVersion = '2.14.1'
-}
\ No newline at end of file
diff --git a/circle.yml b/circle.yml
deleted file mode 100644
index cb73691c..00000000
--- a/circle.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-machine:
- environment:
- KEYSTORE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/sigining.keystore
- PUBLISHER_ACCT_JSON_FILE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/publisher_profile.json
-
-dependencies:
-
- pre:
- - echo y | android update sdk --no-ui --all --filter "android-24"
- - echo y | android update sdk --no-ui --all --filter "build-tools-24.0.2"
- - bash ./misc/download_keystore.sh
- - bash ./misc/inject_circle_build_number.sh
-
-test:
-
- override:
- - (./gradlew test assembleRelease javadoc):
- timeout: 360
-
- post:
-
- # core lib:
- - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/outputs/aar/ $CIRCLE_ARTIFACTS
-
- # demo app .apk:
- - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/demoapp/build/outputs/apk/ $CIRCLE_ARTIFACTS
-
- # javadoc:
- - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/docs/javadoc/ $CIRCLE_ARTIFACTS
-
-
- # junit xml report:
- - mkdir -p $CIRCLE_TEST_REPORTS/junit-xml/
- - find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit-xml/ \;
-
- # junit html report:
- # TODO: recursively copy subdirs etc
- - mkdir -p $CIRCLE_TEST_REPORTS/junit-html/
- - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/tests/release/* $CIRCLE_TEST_REPORTS/junit-html/
-
- # lint report:
- - mkdir -p $CIRCLE_TEST_REPORTS/lint/
- - find . -type f -regex ".*/build/outputs/.*html" -exec cp {} $CIRCLE_TEST_REPORTS/lint/ \;
-
- # code coverage:
- - ./gradlew jacocoTestReportDebug
- - mkdir -p $CIRCLE_TEST_REPORTS/jacoco/
- - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/jacoco/debug/. $CIRCLE_TEST_REPORTS/jacoco
- - bash <(curl -s https://codecov.io/bash)
-
-deployment:
- master:
- branch: master
- commands:
- - (./gradlew bintrayUpload):
- timeout: 360
- - bash ./misc/download_google_publisher_json.sh
- - ./gradlew publishApkRelease
diff --git a/demoapp-wearable/build.gradle b/demoapp-wearable/build.gradle
index 98d5307d..666b353e 100644
--- a/demoapp-wearable/build.gradle
+++ b/demoapp-wearable/build.gradle
@@ -16,17 +16,14 @@
buildscript {
repositories {
- jcenter()
+ google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:1.2.3'
+ classpath 'com.android.tools.build:gradle:3.1.4'
}
}
-apply plugin: 'com.android.application'
-repositories {
- jcenter()
-}
+apply plugin: 'com.android.application'
android {
compileSdkVersion theCompileSdkVersion
@@ -41,9 +38,10 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_6
- targetCompatibility JavaVersion.VERSION_1_6
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
+
buildTypes {
release {
minifyEnabled false
@@ -54,6 +52,6 @@ android {
dependencies {
compile project(':androidplot-core')
- compile 'com.google.android.support:wearable:1.3.0'
+ compile 'com.google.android.support:wearable:2.3.0'
compile 'com.google.android.gms:play-services-wearable:8.3.0'
}
diff --git a/demoapp/build.gradle b/demoapp/build.gradle
index 141a6f80..2c2fbd52 100644
--- a/demoapp/build.gradle
+++ b/demoapp/build.gradle
@@ -14,37 +14,54 @@
* limitations under the License.
*/
-apply plugin: 'com.android.application'
-apply plugin: 'com.github.triplet.play'
-
-dependencies {
- compile project(':androidplot-core')
- compile 'com.crittercism:crittercism-android-agent:5.4.0'
-
- debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
- releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
- testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
-}
-
buildscript {
-
+ ext.kotlin_version = '2.0.0'
repositories {
mavenCentral()
}
-
dependencies {
- classpath 'com.github.triplet.gradle:play-publisher:1.1.5'
+ classpath 'com.github.triplet.gradle:play-publisher:3.6.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
+plugins {
+ id 'com.android.application'
+ id 'com.github.triplet.play' version '3.6.0'
+}
+
+apply plugin: 'kotlin-android'
+
+dependencies {
+ implementation "androidx.core:core-ktx:1.13.1"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation project(':androidplot-core')
+ implementation 'androidx.recyclerview:recyclerview:1.3.2'
+
+ debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
+}
+
+kotlin {
+ jvmToolchain(17)
+}
+
android {
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
compileSdkVersion theCompileSdkVersion
- buildToolsVersion theBuildToolsVersion
+
+ buildFeatures {
+ viewBinding true
+ }
defaultConfig {
versionCode theVersionCode
versionName theVersionName
- minSdkVersion 11
+ minSdkVersion 19
targetSdkVersion theTargetSdkVersion
applicationId "com.androidplot.demos"
}
@@ -68,18 +85,18 @@ android {
debuggable true
}
}
-
-
- /**
- * TODO: uncomment this and address all the lint issues.
- */
- lintOptions {
+ lint {
abortOnError false
}
+ namespace 'com.androidplot.demos'
+
}
play {
// see: https://github.com/Triple-T/gradle-play-publisher
- jsonFile = file(System.getenv("PUBLISHER_ACCT_JSON_FILE") ?: "publisher.json")
+ serviceAccountCredentials.set(file(System.getenv("PUBLISHER_ACCT_JSON_FILE") ?: "publisher.json"))
track = 'beta'
}
+repositories {
+ mavenCentral()
+}
diff --git a/demoapp/project.properties b/demoapp/project.properties
index 29656f2d..90a2cb18 100644
--- a/demoapp/project.properties
+++ b/demoapp/project.properties
@@ -39,5 +39,3 @@
#proguard.config=${sdk.dir}\tools\proguard\proguard-android.txt:proguard-project.txt
# using config with optimization enabled so that things like log statement removal will work:
proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt
-# Project target.
-target=android-16
diff --git a/demoapp/src/main/AndroidManifest.xml b/demoapp/src/main/AndroidManifest.xml
index 09d9e761..5b73a2b7 100644
--- a/demoapp/src/main/AndroidManifest.xml
+++ b/demoapp/src/main/AndroidManifest.xml
@@ -15,7 +15,7 @@
~ limitations under the License.
-->
-
+
@@ -27,69 +27,88 @@
android:icon="@drawable/ic_launcher">
+ android:label="@string/app_name"
+ android:exported="true">
+ android:label="@string/app_name"
+ android:exported="false">
+
+
+
+
+
+ android:label="@string/app_name"
+ android:exported="false">
+ android:label="@string/app_name"
+ android:exported="false">
+ android:label="@string/app_name"
+ android:exported="false">
+ android:label="@string/app_name"
+ android:exported="false">
+ android:label="@string/app_name"
+ android:exported="false">
+ android:screenOrientation="landscape"
+ android:exported="false">
+ android:label="@string/app_name"
+ android:exported="false">
+ android:label="@string/app_name"
+ android:exported="false">
-
+
+
@@ -99,12 +118,14 @@
+
+ android:name="com.androidplot.demos.widget.DemoAppWidgetProvider"
+ android:exported="false">
diff --git a/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java
new file mode 100644
index 00000000..43d3adc0
--- /dev/null
+++ b/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * 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.androidplot.demos;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+import com.androidplot.xy.BoundaryMode;
+import com.androidplot.xy.CatmullRomInterpolator;
+import com.androidplot.xy.LineAndPointFormatter;
+import com.androidplot.xy.ScalingXYSeries;
+import com.androidplot.xy.SimpleXYSeries;
+import com.androidplot.xy.XYGraphWidget;
+import com.androidplot.xy.XYPlot;
+import com.androidplot.xy.XYSeries;
+
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.util.Arrays;
+
+/**
+ * Demonstrates animating XYSeries data from a zero value up/down to the actual values set. Once
+ * the animation completes, labels for each point are made visible.
+ *
+ * IMPORTANT: This example makes use of {@link ValueAnimator} which is only available in
+ * SDK level 11 and later..
+ */
+public class AnimatedXYPlotActivity extends Activity {
+
+ private XYPlot plot;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.simple_xy_plot_example);
+
+ // initialize our XYPlot reference:
+ plot = (XYPlot) findViewById(R.id.plot);
+
+ // create a couple arrays of y-values to plot:
+ final Number[] domainLabels = {1, 2, 3, 6, 7, 8, 9, 10, 13, 14};
+ Number[] series1Numbers = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64};
+ Number[] series2Numbers = {5, 2, 10, 5, 20, 10, 40, 20, 80, 40};
+
+ // turn the above arrays into XYSeries':
+ // (Y_VALS_ONLY means use the element index as the x value)
+ XYSeries series1 = new SimpleXYSeries(
+ Arrays.asList(series1Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Series1");
+ XYSeries series2 = new SimpleXYSeries(
+ Arrays.asList(series2Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Series2");
+
+ // create formatters to use for drawing a series using LineAndPointRenderer
+ // and configure them from xml:
+ final LineAndPointFormatter series1Format =
+ new LineAndPointFormatter(this, R.xml.line_point_formatter);
+
+ final LineAndPointFormatter series2Format =
+ new LineAndPointFormatter(this, R.xml.line_point_formatter);
+ series2Format.getLinePaint().setColor(Color.RED);
+
+ // just for fun, add some smoothing to the lines:
+ // see: http://androidplot.com/smooth-curves-and-androidplot/
+ series1Format.setInterpolationParams(
+ new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal));
+
+ series2Format.setInterpolationParams(
+ new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal));
+
+ // wrap each series in instances of ScalingXYSeries before adding to the plot
+ // so that we can animate the series values below:
+ final ScalingXYSeries scalingSeries1 = new ScalingXYSeries(series1, 0, ScalingXYSeries.Mode.Y_ONLY);
+ plot.addSeries(scalingSeries1, series1Format);
+
+ final ScalingXYSeries scalingSeries2 = new ScalingXYSeries(series2, 0, ScalingXYSeries.Mode.Y_ONLY);
+ plot.addSeries(scalingSeries2, series2Format);
+
+ plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() {
+ @Override
+ public StringBuffer format(Object obj,
+ @NonNull StringBuffer toAppendTo,
+ @NonNull FieldPosition pos) {
+ int i = Math.round(((Number) obj).floatValue());
+ return toAppendTo.append(domainLabels[i]);
+ }
+ @Override
+ public Object parseObject(String source, @NonNull ParsePosition pos) {
+ return null;
+ }
+ });
+
+ plot.setRangeBoundaries(0, 100, BoundaryMode.FIXED);
+
+ // animate a scale value from a starting val of 0 to a final value of 1:
+ ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+
+ // use an animation pattern that begins and ends slowly:
+ animator.setInterpolator(new AccelerateDecelerateInterpolator());
+
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ float scale = valueAnimator.getAnimatedFraction();
+ scalingSeries1.setScale(scale);
+ scalingSeries2.setScale(scale);
+ plot.redraw();
+ }
+ });
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ // the animation is over, so show point labels:
+ series1Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE);
+ series2Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE);
+ plot.redraw();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+
+ }
+ });
+
+ // the animation will run for 1.5 seconds:
+ animator.setDuration(1500);
+ animator.start();
+ }
+}
diff --git a/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java
index e3400ec5..3cc2faa4 100644
--- a/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java
@@ -22,6 +22,7 @@
import java.text.ParsePosition;
import java.util.Arrays;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.Paint;
@@ -86,6 +87,7 @@ private enum SeriesSize {
private Pair selection;
+ @SuppressLint("ClickableViewAccessibility")
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -96,7 +98,11 @@ public void onCreate(Bundle savedInstanceState) {
plot = (XYPlot) findViewById(R.id.plot);
formatter1 = new MyBarFormatter(Color.rgb(100, 150, 100), Color.LTGRAY);
+ formatter1.setMarginLeft(PixelUtils.dpToPix(1));
+ formatter1.setMarginRight(PixelUtils.dpToPix(1));
formatter2 = new MyBarFormatter(Color.rgb(100, 100, 150), Color.LTGRAY);
+ formatter2.setMarginLeft(PixelUtils.dpToPix(1));
+ formatter2.setMarginRight(PixelUtils.dpToPix(1));
selectionFormatter = new MyBarFormatter(Color.YELLOW, Color.WHITE);
selectionWidget = new TextLabelWidget(plot.getLayoutManager(), NO_SELECTION_TXT,
@@ -152,12 +158,12 @@ public boolean onTouch(View view, MotionEvent motionEvent) {
});
spRenderStyle = (Spinner) findViewById(R.id.spRenderStyle);
- ArrayAdapter adapter = new ArrayAdapter(this,
- android.R.layout.simple_spinner_item, BarRenderer.Style
+ ArrayAdapter adapter = new ArrayAdapter(this,
+ android.R.layout.simple_spinner_item, BarRenderer.BarOrientation
.values());
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spRenderStyle.setAdapter(adapter);
- spRenderStyle.setSelection(BarRenderer.Style.OVERLAID.ordinal());
+ spRenderStyle.setSelection(BarRenderer.BarOrientation.OVERLAID.ordinal());
spRenderStyle.setOnItemSelectedListener(new OnItemSelectedListener() {
public void onItemSelected(AdapterView> arg0, View arg1, int arg2, long arg3) {
updatePlot();
@@ -169,15 +175,15 @@ public void onNothingSelected(AdapterView> arg0) {
});
spWidthStyle = (Spinner) findViewById(R.id.spWidthStyle);
- ArrayAdapter adapter1 = new ArrayAdapter(
- this, android.R.layout.simple_spinner_item, BarRenderer.BarWidthMode
+ ArrayAdapter adapter1 = new ArrayAdapter(
+ this, android.R.layout.simple_spinner_item, BarRenderer.BarGroupWidthMode
.values());
adapter1.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spWidthStyle.setAdapter(adapter1);
- spWidthStyle.setSelection(BarRenderer.BarWidthMode.FIXED_WIDTH.ordinal());
+ spWidthStyle.setSelection(BarRenderer.BarGroupWidthMode.FIXED_WIDTH.ordinal());
spWidthStyle.setOnItemSelectedListener(new OnItemSelectedListener() {
public void onItemSelected(AdapterView> arg0, View arg1, int arg2, long arg3) {
- if (BarRenderer.BarWidthMode.FIXED_WIDTH.equals(spWidthStyle.getSelectedItem())) {
+ if (BarRenderer.BarGroupWidthMode.FIXED_WIDTH.equals(spWidthStyle.getSelectedItem())) {
sbFixedWidth.setVisibility(View.VISIBLE);
sbVariableWidth.setVisibility(View.INVISIBLE);
} else {
@@ -282,9 +288,7 @@ public Number parse(String string, ParsePosition position) {
throw new UnsupportedOperationException("Not yet implemented.");
}
});
-
updatePlot();
-
}
private void updatePlot() {
@@ -326,13 +330,15 @@ private void updatePlot(SeriesSize seriesSize) {
if (series2CheckBox.isChecked()) plot.addSeries(series2, formatter2);
// Setup the BarRenderer with our selected options
- MyBarRenderer renderer = ((MyBarRenderer) plot.getRenderer(MyBarRenderer.class));
- renderer.setStyle((BarRenderer.Style) spRenderStyle.getSelectedItem());
- renderer.setBarWidthMode((BarRenderer.BarWidthMode) spWidthStyle.getSelectedItem());
- renderer.setBarWidth(sbFixedWidth.getProgress());
- renderer.setBarGap(sbVariableWidth.getProgress());
-
- if (BarRenderer.Style.STACKED.equals(spRenderStyle.getSelectedItem())) {
+ MyBarRenderer renderer = plot.getRenderer(MyBarRenderer.class);
+ renderer.setBarOrientation((BarRenderer.BarOrientation) spRenderStyle.getSelectedItem());
+ final BarRenderer.BarGroupWidthMode barGroupWidthMode
+ = (BarRenderer.BarGroupWidthMode) spWidthStyle.getSelectedItem();
+ renderer.setBarGroupWidth(barGroupWidthMode,
+ barGroupWidthMode == BarRenderer.BarGroupWidthMode.FIXED_WIDTH
+ ? sbFixedWidth.getProgress() : sbVariableWidth.getProgress());
+
+ if (BarRenderer.BarOrientation.STACKED.equals(spRenderStyle.getSelectedItem())) {
plot.getInnerLimits().setMaxY(15);
} else {
plot.getInnerLimits().setMaxY(0);
diff --git a/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java
index 432b16ed..86ae1fbb 100644
--- a/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java
@@ -21,15 +21,28 @@
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.os.Bundle;
+import androidx.annotation.NonNull;
-import com.androidplot.*;
-import com.androidplot.util.*;
+import com.androidplot.Region;
+import com.androidplot.util.PixelUtils;
+import com.androidplot.util.SeriesUtils;
+import com.androidplot.xy.BoundaryMode;
import com.androidplot.xy.CandlestickFormatter;
import com.androidplot.xy.CandlestickMaker;
import com.androidplot.xy.CandlestickSeries;
-import com.androidplot.xy.*;
-
-import java.text.*;
+import com.androidplot.xy.CatmullRomInterpolator;
+import com.androidplot.xy.LineAndPointFormatter;
+import com.androidplot.xy.PointLabelFormatter;
+import com.androidplot.xy.PointLabeler;
+import com.androidplot.xy.StepMode;
+import com.androidplot.xy.XYGraphWidget;
+import com.androidplot.xy.XYPlot;
+import com.androidplot.xy.XYSeries;
+
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
/**
* A simple example of a candlestick chart rendered on an {@link XYPlot}.
@@ -98,8 +111,8 @@ public void onCreate(Bundle savedInstanceState) {
plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1);
plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() {
@Override
- public StringBuffer format(Object object, StringBuffer buffer,
- FieldPosition field) {
+ public StringBuffer format(Object object, @NonNull StringBuffer buffer,
+ @NonNull FieldPosition field) {
int day = ((Number) object).intValue() % 7;
switch (day) {
case 0:
@@ -130,7 +143,7 @@ public StringBuffer format(Object object, StringBuffer buffer,
}
@Override
- public Object parseObject(String string, ParsePosition position) {
+ public Object parseObject(String string, @NonNull ParsePosition position) {
return null;
}
});
diff --git a/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java b/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java
index f8c00eae..df3d6117 100644
--- a/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java
+++ b/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java
@@ -2,23 +2,10 @@
import android.app.*;
-import com.squareup.leakcanary.*;
-
-/**
- * Created by halfhp on 10/1/16.
- */
public class DemoApplication extends Application {
@Override public void onCreate() {
super.onCreate();
- initLeakCanary();
- }
-
- protected void initLeakCanary() {
- if (LeakCanary.isInAnalyzerProcess(this)) {
- return;
- }
- LeakCanary.install(this);
}
}
diff --git a/demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java b/demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java
new file mode 100644
index 00000000..f0116121
--- /dev/null
+++ b/demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * 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.androidplot.demos;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+
+import com.androidplot.util.PixelUtils;
+import com.androidplot.xy.BoundaryMode;
+import com.androidplot.xy.CatmullRomInterpolator;
+import com.androidplot.xy.LineAndPointFormatter;
+import com.androidplot.xy.NormedXYSeries;
+import com.androidplot.xy.SimpleXYSeries;
+import com.androidplot.xy.StepMode;
+import com.androidplot.xy.XYGraphWidget;
+import com.androidplot.xy.XYPlot;
+
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.util.Arrays;
+
+/**
+ * Demonstrates normalizing series data using {@link com.androidplot.xy.NormedXYSeries}
+ * and displaying a dual scale on an XYPlot.
+ *
+ * This particular example displays the yearly cost of raising of a child along the federal
+ * minimum wage from 2012 to 2016.
+ *
+ */
+public class DualScaleActivity extends Activity {
+
+ private XYPlot plot;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.dual_scale_example);
+ plot = (XYPlot) findViewById(R.id.plot);
+
+ Number[] childCosts = {5500, 5550,5496, 5800, 5815};
+ Number[] minWages = {9, 9, 9, 9, 10};
+
+ // create and normalize series data:
+ final NormedXYSeries costsSeries = new NormedXYSeries(new SimpleXYSeries(
+ Arrays.asList(childCosts), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Yearly Cost"));
+ final NormedXYSeries minWageSeries = new NormedXYSeries(new SimpleXYSeries(
+ Arrays.asList(minWages), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Min Wage"));
+
+ // create formatters to use for drawing a series using LineAndPointRenderer
+ // and configure them from xml:
+ LineAndPointFormatter childCostsFormat =
+ new LineAndPointFormatter(this, R.xml.line_point_formatter_with_labels);
+ childCostsFormat.setVertexPaint(null);
+ childCostsFormat.setPointLabelFormatter(null);
+
+ LineAndPointFormatter minWageFormat =
+ new LineAndPointFormatter(this, R.xml.line_point_formatter_with_labels_2);
+ minWageFormat.setVertexPaint(null);
+ minWageFormat.setPointLabelFormatter(null);
+
+ // add an "dash" effect to the series2 line:
+ minWageFormat.getLinePaint().setPathEffect(new DashPathEffect(new float[] {
+
+ // always use DP when specifying pixel sizes, to keep things consistent across devices:
+ PixelUtils.dpToPix(20),
+ PixelUtils.dpToPix(15)}, 0));
+
+ // add some smoothing to the lines:
+ // see: http://androidplot.com/smooth-curves-and-androidplot/
+ childCostsFormat.setInterpolationParams(
+ new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal));
+
+ minWageFormat.setInterpolationParams(
+ new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal));
+
+ // add a new series' to the xyplot:
+ plot.addSeries(costsSeries, childCostsFormat);
+ plot.addSeries(minWageSeries, minWageFormat);
+
+ plot.setRangeBoundaries(-1, 2, BoundaryMode.FIXED);
+
+ plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).getPaint().setColor(Color.GREEN);
+ plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).setFormat(new Format() {
+
+ final DecimalFormat df = new DecimalFormat("$#,###");
+
+ @Override
+ public StringBuffer format(Object o, @NonNull StringBuffer stringBuffer,
+ @NonNull FieldPosition fieldPosition) {
+ final Number cost = costsSeries.denormalizeYVal((Number) o);
+ stringBuffer.append(df.format(cost.doubleValue()));
+ return stringBuffer;
+ }
+
+ @Override
+ public Object parseObject(String s, @NonNull ParsePosition parsePosition) {
+ return null;
+ }
+ });
+
+ plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.RIGHT).getPaint().setColor(Color.BLUE);
+ plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.RIGHT).setFormat(new Format() {
+
+ final DecimalFormat df = new DecimalFormat("$#.##");
+
+ @Override
+ public StringBuffer format(Object o, @NonNull StringBuffer stringBuffer,
+ @NonNull FieldPosition fieldPosition) {
+ Number minWage = minWageSeries.denormalizeYVal((Number) o);
+ stringBuffer.append(df.format(minWage.doubleValue()));
+ return stringBuffer;
+ }
+
+ @Override
+ public Object parseObject(String s, @NonNull ParsePosition parsePosition) {
+ return null;
+ }
+ });
+
+ plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1);
+ plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() {
+
+ @Override
+ public StringBuffer format(Object o, @NonNull StringBuffer stringBuffer,
+ @NonNull FieldPosition fieldPosition) {
+
+ Number year = ((Number) o).intValue() + 2012;
+ stringBuffer.append(year);
+ return stringBuffer;
+ }
+
+ @Override
+ public Object parseObject(String s, @NonNull ParsePosition parsePosition) {
+ return null;
+ }
+ });
+ }
+}
diff --git a/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java
index f76dca01..f18b518b 100644
--- a/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java
@@ -138,8 +138,8 @@ public void notifyObservers() {
private static final int MAX_AMP_SEED = 100;
private static final int MIN_AMP_SEED = 10;
private static final int AMP_STEP = 1;
- public static final int SINE1 = 0;
- public static final int SINE2 = 1;
+ static final int SINE1 = 0;
+ static final int SINE2 = 1;
private static final int SAMPLE_SIZE = 31;
private int phase = 0;
private int sinAmp = 1;
@@ -150,7 +150,7 @@ public void notifyObservers() {
notifier = new MyObservable();
}
- public void stopThread() {
+ void stopThread() {
keepRunning = false;
}
@@ -181,18 +181,18 @@ public void run() {
}
}
- public int getItemCount(int series) {
+ int getItemCount(int series) {
return SAMPLE_SIZE;
}
- public Number getX(int series, int index) {
+ Number getX(int series, int index) {
if (index >= SAMPLE_SIZE) {
throw new IllegalArgumentException();
}
return index;
}
- public Number getY(int series, int index) {
+ Number getY(int series, int index) {
if (index >= SAMPLE_SIZE) {
throw new IllegalArgumentException();
}
@@ -208,7 +208,7 @@ public Number getY(int series, int index) {
}
}
- public void addObserver(Observer observer) {
+ void addObserver(Observer observer) {
notifier.addObserver(observer);
}
@@ -223,7 +223,7 @@ class SampleDynamicSeries implements XYSeries {
private int seriesIndex;
private String title;
- public SampleDynamicSeries(SampleDynamicXYDatasource datasource, int seriesIndex, String title) {
+ SampleDynamicSeries(SampleDynamicXYDatasource datasource, int seriesIndex, String title) {
this.datasource = datasource;
this.seriesIndex = seriesIndex;
this.title = title;
diff --git a/demoapp/src/main/java/com/androidplot/demos/ECGExample.java b/demoapp/src/main/java/com/androidplot/demos/ECGExample.java
index 31d71e87..2bd7dec5 100644
--- a/demoapp/src/main/java/com/androidplot/demos/ECGExample.java
+++ b/demoapp/src/main/java/com/androidplot/demos/ECGExample.java
@@ -82,7 +82,7 @@ public static class MyFadeFormatter extends AdvancedLineAndPointRenderer.Formatt
private int trailSize;
- public MyFadeFormatter(int trailSize) {
+ MyFadeFormatter(int trailSize) {
this.trailSize = trailSize;
}
@@ -125,7 +125,7 @@ public static class ECGModel implements XYSeries {
* @param size Sample size contained within this model
* @param updateFreqHz Frequency at which new samples are added to the model
*/
- public ECGModel(int size, int updateFreqHz) {
+ ECGModel(int size, int updateFreqHz) {
data = new Number[size];
for(int i = 0; i < data.length; i++) {
data[i] = 0;
@@ -176,7 +176,7 @@ public void run() {
});
}
- public void start(final WeakReference rendererRef) {
+ void start(final WeakReference rendererRef) {
this.rendererRef = rendererRef;
keepRunning = true;
thread.start();
diff --git a/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java b/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java
index 47acf1cb..290b7802 100644
--- a/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java
@@ -20,15 +20,21 @@
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
+import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
+
import com.androidplot.Plot;
import com.androidplot.ui.SeriesBundle;
import com.androidplot.util.PixelUtils;
-import com.androidplot.xy.*;
+import com.androidplot.xy.CatmullRomInterpolator;
+import com.androidplot.xy.LineAndPointFormatter;
+import com.androidplot.xy.SimpleXYSeries;
+import com.androidplot.xy.XYPlot;
+import com.androidplot.xy.XYSeries;
import java.util.ArrayList;
import java.util.List;
@@ -72,10 +78,10 @@ protected void generateData() {
double bp = Math.random();
LineAndPointFormatter lpf = new LineAndPointFormatter(
- Color.rgb(new Double(rl * 255).intValue(),
- new Double(gl * 255).intValue(), new Double(bl * 255).intValue()),
- Color.rgb(new Double(rp * 255).intValue(),
- new Double(gp * 255).intValue(), new Double(bp * 255).intValue()),
+ Color.rgb(Double.valueOf(rl * 255).intValue(),
+ Double.valueOf(gl * 255).intValue(), Double.valueOf(bl * 255).intValue()),
+ Color.rgb(Double.valueOf(rp * 255).intValue(),
+ Double.valueOf(gp * 255).intValue(), Double.valueOf(bp * 255).intValue()),
null, null);
// for fun, configure interpolation on the formatter:
@@ -100,8 +106,9 @@ public int getCount() {
return NUM_PLOTS;
}
+ @NonNull
@Override
- public View getView(int pos, View convertView, ViewGroup parent) {
+ public View getView(int pos, View convertView, @NonNull ViewGroup parent) {
LayoutInflater inf = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = convertView;
diff --git a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java b/demoapp/src/main/java/com/androidplot/demos/MainActivity.java
deleted file mode 100644
index 71ef659e..00000000
--- a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * 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.androidplot.demos;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import com.crittercism.app.Crittercism;
-
-public class MainActivity extends Activity {
-
- private static final String TAG = MainActivity.class.getName();
-
- // DO *NOT* CHANGE THIS LINE! (CI-MATCH-POPULATE)
- private static final String CRITTERCISM_APP_ID = null;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (CRITTERCISM_APP_ID != null) {
- Log.d(TAG, "Crittercism initialized.");
- Crittercism.initialize(getApplicationContext(), CRITTERCISM_APP_ID);
- }
-
- setContentView(R.layout.main);
-
- Button startScatterPlotExButton = (Button) findViewById(R.id.startScatterExButton);
- startScatterPlotExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, ScatterPlotActivity.class));
- }
- });
-
- Button startSimplePieExButton = (Button) findViewById(R.id.startSimplePieExButton);
- startSimplePieExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, SimplePieChartActivity.class));
- }
- });
-
- Button startDynamicXYExButton = (Button) findViewById(R.id.startDynamicXYExButton);
- startDynamicXYExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, DynamicXYPlotActivity.class));
- }
- });
-
- Button startCandlestickExButton = (Button) findViewById(R.id.startCandlestickExButton);
- startCandlestickExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, CandlestickChartActivity.class));
- }
- });
-
- Button startSimpleXYExButton = (Button) findViewById(R.id.startSimpleXYExButton);
- startSimpleXYExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, SimpleXYPlotActivity.class));
- }
- });
-
- Button startBarPlotExButton = (Button) findViewById(R.id.startBarPlotExButton);
- startBarPlotExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, BarPlotExampleActivity.class));
- }
- });
-
- Button startOrSensorExButton = (Button) findViewById(R.id.startOrSensorExButton);
- startOrSensorExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, OrientationSensorExampleActivity.class));
- }
- });
-
- Button startTimeSeriesExButon = (Button) findViewById(R.id.startTimeSeriesExButton);
- startTimeSeriesExButon.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, TimeSeriesActivity.class));
- }
- });
-
- Button startStepChartExButton = (Button) findViewById(R.id.startStepChartExButton);
- startStepChartExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, StepChartExampleActivity.class));
- }
- });
-
- Button startScrollZoomExButton = (Button) findViewById(R.id.startScrollZoomButton);
- startScrollZoomExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, TouchZoomExampleActivity.class));
- }
- });
-
- Button startXyRegionExampleButton = (Button) findViewById(R.id.startXyRegionExampleButton);
- startXyRegionExampleButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, XYRegionExampleActivity.class));
- }
- });
-
-
- Button listViewExButton = (Button) findViewById(R.id.startXyListViewExButton);
- listViewExButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, ListViewActivity.class));
- }
- });
-
- Button startXYPlotWithBgImgExampleButton = (Button) findViewById(R.id.startXYPlotWithBgImgExample);
- startXYPlotWithBgImgExampleButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, XYPlotWithBgImgActivity.class));
- }
- });
-
- // ECG
- Button startECGExampleButton = (Button) findViewById(R.id.startECGExample);
- startECGExampleButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, ECGExample.class));
- }
- });
-
- // f(x) plot
- Button fxPlotExampleButton = (Button) findViewById(R.id.fxPlotExample);
- fxPlotExampleButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, FXPlotExampleActivity.class));
- }
- });
-
- // bubble chart
- Button bubbleChartButton = (Button) findViewById(R.id.bubbleChartExample);
- bubbleChartButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- startActivity(new Intent(MainActivity.this, BubbleChartActivity.class));
- }
- });
- }
-}
diff --git a/demoapp/src/main/java/com/androidplot/demos/MainActivity.kt b/demoapp/src/main/java/com/androidplot/demos/MainActivity.kt
new file mode 100644
index 00000000..6632af40
--- /dev/null
+++ b/demoapp/src/main/java/com/androidplot/demos/MainActivity.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2021 AndroidPlot.com
+ *
+ * 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.androidplot.demos
+
+import android.app.Activity
+import android.os.Bundle
+import android.content.Intent
+import com.androidplot.demos.databinding.MainBinding
+
+class MainActivity : Activity() {
+
+ private lateinit var binding: MainBinding
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = MainBinding.inflate(layoutInflater)
+
+ binding.animatedXYPlotExButton.setOnClickListener {
+ startActivity(Intent(this, AnimatedXYPlotActivity::class.java))
+ }
+
+ binding.startScatterExButton.setOnClickListener {
+ startActivity(Intent(this, ScatterPlotActivity::class.java))
+ }
+
+ binding.startSimplePieExButton.setOnClickListener {
+ startActivity(Intent(this, SimplePieChartActivity::class.java))
+ }
+
+ binding.startDynamicXYExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, DynamicXYPlotActivity::class.java))
+ }
+
+ binding.startCandlestickExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, CandlestickChartActivity::class.java))
+ }
+
+ binding.startSimpleXYExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, SimpleXYPlotActivity::class.java))
+ }
+
+ binding.startBarPlotExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, BarPlotExampleActivity::class.java))
+ }
+
+ binding.startOrSensorExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, OrientationSensorExampleActivity::class.java))
+ }
+
+ binding.startDualScaleExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, DualScaleActivity::class.java))
+ }
+
+ binding.startTimeSeriesExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, TimeSeriesActivity::class.java))
+ }
+
+ binding.startStepChartExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, StepChartExampleActivity::class.java))
+ }
+
+ binding.startScrollZoomButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, TouchZoomExampleActivity::class.java))
+ }
+
+ binding.startXyRegionExampleButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, XYRegionExampleActivity::class.java))
+ }
+
+ binding.startXyListViewExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, ListViewActivity::class.java))
+ }
+
+ binding.startXyRecyclerViewExButton.setOnClickListener {
+ startActivity(Intent(this@MainActivity, RecyclerViewActivity::class.java))
+ }
+
+ binding.startXYPlotWithBgImgExample.setOnClickListener {
+ startActivity(Intent(this@MainActivity, XYPlotWithBgImgActivity::class.java))
+ }
+
+ binding.startECGExample.setOnClickListener {
+ startActivity(Intent(this@MainActivity, ECGExample::class.java))
+ }
+
+ binding.fxPlotExample.setOnClickListener {
+ startActivity(Intent(this@MainActivity, FXPlotExampleActivity::class.java))
+ }
+
+ binding.bubbleChartExample.setOnClickListener {
+ startActivity(Intent(this@MainActivity, BubbleChartActivity::class.java))
+ }
+
+ setContentView(binding.root)
+ }
+}
\ No newline at end of file
diff --git a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java
index 3ef6d04f..71e721d8 100644
--- a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java
@@ -28,6 +28,7 @@
import android.widget.CheckBox;
import android.widget.CompoundButton;
import com.androidplot.Plot;
+import com.androidplot.util.PixelUtils;
import com.androidplot.util.PlotStatistics;
import com.androidplot.util.Redrawer;
import com.androidplot.xy.*;
@@ -163,7 +164,8 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
BarRenderer barRenderer = aprLevelsPlot.getRenderer(BarRenderer.class);
if(barRenderer != null) {
// make our bars a little thicker than the default so they can be seen better:
- barRenderer.setBarWidth(25);
+ barRenderer.setBarGroupWidth(
+ BarRenderer.BarGroupWidthMode.FIXED_WIDTH, PixelUtils.dpToPix(18));
}
// register for orientation sensor events:
diff --git a/demoapp/src/main/java/com/androidplot/demos/RecyclerViewActivity.kt b/demoapp/src/main/java/com/androidplot/demos/RecyclerViewActivity.kt
new file mode 100644
index 00000000..194ee6e0
--- /dev/null
+++ b/demoapp/src/main/java/com/androidplot/demos/RecyclerViewActivity.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 AndroidPlot.com
+ *
+ * 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.androidplot.demos
+
+import android.app.Activity
+import android.graphics.Color
+import com.androidplot.ui.SeriesBundle
+import android.os.Bundle
+import android.view.ViewGroup
+import android.view.LayoutInflater
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.androidplot.demos.databinding.RecyclerviewExampleBinding
+import com.androidplot.demos.databinding.RecyclerviewExampleItemBinding
+import com.androidplot.util.PixelUtils
+import com.androidplot.xy.*
+import java.util.*
+
+class RecyclerViewActivity : Activity() {
+ private lateinit var binding: RecyclerviewExampleBinding
+
+ companion object {
+ private const val NUM_PLOTS = 10
+ private const val NUM_POINTS_PER_SERIES = 10
+ private const val NUM_SERIES_PER_PLOT = 5
+ }
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ PixelUtils.init(this)
+
+ binding = RecyclerviewExampleBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ binding.recyclerView.setHasFixedSize(true)
+ binding.recyclerView.layoutManager = LinearLayoutManager(this)
+ binding.recyclerView.adapter = MyRecyclerViewAdapter()
+ }
+
+ class MyRecyclerViewHolder(
+ private val binding: RecyclerviewExampleItemBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(data: List>, title: String) {
+ val plot = binding.plot
+ plot.clear()
+ plot.title.text = title
+ data.map { plot.addSeries(it.series, it.formatter) }
+ plot.redraw()
+ }
+ }
+
+ class MyRecyclerViewAdapter : RecyclerView.Adapter() {
+ private val seriesData = generateData()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyRecyclerViewHolder {
+ val itemBinding = RecyclerviewExampleItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return MyRecyclerViewHolder(itemBinding)
+ }
+
+ override fun onBindViewHolder(holder: MyRecyclerViewHolder, position: Int) {
+ holder.bind(seriesData[position], "Series $position")
+ }
+
+ override fun getItemCount() = seriesData.size
+
+ private fun generateData(): List>> {
+ val theData = mutableListOf>>()
+ fun generateBundle(seriesLabel: String): SeriesBundle {
+ val generator = Random()
+ val nums = ArrayList()
+ for (j in 0 until NUM_POINTS_PER_SERIES) {
+ nums.add(generator.nextFloat())
+ }
+
+ val formatter = LineAndPointFormatter(
+ Color.rgb(
+ java.lang.Double.valueOf(Math.random() * 255).toInt(),
+ java.lang.Double.valueOf(Math.random() * 255).toInt(),
+ java.lang.Double.valueOf(Math.random() * 255).toInt()
+ ),
+ Color.rgb(
+ java.lang.Double.valueOf(Math.random() * 255).toInt(),
+ java.lang.Double.valueOf(Math.random() * 255).toInt(),
+ java.lang.Double.valueOf(Math.random() * 255).toInt()
+ ),
+ null, null
+ )
+
+ // for fun, configure interpolation on the formatter:
+ formatter.interpolationParams = CatmullRomInterpolator.Params(
+ 20,
+ CatmullRomInterpolator.Type.Centripetal
+ )
+
+ return SeriesBundle(
+ SimpleXYSeries(
+ nums,
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY,
+ seriesLabel
+ ),
+ formatter
+ )
+ }
+
+ for (i in 0 until NUM_PLOTS) {
+ val seriesList: MutableList> =
+ ArrayList(NUM_SERIES_PER_PLOT)
+
+ for (k in 0 until NUM_SERIES_PER_PLOT) {
+ seriesList.add(generateBundle("S$k"))
+ }
+ theData.add(seriesList)
+ }
+ return theData
+ }
+ }
+}
\ No newline at end of file
diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java
index 216af0ea..cf419083 100644
--- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java
@@ -16,11 +16,15 @@
package com.androidplot.demos;
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.*;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.SeekBar;
import android.widget.TextView;
import com.androidplot.pie.PieChart;
@@ -49,6 +53,7 @@ public class SimplePieChartActivity extends Activity
private Segment s3;
private Segment s4;
+ @SuppressLint("ClickableViewAccessibility")
@Override
public void onCreate(Bundle savedInstanceState)
{
@@ -59,6 +64,9 @@ public void onCreate(Bundle savedInstanceState)
// initialize our XYPlot reference:
pie = (PieChart) findViewById(R.id.mySimplePieChart);
+ // enable the legend:
+ pie.getLegend().setVisible(true);
+
final float padding = PixelUtils.dpToPix(30);
pie.getPie().setPadding(padding, padding, padding, padding);
@@ -69,10 +77,13 @@ public boolean onTouch(View view, MotionEvent motionEvent) {
PointF click = new PointF(motionEvent.getX(), motionEvent.getY());
if(pie.getPie().containsPoint(click)) {
Segment segment = pie.getRenderer(PieRenderer.class).getContainingSegment(click);
- final boolean isSelected = getFormatter(segment).getOffset() != 0;
- deselectAll();
- setSelected(segment, !isSelected);
- pie.redraw();
+
+ if(segment != null) {
+ final boolean isSelected = getFormatter(segment).getOffset() != 0;
+ deselectAll();
+ setSelected(segment, !isSelected);
+ pie.redraw();
+ }
}
return false;
}
@@ -151,7 +162,39 @@ public void onStopTrackingTouch(SeekBar seekBar) {
pie.getBackgroundPaint().setColor(Color.TRANSPARENT);
}
+ @Override
+ public void onStart() {
+ super.onStart();
+ setupIntroAnimation();
+ }
+
protected void updateDonutText() {
donutSizeTextView.setText(donutSizeSeekBar.getProgress() + "%");
}
+
+ protected void setupIntroAnimation() {
+
+ final PieRenderer renderer = pie.getRenderer(PieRenderer.class);
+ // start with a zero degrees pie:
+
+ renderer.setExtentDegs(0);
+ // animate a scale value from a starting val of 0 to a final value of 1:
+ ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+
+ // use an animation pattern that begins and ends slowly:
+ animator.setInterpolator(new AccelerateDecelerateInterpolator());
+
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ float scale = valueAnimator.getAnimatedFraction();
+ renderer.setExtentDegs(360 * scale);
+ pie.redraw();
+ }
+ });
+
+ // the animation will run for 1.5 seconds:
+ animator.setDuration(1500);
+ animator.start();
+ }
}
diff --git a/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java
index 904f02c7..db3a5a28 100644
--- a/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 AndroidPlot.com
+ * Copyright 2018 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,19 +15,24 @@
*/
package com.androidplot.demos;
+
import android.app.Activity;
-import android.graphics.*;
+import android.graphics.DashPathEffect;
import android.os.Bundle;
+import androidx.annotation.NonNull;
import com.androidplot.util.PixelUtils;
+import com.androidplot.xy.CatmullRomInterpolator;
+import com.androidplot.xy.LineAndPointFormatter;
import com.androidplot.xy.SimpleXYSeries;
+import com.androidplot.xy.XYGraphWidget;
+import com.androidplot.xy.XYPlot;
import com.androidplot.xy.XYSeries;
-import com.androidplot.xy.*;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
-import java.util.*;
+import java.util.Arrays;
/**
* A simple XYPlot
@@ -43,7 +48,7 @@ public void onCreate(Bundle savedInstanceState)
setContentView(R.layout.simple_xy_plot_example);
// initialize our XYPlot reference:
- plot = (XYPlot) findViewById(R.id.plot);
+ plot = findViewById(R.id.plot);
// create a couple arrays of y-values to plot:
final Number[] domainLabels = {1, 2, 3, 6, 7, 8, 9, 10, 13, 14};
@@ -72,7 +77,7 @@ public void onCreate(Bundle savedInstanceState)
PixelUtils.dpToPix(20),
PixelUtils.dpToPix(15)}, 0));
- // just for fun, add some smoothing to the lines:
+ // (optional) add some smoothing to the lines:
// see: http://androidplot.com/smooth-curves-and-androidplot/
series1Format.setInterpolationParams(
new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal));
@@ -86,12 +91,12 @@ public void onCreate(Bundle savedInstanceState)
plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() {
@Override
- public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
+ public StringBuffer format(Object obj, @NonNull StringBuffer toAppendTo, @NonNull FieldPosition pos) {
int i = Math.round(((Number) obj).floatValue());
return toAppendTo.append(domainLabels[i]);
}
@Override
- public Object parseObject(String source, ParsePosition pos) {
+ public Object parseObject(String source, @NonNull ParsePosition pos) {
return null;
}
});
diff --git a/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java
index 5caf8651..4b1fdc35 100644
--- a/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java
@@ -18,12 +18,20 @@
import android.app.Activity;
-import android.graphics.*;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Shader;
import android.os.Bundle;
+import androidx.annotation.NonNull;
+
import com.androidplot.util.PixelUtils;
import com.androidplot.xy.SimpleXYSeries;
+import com.androidplot.xy.StepFormatter;
+import com.androidplot.xy.StepMode;
+import com.androidplot.xy.XYGraphWidget;
+import com.androidplot.xy.XYPlot;
import com.androidplot.xy.XYSeries;
-import com.androidplot.xy.*;
import java.text.DecimalFormat;
import java.text.FieldPosition;
@@ -82,8 +90,8 @@ public void onCreate(Bundle savedInstanceState)
// create a custom getFormatter to draw our state names as range tick labels:
mySimpleXYPlot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).setFormat(new Format() {
@Override
- public StringBuffer format(Object obj, StringBuffer toAppendTo,
- FieldPosition pos) {
+ public StringBuffer format(Object obj, @NonNull StringBuffer toAppendTo,
+ @NonNull FieldPosition pos) {
Number num = (Number) obj;
switch (num.intValue()) {
case 1:
@@ -106,7 +114,7 @@ public StringBuffer format(Object obj, StringBuffer toAppendTo,
}
@Override
- public Object parseObject(String source, ParsePosition pos) {
+ public Object parseObject(String source, @NonNull ParsePosition pos) {
return null;
}
});
diff --git a/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java b/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java
index e457ad0f..12930bd7 100644
--- a/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java
@@ -16,21 +16,38 @@
package com.androidplot.demos;
+import android.annotation.SuppressLint;
import android.app.Activity;
-import android.graphics.*;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
import android.os.Bundle;
+import androidx.annotation.NonNull;
+import com.androidplot.util.PixelUtils;
+import com.androidplot.xy.BoundaryMode;
+import com.androidplot.xy.LineAndPointFormatter;
import com.androidplot.xy.SimpleXYSeries;
-import com.androidplot.xy.XYSeries;
-import com.androidplot.xy.*;
-
-import java.text.*;
+import com.androidplot.xy.StepMode;
+import com.androidplot.xy.XYGraphWidget;
+import com.androidplot.xy.XYPlot;
+
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.Date;
+import java.util.GregorianCalendar;
public class TimeSeriesActivity extends Activity {
+ private static final String SERIES_TITLE = "Signthings in USA";
+
private XYPlot plot1;
+ private SimpleXYSeries series;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -38,43 +55,36 @@ public void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.time_series_example);
plot1 = (XYPlot) findViewById(R.id.plot1);
- Number[] numSightings = {5, 8, 6, 9, 3, 8, 5};
-
- // an array of years in milliseconds:
- Number[] years = {
- 978307200, // 2001
- 998309300,
- 1009843200, // 2002
- 1041379200, // 2003
- 1052012100,
- 1072915200, // 2004
- 1104537600 // 2005
+
+ // these will be our domain index labels:
+ final Date[] years = {
+ new GregorianCalendar(2001, Calendar.JANUARY, 1).getTime(),
+ new GregorianCalendar(2001, Calendar.JULY, 1).getTime(),
+ new GregorianCalendar(2002, Calendar.JANUARY, 1).getTime(),
+ new GregorianCalendar(2002, Calendar.JULY, 1).getTime(),
+ new GregorianCalendar(2003, Calendar.JANUARY, 1).getTime(),
+ new GregorianCalendar(2003, Calendar.JULY, 1).getTime(),
+ new GregorianCalendar(2004, Calendar.JANUARY, 1).getTime(),
+ new GregorianCalendar(2004, Calendar.JULY, 1).getTime(),
+ new GregorianCalendar(2005, Calendar.JANUARY, 1).getTime(),
+ new GregorianCalendar(2005, Calendar.JULY, 1).getTime()
};
- // create our series from our array of nums:
- XYSeries series2 = new SimpleXYSeries(
- Arrays.asList(years),
- Arrays.asList(numSightings),
- "Sightings in USA");
+
+ addSeries(savedInstanceState);
+
+ plot1.setRangeBoundaries(0, 10, BoundaryMode.FIXED);
plot1.getGraph().getGridBackgroundPaint().setColor(Color.WHITE);
plot1.getGraph().getDomainGridLinePaint().setColor(Color.BLACK);
plot1.getGraph().getDomainGridLinePaint().
- setPathEffect(new DashPathEffect(new float[] {1, 1}, 1));
+ setPathEffect(new DashPathEffect(new float[]{1, 1}, 1));
plot1.getGraph().getRangeGridLinePaint().setColor(Color.BLACK);
plot1.getGraph().getRangeGridLinePaint().
- setPathEffect(new DashPathEffect(new float[] {1, 1}, 1));
+ setPathEffect(new DashPathEffect(new float[]{1, 1}, 1));
plot1.getGraph().getDomainOriginLinePaint().setColor(Color.BLACK);
plot1.getGraph().getRangeOriginLinePaint().setColor(Color.BLACK);
- // setup our line fill paint to be a slightly transparent gradient:
- Paint lineFill = new Paint();
- lineFill.setAlpha(200);
-
- LineAndPointFormatter formatter =
- new LineAndPointFormatter(Color.rgb(0, 0, 0), Color.BLUE, Color.RED, null);
- formatter.setFillPaint(lineFill);
plot1.getGraph().setPaddingRight(2);
- plot1.addSeries(series2, formatter);
// draw a domain tick for each year:
plot1.setDomainStep(StepMode.SUBDIVIDE, years.length);
@@ -93,24 +103,65 @@ public void onCreate(Bundle savedInstanceState) {
// create a simple date format that draws on the year portion of our timestamp.
// see http://download.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html
// for a full description of SimpleDateFormat.
- private SimpleDateFormat dateFormat = new SimpleDateFormat("MM-yyyy");
+ @SuppressLint("SimpleDateFormat")
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("MMM yyyy");
@Override
- public StringBuffer format(Object obj, StringBuffer toAppendTo,
- FieldPosition pos) {
-
- // because our timestamps are in seconds and SimpleDateFormat expects milliseconds
- // we multiply our timestamp by 1000:
- long timestamp = ((Number) obj).longValue() * 1000;
- Date date = new Date(timestamp);
- return dateFormat.format(date, toAppendTo, pos);
+ public StringBuffer format(Object obj,
+ @NonNull StringBuffer toAppendTo,
+ @NonNull FieldPosition pos) {
+
+ // this rounding is necessary to avoid precision loss when converting from
+ // double back to int:
+ int yearIndex = (int) Math.round(((Number) obj).doubleValue());
+ return dateFormat.format(years[yearIndex], toAppendTo, pos);
}
@Override
- public Object parseObject(String source, ParsePosition pos) {
+ public Object parseObject(String source, @NonNull ParsePosition pos) {
return null;
}
});
}
+
+ /**
+ * Instantiates our XYSeries, checking the current savedInstanceState for existing series data
+ * to avoid having to regenerate on each resume. If your series data is small and easy to
+ * regenerate (as it is here) then you can skip saving/restoring your series data to
+ * savedInstanceState.
+ * @param savedInstanceState Current saved instance state, if any; may be null.
+ */
+ private void addSeries(Bundle savedInstanceState) {
+ Number[] yVals;
+
+ if(savedInstanceState != null) {
+ yVals = (Number[]) savedInstanceState.getSerializable(SERIES_TITLE);
+ } else {
+ yVals = new Number[]{5, 8, 6, 9, 3, 8, 5, 4, 7, 4};
+ }
+
+ // create our series from our array of nums:
+ series = new SimpleXYSeries(Arrays.asList(yVals),
+ SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, SERIES_TITLE);
+
+ LineAndPointFormatter formatter =
+ new LineAndPointFormatter(Color.rgb(0, 0, 0), Color.RED, Color.RED, null);
+ formatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(10));
+ formatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(5));
+
+ // setup our line fill paint to be a slightly transparent gradient:
+ Paint lineFill = new Paint();
+ lineFill.setAlpha(200);
+
+ formatter.setFillPaint(lineFill);
+
+ plot1.addSeries(series, formatter);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ // persist our series data so we don't have to regenerate each time:
+ bundle.putSerializable(SERIES_TITLE, series.getyVals().toArray(new Number[]{}));
+ }
}
\ No newline at end of file
diff --git a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java
index 5dc26fcd..dfae19d9 100644
--- a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java
@@ -29,8 +29,9 @@
import com.androidplot.xy.*;
public class TouchZoomExampleActivity extends Activity {
- private static final int SERIES_SIZE = 10000;
+ private static final int SERIES_SIZE = 3000;
private static final int SERIES_ALPHA = 255;
+ private static final int NUM_GRIDLINES = 5;
private XYPlot plot;
private PanZoom panZoom;
private Button resetButton;
@@ -40,24 +41,30 @@ public class TouchZoomExampleActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.touch_zoom_example);
- resetButton = (Button) findViewById(R.id.resetButton);
+ resetButton = findViewById(R.id.resetButton);
resetButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
reset();
}
});
- plot = (XYPlot) findViewById(R.id.plot);
+ plot = findViewById(R.id.plot);
// set a fixed origin and a "by-value" step mode so that grid lines will
// move dynamically with the data when the users pans or zooms:
plot.setUserDomainOrigin(0);
plot.setUserRangeOrigin(0);
- plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1000);
- plot.setRangeStep(StepMode.INCREMENT_BY_VAL, 100);
- panSpinner = (Spinner) findViewById(R.id.pan_spinner);
- zoomSpinner = (Spinner) findViewById(R.id.zoom_spinner);
+ // predefine the stepping of both axis
+ // increment will be chosen from list to best fit NUM_GRIDLINES grid lines
+ double[] inc_domain = new double[]{10,50,100,500};
+ double[] inc_range = new double[]{1,5,10,20,50,100};
+ plot.setDomainStepModel(new StepModelFit(plot.getBounds().getxRegion(),inc_domain,NUM_GRIDLINES));
+ plot.setRangeStepModel( new StepModelFit(plot.getBounds().getyRegion(),inc_range,NUM_GRIDLINES));
+
+
+ panSpinner = findViewById(R.id.pan_spinner);
+ zoomSpinner = findViewById(R.id.zoom_spinner);
plot.getGraph().setLinesPerRangeLabel(2);
plot.getGraph().setLinesPerDomainLabel(2);
plot.getGraph().getBackgroundPaint().setColor(Color.TRANSPARENT);
@@ -71,27 +78,17 @@ public void onClick(View view) {
plot.setBorderStyle(Plot.BorderStyle.NONE, null, null);
- panZoom = PanZoom.attach(plot);
- plot.getOuterLimits().set(0, 10000, 0, 1000);
+ panZoom = PanZoom.attach(plot, PanZoom.Pan.BOTH, PanZoom.Zoom.STRETCH_BOTH, PanZoom.ZoomLimit.MIN_TICKS);
+ plot.getOuterLimits().set(0, 3000, 0, 1000);
initSpinners();
// enable autoselect of sampling level based on visible boundaries:
plot.getRegistry().setEstimator(new ZoomEstimator());
- if(savedInstanceState != null && savedInstanceState.containsKey("seriesRegistry")) {
- XYSeriesRegistry registry = (XYSeriesRegistry) savedInstanceState.getSerializable("seriesRegistry");
- plot.setRegistry(registry);
- } else {
- generateSeriesData();
- }
+ generateSeriesData();
reset();
}
- @Override
- public void onSaveInstanceState(Bundle bundle) {
- bundle.putSerializable("seriesRegistry", plot.getRegistry());
- }
-
private void reset() {
plot.setDomainBoundaries(0, 10000, BoundaryMode.FIXED);
plot.setRangeBoundaries(0, 1000, BoundaryMode.FIXED);
@@ -155,6 +152,7 @@ public void onNothingSelected(AdapterView> parent) {
}
});
+
zoomSpinner.setAdapter(
new ArrayAdapter<>(this, R.layout.spinner_item, PanZoom.Zoom.values()));
zoomSpinner.setSelection(panZoom.getZoom().ordinal());
@@ -170,5 +168,19 @@ public void onNothingSelected(AdapterView> parent) {
}
});
}
+
+ // (optional) save the current pan/zoom state
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ bundle.putSerializable("pan-zoom-state", panZoom.getState());
+ }
+
+ // (optional) restore the previously saved pan/zoom state
+ @Override
+ public void onRestoreInstanceState(Bundle bundle) {
+ PanZoom.State state = (PanZoom.State) bundle.getSerializable("pan-zoom-state");
+ panZoom.setState(state);
+ plot.redraw();
+ }
}
diff --git a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java
index c9cc953d..0f48c3c2 100644
--- a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java
+++ b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java
@@ -69,8 +69,8 @@ public class XYRegionExampleActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.xyregion_example);
- plot = (XYPlot) findViewById(R.id.xyRegionExamplePlot);
- timCB = (CheckBox) findViewById(R.id.s1CheckBox);
+ plot = findViewById(R.id.xyRegionExamplePlot);
+ timCB = findViewById(R.id.s1CheckBox);
timCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
@@ -78,7 +78,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
}
});
- nickCB = (CheckBox) findViewById(R.id.s2CheckBox);
+ nickCB = findViewById(R.id.s2CheckBox);
nickCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
@@ -86,7 +86,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
}
});
- r2CheckBox = (CheckBox) findViewById(R.id.r2CheckBox);
+ r2CheckBox = findViewById(R.id.r2CheckBox);
r2CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
@@ -94,7 +94,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
}
});
- r3CheckBox = (CheckBox) findViewById(R.id.r3CheckBox);
+ r3CheckBox = findViewById(R.id.r3CheckBox);
r3CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
@@ -102,7 +102,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
}
});
- r4CheckBox = (CheckBox) findViewById(R.id.r4CheckBox);
+ r4CheckBox = findViewById(R.id.r4CheckBox);
r4CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
diff --git a/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java b/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java
index fe3627b3..592fbce3 100644
--- a/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java
+++ b/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java
@@ -25,6 +25,8 @@
import android.widget.RemoteViews;
import com.androidplot.demos.R;
import com.androidplot.ui.*;
+import com.androidplot.util.PixelUtils;
+import com.androidplot.xy.XYGraphWidget;
import com.androidplot.xy.XYSeries;
import com.androidplot.xy.LineAndPointFormatter;
import com.androidplot.xy.SimpleXYSeries;
@@ -43,10 +45,6 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a
plot.getGraph().setMargins(0, 0, 0 , 0);
plot.getGraph().setPadding(0, 0, 0, 0);
-// plot.getGraphWidget().getDomainLineLabelPaint().setTextSize(PixelUtils.spToPix(10));
-// plot.getGraphWidget().getRangeLineLabelPaint().setTextSize(PixelUtils.spToPix(10));
-// plot.getGraphWidget().getDomainOriginLineLabelPaint().setTextSize(PixelUtils.spToPix(10));
-// plot.getGraphWidget().getRangeOriginLineLabelPaint().setTextSize(PixelUtils.spToPix(10));
plot.getGraph().position(0, HorizontalPositioning.ABSOLUTE_FROM_LEFT, 0,
VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.LEFT_TOP);
@@ -55,6 +53,15 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a
plot.getLayoutManager().moveToTop(plot.getTitle());
+ plot.getGraph().setLineLabelEdges(XYGraphWidget.Edge.LEFT, XYGraphWidget.Edge.BOTTOM);
+ plot.getGraph().getLineLabelInsets().setLeft(PixelUtils.dpToPix(16));
+ plot.getGraph().getLineLabelInsets().setBottom(PixelUtils.dpToPix(4));
+ plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).getPaint().setColor(Color.RED);
+ plot.getGraph().getGridInsets().setTop(PixelUtils.dpToPix(12));
+ plot.getGraph().getGridInsets().setRight(PixelUtils.dpToPix(12));
+ plot.getGraph().getGridInsets().setLeft(PixelUtils.dpToPix(36));
+ plot.getGraph().getGridInsets().setBottom(PixelUtils.dpToPix(16));
+
plot.measure(w, h);
plot.layout(0, 0, w, h);
diff --git a/demoapp/src/main/res/layout/bar_plot_example.xml b/demoapp/src/main/res/layout/bar_plot_example.xml
index e607f973..3182342d 100644
--- a/demoapp/src/main/res/layout/bar_plot_example.xml
+++ b/demoapp/src/main/res/layout/bar_plot_example.xml
@@ -1,5 +1,4 @@
-
-
+ xmlns:ap="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ ap:title="Growth" />
+ android:layout_height="wrap_content" />
+ android:layout_height="wrap_content" />
+ android:layout_height="wrap_content" />
+ android:progress="10" />
+ android:progress="1" />
+ android:checked="true"
+ android:text="Series 1" />
+ android:checked="true"
+ android:text="Series 2" />
\ No newline at end of file
diff --git a/demoapp/src/main/res/layout/demo_app_widget.xml b/demoapp/src/main/res/layout/demo_app_widget.xml
index 85b5fe6b..81977453 100644
--- a/demoapp/src/main/res/layout/demo_app_widget.xml
+++ b/demoapp/src/main/res/layout/demo_app_widget.xml
@@ -17,14 +17,13 @@
-->
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
-
-
+
\ No newline at end of file
diff --git a/demoapp/src/main/res/layout/dual_scale_example.xml b/demoapp/src/main/res/layout/dual_scale_example.xml
new file mode 100644
index 00000000..f442b4ff
--- /dev/null
+++ b/demoapp/src/main/res/layout/dual_scale_example.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/src/main/res/layout/dynamic_xyplot_example.xml b/demoapp/src/main/res/layout/dynamic_xyplot_example.xml
index ddfd0cba..5b0c5644 100644
--- a/demoapp/src/main/res/layout/dynamic_xyplot_example.xml
+++ b/demoapp/src/main/res/layout/dynamic_xyplot_example.xml
@@ -1,5 +1,4 @@
-
-
+ xmlns:ap="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ ap:legendTextSize="15sp"
+ ap:rangeTitle="Range"
+ ap:title="A Dynamic XY Plot" />
diff --git a/demoapp/src/main/res/layout/main.xml b/demoapp/src/main/res/layout/main.xml
index 7295e172..3a2d873f 100644
--- a/demoapp/src/main/res/layout/main.xml
+++ b/demoapp/src/main/res/layout/main.xml
@@ -32,89 +32,124 @@
+
+
+
+
+
+
diff --git a/demoapp/src/main/res/layout/pie_chart.xml b/demoapp/src/main/res/layout/pie_chart.xml
index e6fd0b0c..3722d9e2 100644
--- a/demoapp/src/main/res/layout/pie_chart.xml
+++ b/demoapp/src/main/res/layout/pie_chart.xml
@@ -16,7 +16,8 @@
-->
-
+
-
+
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/demoapp/src/main/res/values-ldpi/dimens.xml b/demoapp/src/main/res/layout/recyclerview_example_item.xml
similarity index 69%
rename from demoapp/src/main/res/values-ldpi/dimens.xml
rename to demoapp/src/main/res/layout/recyclerview_example_item.xml
index a58e6c64..b8392dc9 100644
--- a/demoapp/src/main/res/values-ldpi/dimens.xml
+++ b/demoapp/src/main/res/layout/recyclerview_example_item.xml
@@ -1,4 +1,5 @@
+
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/demoapp/src/main/res/layout/simple_xy_plot_example.xml b/demoapp/src/main/res/layout/simple_xy_plot_example.xml
index fecf1241..08a2c5af 100644
--- a/demoapp/src/main/res/layout/simple_xy_plot_example.xml
+++ b/demoapp/src/main/res/layout/simple_xy_plot_example.xml
@@ -18,16 +18,19 @@
+ android:layout_width="match_parent"
+ android:orientation="vertical">
+ ap:lineLabelRotationBottom="-45"
+ android:layout_weight="1"/>
+
\ No newline at end of file
diff --git a/demoapp/src/main/res/layout/step_chart_example.xml b/demoapp/src/main/res/layout/step_chart_example.xml
index 9ce501ab..d72dc051 100644
--- a/demoapp/src/main/res/layout/step_chart_example.xml
+++ b/demoapp/src/main/res/layout/step_chart_example.xml
@@ -1,5 +1,4 @@
-
-
+ xmlns:ap="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ android:id="@+id/stepChartExamplePlot"
+ style="@style/APDefacto.Dark"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ ap:domainTitle="time (secs)"
+ ap:rangeTitle="server state"
+ ap:title="HTTP Server State (15 Sec)" />
diff --git a/demoapp/src/main/res/layout/time_series_example.xml b/demoapp/src/main/res/layout/time_series_example.xml
index 02b5c184..37b59957 100644
--- a/demoapp/src/main/res/layout/time_series_example.xml
+++ b/demoapp/src/main/res/layout/time_series_example.xml
@@ -17,21 +17,27 @@
-->
+ xmlns:ap="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ android:id="@+id/plot1"
+ style="@style/APDefacto.Light"
+ renderMode="use_main_thread"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ ap:backgroundColor="@color/ap_white"
+ ap:graphBackgroundColor="@color/ap_white"
+ ap:gridInsetBottom="40dp"
+ ap:gridInsetLeft="35dp"
+ ap:lineLabelInsetBottom="25dp"
+ ap:lineLabelInsetLeft="25dp"
+ ap:lineLabelRotationBottom="-45"
+ ap:paddingBottom="10dp"
+ ap:paddingLeft="10dp"
+ ap:paddingRight="10dp"
+ ap:paddingTop="10dp"
+ ap:title="@string/ts_plot1_title" />
\ No newline at end of file
diff --git a/demoapp/src/main/res/layout/touch_zoom_example.xml b/demoapp/src/main/res/layout/touch_zoom_example.xml
index 654b0c67..5f3bd5da 100644
--- a/demoapp/src/main/res/layout/touch_zoom_example.xml
+++ b/demoapp/src/main/res/layout/touch_zoom_example.xml
@@ -1,5 +1,4 @@
-
-
+ xmlns:ap="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/ap_black"
+ android:orientation="vertical">
+ ap:renderMode="use_background_thread"
+ ap:title="A Simple XY Plot" />
+ android:layout_weight="5"
+ android:orientation="vertical">
+ android:text="Pan"
+ android:textSize="12sp" />
+ android:layout_height="wrap_content"
+ android:padding="0dp">
@@ -63,29 +63,30 @@
+ android:layout_weight="3"
+ android:orientation="vertical">
+ android:text="Zoom"
+ android:textSize="12sp" />
+ android:layout_height="wrap_content"
+ android:padding="0dp">
+
+ android:text="Reset" />
\ No newline at end of file
diff --git a/demoapp/src/main/res/values-hdpi/dimens.xml b/demoapp/src/main/res/values-hdpi/dimens.xml
index df9a1f9f..2c320eef 100644
--- a/demoapp/src/main/res/values-hdpi/dimens.xml
+++ b/demoapp/src/main/res/values-hdpi/dimens.xml
@@ -1,21 +1,3 @@
-
-
-
-
30dp20dp
diff --git a/demoapp/src/main/res/values/dimens.xml b/demoapp/src/main/res/values/dimens.xml
index a4ea138d..23de7062 100644
--- a/demoapp/src/main/res/values/dimens.xml
+++ b/demoapp/src/main/res/values/dimens.xml
@@ -1,21 +1,3 @@
-
-
-
-
diff --git a/demoapp/src/main/res/values/strings.xml b/demoapp/src/main/res/values/strings.xml
index 37ade4d0..9d2697f4 100644
--- a/demoapp/src/main/res/values/strings.xml
+++ b/demoapp/src/main/res/values/strings.xml
@@ -19,5 +19,5 @@
Androidplot DemosA Simple XY PlotTime Series
- Yearly UFO Sightings
+ UFO Sightings
diff --git a/docs/advanced_xy_plot.md b/docs/advanced_xy_plot.md
index 24b5502c..76b1552f 100644
--- a/docs/advanced_xy_plot.md
+++ b/docs/advanced_xy_plot.md
@@ -15,20 +15,28 @@ By default, Androidplot iterates over every element in each series every render
determine it's current min/max values. This is necessary in order to support dynamic plots where
data can change at any moment and invalidate the previously calculated min/max vals.
-FastXYSeries allows the developer to avoid the high overhead of this iteration by calculating these
-min/max values more efficiently. For example, if you have data whose YVals are strictly ascending,
-the first and last YVal of the series will always be the min/max YVals respectively.
+FastXYSeries allows the developer to provide a more efficient algorithm to obtain min/max XVals
+and avoid the high overhead of this iteration. For example, if you know your XVals will always
+be in strict ascending order then the first and last XVal of the series will always contain the
+min/max XVals respectively.
XYSeries implementation supports a min/max algorithm implementation that is more efficient than doing
a comparison on each point of the series via iteration. It's a good idea to implement this interface
if your series will contain more than about 500 points
## OrderedXYSeries
-If the YVals of your series are in ascending order, implementing this interface provides a hint to
+If the XVals of your series are in ascending order, implementing this interface provides a hint to
the series renderer that allows it to avoid iterating over points that are outside the screen's
visible domain. For larger data sets, implementing this interface can mean the difference between
smooth animations and freezing.
+## ScalingXYSeries
+Wraps any other instance of XYSeries and provides a simple interface for dynamically
+scaling x and/or y values. A popular use case for dynamic scaling is to create an animated intro
+for your XYSeries where yVals increase (or decrease in the case of negative values) from 0 towards
+their original value. The [AnimatedXYPlotActivity](../demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java)
+in the DemoApp is one example on how this can be accomplished.
+
## SampledXYSeries
SampledXYSeries is meant for use with extremely large datasets. Given a series, multiple sampled
series instances at stepped resolutions are generated for faster rendering.
@@ -62,6 +70,51 @@ plot.getRegistry().setEstimator(new ZoomEstimator());
If you want to take advantage of the performance benefits of sampling but don't need pan/zoom support
check out the Sampling section below.
+## NormedXYSeries
+A convenience wrapper to simplify normalizing XYSeries data in the range of 0 to 1. Usage is straightforward:
+
+```java
+XYSeries normedSeries = new NormedXYSeries(series);
+```
+
+which is equivalent to:
+
+```java
+XYSeries normedSeries = new NormedXYSeries(series, null, new Norm(null, 0, false));
+```
+
+The first null argument pertains to the normalization being applied to the x axis values. While normalization
+can certainly be applied on this axis, it's typically unused, which is why the single argument constructor
+defaults to a null value here.
+
+Both examples auto normalize the passed in series, maximizing the resolution of the
+output result. Sometimes however, it's desirable to control the output resolution for the purpose
+of visually shifting the result up or down in the graph. This can be done by using the `NormedXYSeries.Norm`
+constructor's second and third arguments: `offset` and `useOffsetCompression`.
+
+##### offset
+This value is added to the original normalized value to effectively shift the series data up or down
+within the normalized range. If the offset is large enough and offset compression is not used, this
+can cause normalized values to exceed the norm range of 0 to 1.
+
+##### useOffsetCompression
+If you want to shift a normalized series up or down in the graph but do not want the shift to potentially
+move values offscreen, you can set `useOffsetComression` to true. This tells `NormedXYSeries` to shrink
+the scale of the associated series relative to the offset being used to ensure that normed values
+stay within the range of 0 to 1. If set to true and you supply an offset <0 or >1, an
+`IllegalArgumentException` will be thrown.
+
+We've not talked about the first argument to the `NormedXYSeries.Norm` constructor: `minMax`. This is an
+optional optimization value that reflects min/max values in the series data being passed in. If they
+are known, you can pass these in to speed up normalization, otherwise just pass in null and they will
+be auto calculated.
+
+IMPORTANT: If your series data is dynamic and it's min/max values change at runtime, you'll need to invoke
+`NormedXYSeries.normalize(Norm, Norm)` immediately following each change in order to maintain accuracy.
+
+There's a [reference implementation](../demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java)
+of a dual scale plot that demonstrates NormedXYSeries usage in the DemoApp.
+
# Sampling
Series size is generally the biggest factor when it comes to rendering performance. If you're plotting
very large datasets, its generally a good idea to sample the data to improve performance.
@@ -70,20 +123,32 @@ the 200 points that most accurately represent the profile of the original series
```java
// An instance of any implementation of XYSeries. Assume it's size is 10,000
-XYSeries origalSeries = ...;
+XYSeries originalSeries = ...;
// Sampled series of size 200:
EditableXYSeries sampledSeries = new FixedSizeEditableXYSeries(
- origalSeries.getTitle(), 200);
+ originalSeries.getTitle(), 200);
// an instance of an implementation of Sampler:
Sampler sampler = ...
// do the actual sampling:
-new LTTBSampler().run(origalSeries, sampledSeries);
+new LTTBSampler().run(originalSeries, sampledSeries);
```
Currently LTTBSampler is the only available implementation.
# Storing series data in onSaveInstanceState
-TODO
\ No newline at end of file
+If your series data requires a non trivial amount of preprocessing (subsampling etc.) or your data comes
+from a dynamic source, you'll likely want to persist your series data when your Activity saves its
+instance state. There are a few caveats to be aware of:
+
+* You can only persist about 1mb worth of data at a time so if your series data is much larger than that
+you'll need to find a creative solution to the problem
+* Due to [quirks in the way Android persists data](http://stackoverflow.com/questions/12300886/linkedlist-put-into-intent-extra-gets-recast-to-arraylist-when-retrieving-in-nex)
+`XYSeries` implementations such as `SimpleXYSeries` that use `LinkedList` instances to store data cannot be serialized directly.
+* Formatters generally cannot be persisted as they typically contain instances of `Paint` that cannot be serialized directly..
+
+Due to these limitations we suggest storing `XYSeries` data into an array or `ArrayList` and using that to
+instantiate your `XYSeries`. The DemoApp's [Time Series Example](../demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java)
+contains a full source example.
\ No newline at end of file
diff --git a/docs/attrs.md b/docs/attrs.md
new file mode 100644
index 00000000..afda917c
--- /dev/null
+++ b/docs/attrs.md
@@ -0,0 +1,598 @@
+_This documentation is auto generated from [attrs.xml](../androidplot-core/src/main/res/values/attrs.xml)._
+
+# Androidplot XML Attributes
+Attributes are broken down by element followed by either their type or list of accepted values.
+Supported Elements:
+
+* [Plot](#Plot)
+* [XYPlot](#XYPlot)
+* [PieChart](#PieChart)
+
+## Plot
+Plot's attrs are available in all Plot types.
+
+### markupEnabled
+__boolean__
+
+### renderMode
+* use_background_thread
+* use_main_thread
+
+### marginTop
+__dimension__
+
+### marginBottom
+__dimension__
+
+### marginLeft
+__dimension__
+
+### marginRight
+__dimension__
+
+### paddingTop
+__dimension__
+
+### paddingBottom
+__dimension__
+
+### paddingLeft
+__dimension__
+
+### paddingRight
+__dimension__
+
+### title
+__dimension__
+
+### titleTextSize
+__string__
+
+### titleTextColor
+__dimension__
+
+### backgroundColor
+__color__
+
+### borderColor
+__color__
+
+### borderThickness
+__color__
+
+## XYPlot
+XML attributes for the [XYPlot](xyplot.md) class.
+
+### previewMode
+TODO
+
+### domainStepMode
+* subdivide
+* increment_by_val
+* increment_by_pixels
+
+### domainStep
+__dimension|float|integer__
+
+### rangeStepMode
+* subdivide
+* increment_by_val
+* increment_by_pixels
+
+### rangeStep
+__dimension|float|integer__
+
+### domainTitle
+__string__
+
+### domainTitleTextColor
+__color__
+
+### domainTitleTextSize
+__dimension__
+
+### domainTitleHeightMode
+* absolute
+* relative
+* fill
+
+### domainTitleWidthMode
+* absolute
+* relative
+* fill
+
+### domainTitleHeight
+__dimension|float|integer__
+
+### domainTitleWidth
+__dimension|float|integer__
+
+### domainTitleHorizontalPositioning
+* absolute_from_left
+* absolute_from_right
+* absolute_from_center
+* relative_from_left
+* relative_from_right
+* relative_from_center
+
+`HorizontalPositioning` component of the `HorizontalPosition` of the `TextLabelWidget`
+that displays the domain title.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### domainTitleVerticalPositioning
+* absolute_from_top
+* absolute_from_bottom
+* absolute_from_center
+* relative_from_top
+* relative_from_bottom
+* relative_from_center
+
+`VerticalPositioning` component of the `VerticalPosition` of the `TextLabelWidget`
+that displays the domain title.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### domainTitleHorizontalPosition
+__dimension|float|integer__
+
+`float` value component of the `HorizontalPosition` of the `TextLabelWidget`
+that displays the domain title.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### domainTitleVerticalPosition
+__dimension|float|integer__
+
+`float` value component of the `VerticalPosition` of the `TextLabelWidget`
+that displays the domain title.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### domainTitleAnchor
+* top_middle
+* left_top
+* left_middle
+* left_bottom
+* right_top
+* right_middle
+* right_bottom
+* bottom_middle
+* center
+
+### domainTitleVisible
+__boolean__
+
+### rangeTitle
+__string__
+
+### rangeTitleTextColor
+__color__
+
+### rangeTitleTextSize
+__dimension__
+
+### rangeTitleHeightMode
+* absolute
+* relative
+* fill
+
+### rangeTitleWidthMode
+* absolute
+* relative
+* fill
+
+### rangeTitleHeight
+__dimension|float|integer__
+
+### rangeTitleWidth
+__dimension|float|integer__
+
+### rangeTitleHorizontalPositioning
+* absolute_from_left
+* absolute_from_right
+* absolute_from_center
+* relative_from_left
+* relative_from_right
+* relative_from_center
+
+### rangeTitleVerticalPositioning
+* absolute_from_top
+* absolute_from_bottom
+* absolute_from_center
+* relative_from_top
+* relative_from_bottom
+* relative_from_center
+
+### rangeTitleHorizontalPosition
+__dimension|float|integer__
+
+### rangeTitleVerticalPosition
+__dimension|float|integer__
+
+### rangeTitleAnchor
+* top_middle
+* left_top
+* left_middle
+* left_bottom
+* right_top
+* right_middle
+* right_bottom
+* bottom_middle
+* center
+
+### rangeTitleVisible
+__boolean__
+
+### drawGridOnTop
+__boolean__
+
+(default is false) When set to true, grid lines are drawn on top of rendered series data
+instead of underneath.
+
+### graphHeightMode
+* absolute
+* relative
+* fill
+
+### graphWidthMode
+* absolute
+* relative
+* fill
+
+### graphHeight
+__dimension|float|integer__
+
+### graphWidth
+__dimension|float|integer__
+
+### graphRotation
+* none
+* ninety_degrees
+* negative_ninety_degrees
+* one_hundred_eighty_degrees
+
+### graphHorizontalPositioning
+* absolute_from_left
+* absolute_from_right
+* absolute_from_center
+* relative_from_left
+* relative_from_right
+* relative_from_center
+
+`HorizontalPositioning` component of the `HorizontalPosition` of the `XYGraphWidget`.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### graphVerticalPositioning
+* absolute_from_top
+* absolute_from_bottom
+* absolute_from_center
+* relative_from_top
+* relative_from_bottom
+* relative_from_center
+
+`VerticalPositioning` component of the `VerticalPosition` of the `XYGraphWidget`.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### graphHorizontalPosition
+__dimension|float|integer__
+
+`float` value component of the `HorizontalPosition` of the `XYGraphWidget`.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### graphVerticalPosition
+__dimension|float|integer__
+
+`float` value component of the `VerticalPosition` of the `XYGraphWidget`.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### graphAnchor
+* top_middle
+* left_top
+* left_middle
+* left_bottom
+* right_top
+* right_middle
+* right_bottom
+* bottom_middle
+* center
+
+### graphVisible
+__boolean__
+
+### graphMarginTop
+__dimension__
+
+### graphMarginBottom
+__dimension__
+
+### graphMarginLeft
+__dimension__
+
+### graphMarginRight
+__dimension__
+
+### graphPaddingTop
+__dimension__
+
+### graphPaddingBottom
+__dimension__
+
+### graphPaddingLeft
+__dimension__
+
+### graphPaddinRight
+__dimension__
+
+### gridClippingEnabled
+__boolean__
+
+### gridInsetTop
+__dimension__
+
+### gridInsetBottom
+__dimension__
+
+### gridInsetLeft
+__dimension__
+
+### gridInsetRight
+__dimension__
+
+### lineLabelInsetTop
+__dimension__
+
+Top edge of a rectangle relative to the XYGraphWidget
+border upon which line labels will be anchored.
+
+### lineLabelInsetBottom
+__dimension__
+
+Bottom edge of a rectangle relative to the XYGraphWidget
+border upon which line labels will be anchored.
+
+### lineLabelInsetLeft
+__dimension__
+
+Left edge of a rectangle relative to the XYGraphWidget
+border upon which line labels will be anchored.
+
+### lineLabelInsetRight
+__dimension__
+
+Right edge of a rectangle relative to the XYGraphWidget
+border upon which line labels will be anchored.
+
+### lineLabels
+* top
+* bottom
+* left
+* right
+
+Enable line labels on one or more edge of the graph. For example, to enable labels on the left
+and bottom edges:
+```
+ap:lineLabels="left|bottom"
+```
+
+### lineLabelAlignTop
+* left
+* center
+* right
+
+Text alignment of line labels drawn on top edge of the plot. This alignment is applied relative
+to the line label insets defined by `lineLabelInsetTop`.
+
+### lineLabelAlignBottom
+* left
+* center
+* right
+
+Text alignment of line labels drawn on bottom edge of the plot. This alignment is applied relative
+to the line label insets defined by `lineLabelInsetBottom`.
+
+### lineLabelAlignLeft
+* left
+* center
+* right
+
+Text alignment of line labels drawn on left edge of the plot. This alignment is applied relative
+to the line label insets defined by `lineLabelInsetLeft`.
+
+### lineLabelAlignRight
+* left
+* center
+* right
+
+Text alignment of line labels drawn on right edge of the plot. This alignment is applied relative
+to the line label insets defined by `lineLabelInsetRight`.
+
+### lineLabelRotationTop
+__float__
+
+Angle at which line labels on the plot's top edge are drawn.
+
+### lineLabelRotationBottom
+__float__
+
+Angle at which line labels on the plot's bottom edge are drawn.
+
+### lineLabelRotationLet
+__float__
+
+Angle at which line labels on the plot's left edge are drawn.
+
+### lineLabelRotationRight
+__float__
+
+Angle at which line labels on the plot's right edge are drawn.
+
+### domainLineThickness
+__dimension__
+
+### rangeLineThickness
+__dimension__
+
+### domainLineColor
+__color__
+
+### rangeLineColor
+__color__
+
+### domainOriginLineThickness
+__dimension__
+
+### rangeOriginLineThickness
+__dimension__
+
+### domainOriginLineColor
+__color__
+
+### rangeOriginLineColor
+__color__
+
+### lineLabelTextSizeTop
+__dimension__
+
+### lineLabelTextSizeBottom
+__dimension__
+
+### lineLabelTextSizeLeft
+__dimension__
+
+### lineLabelTextSizeRight
+__dimension__
+
+### lineLabelTextColorTop
+__color__
+
+### lineLabelTextColorBottom
+__color__
+
+### lineLabelTextColorLeft
+__color__
+
+### lineLabelTextColorRight
+__color__
+
+### lineExtensionTop
+__dimension__
+
+### lineExtensionBottom
+__dimension__
+
+### lineExtensionLeft
+__dimension__
+
+### lineExtensionRight
+__dimension__
+
+### gridBackgroundColor
+__color__
+
+background color of the grid portion of the XYGraphWidget
+
+### graphBackgroundColor
+__color__
+
+background color of the XYGraphWidget
+
+### legendHeightMode
+* absolute
+* relative
+* fill
+
+### legendWidthMode
+* absolute
+* relative
+* fill
+
+### legendHeight
+__dimension|float|integer__
+
+### legendWidth
+__dimension|float|integer__
+
+### legendHorizontalPositioning
+`HorizontalPositioning` component of the `HorizontalPosition` of the `XYLegendWidget`.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+* absolute_from_left
+* absolute_from_right
+* absolute_from_center
+* relative_from_left
+* relative_from_right
+* relative_from_center
+
+### legendVerticalPositioning
+`VerticalPositioning` component of the `VerticalPosition` of the `XYLegendWidget`.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+* absolute_from_top
+* absolute_from_bottom
+* absolute_from_center
+* relative_from_top
+* relative_from_bottom
+* relative_from_center
+
+### legendHorizontalPosition
+__dimension|float|integer__
+
+`float` value component of the `HorizontalPosition` of the `XYLegendWidget`.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### legendVerticalPosition
+__dimension|float|integer__
+
+`float` value component of the `VerticalPosition` of the `XYLegendWidget`.
+See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation.
+
+### legendAnchor
+* top_middle
+* left_top
+* left_middle
+* left_bottom
+* right_top
+* right_middle
+* right_bottom
+* bottom_middle
+* center
+
+### legendTextSize
+__dimension__
+
+### legendTextColor
+__color__
+
+### legendIconHeightMode
+* absolute
+* relative
+* fill
+
+### legendIconWidthMode
+* absolute
+* relative
+* fill
+
+### legendIconHeight
+__dimension|float|integer__
+
+### legendIconWidth
+__dimension|float|integer__
+
+### legendVisible
+__boolean__
+
+### pieBorderThickness
+__dimension__
+
+Determines how far beyond the graph's edge each domain grid line will extend.
+
+### pieBorderThickness
+__dimension__
+
+Determines how far beyond the graph's edge each range grid line will extend.
+
+## PieChart
+TODO
+
+### pieBorderColor
+__color__
+
+### pieBorderThickness
+__dimension__
+
diff --git a/docs/barchart.md b/docs/barchart.md
index 756aea4c..5b384250 100644
--- a/docs/barchart.md
+++ b/docs/barchart.md
@@ -1,11 +1,13 @@
# Bar Charts
-In Androidplot, Bar Charts are implemented as a special renderer that draws the points of an XYSeries as
-vertical bars with the height of the vertical bar representing that point's y-val.
+Bar Charts are drawn with `BarRenderer` by converting xy values of an `XYSeries` into
+vertical bars. The height of the vertical bar is calculated from the yVal and the base
+of the bar is calculated from the plot's range origin value.

# Basic Usage
-Rendering an XYSeries as a bar chart is as simple as adding the series to an XYPlot with an instance of BarFormatter:
+Rendering an `XYSeries` as a bar chart is as simple as adding the series to an `XYPlot` with an
+instance of `BarFormatter`:
```java
// create a bar formatter with a red fill color and a white outline:
@@ -13,12 +15,30 @@ BarFormatter bf = new BarFormatter(Color.RED, Color.WHITE);
plot.addSeries(series, bf);
```
-# Bar Groups
+# BarRenderer
+Unlike most other renderers, much of the visual configuration of a bar chart is configured through it's `BarRenderer`.
+`BarRenderer` provides methods for setting how the width of each bar should be calculated, the space between each `Bar`
+and what style of visual grouping to use.
+
+
+##### Getting the BarRenderer Instance
+Many of the topics below require access to the `BarRenderer` instance to be set. Each instance of
+`XYPlot` contains it's own unique `Renderer` instances. To retrieve the `BarRenderer`
+instance from an `XYPlot`:
+
+```java
+BarRenderer renderer = plot.getRenderer(BarRenderer.class);
+```
+
+# BarGroup
+A `BarGroup` is automatically generated to group values from one or more series by their index values.
+All `Bar` instances belong to a `BarGroup`, even if there is only a single `XYSeries` that exists.
+
A common use case of bar charts is to represent a group of two or more values for a given interval as
individual bars, for example the number of wins and losses for a baseball team for every month over the course
of a year.
-To model this in Androidplot, create two instances of XYSeries; one for wins and one for losses, each
+To model this in Androidplot, create two instances of `XYSeries`; one for wins and one for losses, each
with exactly 12 elements (one for each day of the month):
```java
@@ -26,7 +46,7 @@ XYSeries wins = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "wins
XYSeries losses = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "losses", 0, 1, 1, 0, 1, 0, 0, 0, 2, 1, 0, 1);
```
-Then, we add both series to the plot with their own formatter:
+Each series is then added to the plot with it's own formatter:
```java
// draw wins bars with a green fill:
@@ -38,22 +58,75 @@ BarFormatter lossesFormatter = new BarFormatter(Color.RED, Color.BLACK);
plot.addSeries(losses, lossesFormatter);
```
-Androidplot then draws pairs of wins/losses bars side-by-side for each of the 12 indexes. Androidplot
-knows to do this because BarRenderer (the renderer used to draw series associated with a BarFormatter)
+The pairs of wins/losses bars are then drawn side-by-side for each of the 12 indexes. Androidplot
+knows to do this because `BarRenderer` (the renderer used to draw series associated with a `BarFormatter`)
extends [GroupRenderer](grouprenderer.md).
-# BarRenderer
-Unlike most other renderers, much of the visual configuration of a bar chart is configured through it's BarRenderer.
-BarRenderer provides methods for setting how the width of each bar should be calculated, the space between each bar
-and what style of visual grouping to use.
+# BarRenderer Styles
+`BarRenderer` provides three grouping styles that may be used when rendering two or more XYSeries:
+`OVERLAID`, `STACKED`, `SIDE_BY_SIDE`
+
+##### OVERLAID (default)
+Bars in the same grouping are overlaid on each-other, the bars being drawn by yVal in descending order.
+
+
+
+
+To use:
+```java
+barRenderer.setBarOrientation(BarRenderer.BarOrientation.OVERLAID);
+```
+
+##### STACKED
+Bars in the same group are stacked on top of each-other. Limitations:
+* Range Origin must be set to 0.
+* All `XYSeries` must contain no negative values.
+
+
-# Grouping Styles
-BarRenderer provides three grouping styles:
+To use:
+```java
+barRenderer.setBarOrientation(BarRenderer.BarOrientation.STACKED);
+```
+
+##### SIDE_BY_SIDE
+Bars in the same group are drawn next to one another.
+
+
+
+To use:
+```java
+barRenderer.setBarOrientation(BarRenderer.BarOrientation.SIDE_BY_SIDE);
+```
+
+By default there is no spacing between bars in the same `BarGroup` in this mode. You can add spacing
+by setting a left and right margin on your BarFormatter instances:
-* **BarRenderer.Style.OVERLAID** (default) - bars in the same grouping are overlaid on each-other, the bars being
-drawn in descending order of y-val.
-* **BarRenderer.Style.STACKED** - bars in the same group are stacked on top of each-other.
-* **BarRenderer.Style.SIDE_BY_SIDE** - bars in the same group are drawn next to one another.
+```java
+barFormatter.setMarginLeft(PixelUtils.dpToPix(1));
+barFormatter.setMarginRight(PixelUtils.dpToPix(1));
+```
+
+# BarGroup Widths & Spacing
+When configuring BarGroup widths and spacing, there are two mutually exclusive methods that can be used;
+`FIXED_WIDTH` and `FIXED_GAP`.
+
+
+
+##### FIXED_WIDTH
+The exact size of the `BarGroup` is specified in pixels and the space between each `BarGroup`
+is dynamically calculated based on that size.
+
+```java
+barRenderer.setBarGroupWidth(BarRenderer.BarGroupWidthMode.FIXED_WIDTH, PixelUtils.dpToPix(25));
+```
+##### FIXED_GAP
+The exact size of the "gap" between each `BarGroup` is specified in pixels and the size of each `BarGroup`
+is dynamically calculated based on that spacing.
+
+```java
+barRenderer.setBarGroupWidth(BarRenderer.BarGroupWidthMode.FIXED_GAP, PixelUtils.dpToPix(5));
+```
# Example
See the DemoApp's [bar chart example source](../demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java).
\ No newline at end of file
diff --git a/docs/bubblechart.md b/docs/bubblechart.md
index 2c4e6792..bef52e45 100644
--- a/docs/bubblechart.md
+++ b/docs/bubblechart.md
@@ -1,31 +1,32 @@
# Bubble Charts
-A bubble chart is essentially a two dimensional representation of three dimensional data
-on an xy plot, where the xy values are drawn as a circle whose radius represents the z value.
+A `BubbleChart` is a two dimensional representation of three dimensional data
+on an `XYPlot`, where the xy values are drawn as a circle with a radius representing the zVal.

# Basic Usage
-The first step in creating a bubble chart is to define the data to be plotted. We'll start
-by creating an instance of BubbleSeries:
+The first step in creating a `BubbleChart` is to define the data to be plotted. We'll start
+by creating an instance of `BubbleSeries`:
-Using the implicit i-val for x:
+Using the implicit iVal for x:
```java
BubbleSeries series1 = new BubbleSeries(
- Arrays.asList(new Number[]{3, 5, 2, 3, 6}),
- Arrays.asList(new Number[]{1, 5, 2, 2, 3}), "s1");
+ Arrays.asList(new Number[]{3, 5, 2, 3, 6}), // xCoordinate
+ Arrays.asList(new Number[]{1, 5, 2, 2, 3}), "s1"); // yCoordinate
```
Or it's equivalent four argument counterpart:
```java
BubbleSeries bubbleSeries = new BubbleSeries(
- Arrays.asList(new Number[]{0, 1, 2, 3, 4}),
- Arrays.asList(new Number[]{3, 5, 2, 3, 6}),
- Arrays.asList(new Number[]{1, 5, 2, 2, 3}), "s1");
+ Arrays.asList(new Number[]{0, 1, 2, 3, 4}), // xCoordinate
+ Arrays.asList(new Number[]{3, 5, 2, 3, 6}), // yCoordinate
+ Arrays.asList(new Number[]{1, 5, 2, 2, 3}), // zVal (corresponds to radius)
+ "s1");
```
-Next, create a Formatter defining the fill and outline colors of the bubbles:
+Next, create a `Formatter` defining the fill and outline colors of the bubbles:
```java
@@ -33,15 +34,15 @@ Next, create a Formatter defining the fill and outline colors of the bubbles:
BubbleFormatter formatter = new BubbleFormatter(Color.GREEN, Color.WHITE)
```
-Finally, add the bubble series to our plot as you would any other xyseries instance:
+Finally, add the `BubbleSeries` to our plot as you would any other `XYSeries` instance:
```java
plot.addSeries(bubbleSeries, formatter);
```
# BubbleScaleMode
-By default, BubbleRenderer scales each rendered bubble radius using the square root of it's corresponding
-z-val, preventing apparent size differences in each bubble radius from being visually misleading. See: https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly
+By default, `BubbleRenderer` scales each rendered bubble radius using the square root of it's corresponding
+zVal, preventing apparent size differences in each bubble radius from being visually misleading. See: https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly
If you'd prefer to use a linear scale:
diff --git a/docs/candlestick.md b/docs/candlestick.md
index 80f4f987..778117e9 100644
--- a/docs/candlestick.md
+++ b/docs/candlestick.md
@@ -1,6 +1,77 @@
# Candlestick Charts
-Candlestick charts are implemented in Androidplot as a custom XYSeries type. At the moment, documentation
-is pretty light. For now, [see this doc on usage](http://androidplot.com/docs/candlestick-charts/)
-or check out the DemoApp's [candlestick example source](../demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java).
+Candlestick charts are a special kind of `XYPlot` usually used for rendering financial information.
+Each "candlestick" is composed of 4 values: open, close, high and low.
+
+
+
+Androidplot's implementation of candlestick charts uses four separate `XYSeries` instances to represent
+the candlestick values; each series corresponds to one of the four values that composes each "candlestick".
+
+For example:
+
+```java
+Number[] highVals = new Number[] {12, 10, 15, 8, 7};
+Number[] lowVals = new Number[] {3, 1, 5, 0, 2};
+Number[] openVals = new Number[] {5, 2, 7, 5, 3};
+Number[] closeVals = new Number[] {7, 9, 6, 0, 4};
+```
+
+Using the values above, the first candlestick model would be `{high=12, low=3, open=5, close=7}`,
+the second would be `{high=10, low=1, open=2, close=7}` and so on.
+
+There are a few constraints that must be satisfied before we can create XYSeries from the data above and add them to Androidplot:
+
+* Each series must be of the same size.
+* Each series must have the same x(i); getX(i) must return the same value for each series.
+* There must be exactly four series, added in the precise order: high, low, open, close.
+* The series data must make sense; each candlestick's high must be equal to or greater than all other
+values in the candlestick, and each candlestick's low must be less than or equal to all other values.
+* Each candlestick series belonging to the same candlestick chart must use the same `CandlestickFormatter`
+instance when being added to the XYPlot.
+
+While it's possible to satisfy these constraints by creating each of the four `XYSeries` required by a
+candlestick chart, adding them in order using the same formatter, many projects will find it simpler
+to interact with the data as though it were a single series of candlestick values. To makr this easier,
+two convenience classes: `CandlestickSeries` and `CandlestickMaker`. Here's how they are used together to add a candlestick chart to an `XYPlot`:
+
+```java
+CandlestickSeries candlestickSeries = new CandlestickSeries(
+ new CandlestickSeries.Item(1, 10, 2, 9),
+ new CandlestickSeries.Item(4, 18, 6, 5),
+ new CandlestickSeries.Item(3, 11, 5, 10),
+ new CandlestickSeries.Item(2, 17, 2, 15),
+ new CandlestickSeries.Item(6, 11, 11, 7),
+ new CandlestickSeries.Item(8, 16, 10, 15));
+
+CandlestickFormatter formatter = new CandlestickFormatter();
+
+// add the candlestick series data to the plot:
+CandlestickMaker.make(plot, formatter, candlestickSeries);
+```
+
+It's necessary to use `CandlestickMaker` to add the `CandlestickSeries` (via "make") to the
+`XYPlot` as `CandlestickSeries` does not and cannot implement the `XYSeries` interface.
+
+
+[A full source example is available here](../demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 00000000..cad3ef5f
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,56 @@
+# Contributing Soure Code
+Contributions are always welcome! If you've got an idea for a specific feature and would like feedback
+on implementation or if you want to contribute but don't know where to start, feel free to [
+start a topic in the Forums](https://groups.google.com/forum/#!forum/androidplot). If you already have
+a feature or bugfix to submit, take a look at the code style notes below and then submit a Pull Request.
+
+## Code Style
+For familiarity and simplicity, Androidplot uses [Android's code style guide](https://source.android.com/source/code-style.html).
+In particular:
+
+* Indentations are 4 spaces each - no tab characters.
+* Braces do not go on their own line; they go on the same line as the code before them.
+* Line length should be a max of 100 characters.
+* Don't catch generic Exception and don't ignore caught exceptions.
+* Classes and methods should be documented with standard Javadoc comments.
+
+## Pull Requests
+If you have code changes you'd like to share with the project, issue a [Pull Request](https://help.github.com/articles/about-pull-requests/) against the master
+branch of Androidplot. Please make sure that:
+
+* Difficult to follow logical statements, etc. are reasonably documented.
+* Commented out lines, unused methods / dead code blocks have been removed.
+* Unit Tests have have been refactored appropriately and all pass.
+* A clear description of the change is included in the Pull Request.
+
+Adding Unit Tests for new methods / functionality is highly encouraged.
+
+# Git Newcomers
+If you're new to Git, this section will show you the basics of checking out a local copy of the project and building it.
+
+## Clone
+Androidplot is hosted on Github. If you've got a git client installed, you can either
+[fork](https://help.github.com/articles/fork-a-repo/) the project into your own
+github account, or you can clone the project to your workstation using this command:
+
+```
+git clone https://github.com/halfhp/androidplot.git
+```
+
+You can also [download an ZIP](https://github.com/halfhp/androidplot/archive/master.zip) of the master branch of Androidplot.
+
+## Build
+Androidplot is a Gradle project, which makes building incredibly easy. To perform a full build (runs tests,
+generates javadocs and builds the library and demo app) from the command line:
+
+```
+./gradlew test assemble
+```
+You can also of course use the IDE of your choice instead. [Android Studio](https://developer.android.com/studio/index.html) is highly recommended.
+
+## Useful Links
+If you're new to Git these links will get you started:
+
+* [Setting up Git](https://help.github.com/articles/set-up-git/) Start here if you don't have a Git client installed.
+* [Pro Git (free eBook)](https://git-scm.com/book/en/v2) - _The_ book to read when it comes to learning Git
+* [Git Reference](https://git-scm.com/docs) - Good set of links for general reference
\ No newline at end of file
diff --git a/docs/custom_renderer.md b/docs/custom_renderer.md
new file mode 100644
index 00000000..73cac354
--- /dev/null
+++ b/docs/custom_renderer.md
@@ -0,0 +1,101 @@
+# Custom Renderers
+Androidplot renderers can be extended with extra functionality in the form of custom renderers.
+For the example below we'll look at creating a custom implementation of BarRenderer that will
+draw bars with rounded edges. While this example demonstrates an `XYPlot`, the steps apply to
+`PieChart` etc. as well.
+
+## Create the Formatter
+The first step to creating a custom renderer is defining it's `Formatter`. Aside from providing
+the visual configuration used by the `Renderer` to draw series data, it's also used by Androidplot
+to map a series to a specific renderer type and if necessary, obtain a new instance of that renderer.
+Here's the most basic implementation:
+
+```java
+class RoundedBarFormatter extends BarFormatter {
+
+ {
+ // for now we'll hardcode some formatting values.
+ // a real implementation would probably provide a constructor instead
+ getBorderPaint().setColor(Color.WHITE);
+ getFillPaint().setColor(Color.RED);
+ }
+
+ @Override
+ public Class getRendererClass() {
+ return RoundedBarRenderer.class;
+ }
+
+ @Override
+ public RoundedBarRenderer doGetRendererInstance(XYPlot xyPlot) {
+ return new RoundedBarRenderer(xyPlot);
+ }
+}
+```
+
+## Create the Renderer
+All we want to do here is tweak the way `BarRenderer` behaves a little so we'll use it as our base class:
+
+```java
+class RoundedBarRenderer extends BarRenderer {
+
+ public RoundedBarRenderer(XYPlot plot) {
+ super(plot);
+ }
+
+ @Override
+ protected void drawBar(Canvas canvas, Bar bar, RectF rect) {
+ // TODO this is where we'll add our custom behavior
+ }
+}
+```
+
+In the case of our RoundedBarFormatter, the behavior we need to change all exits within the
+`drawBar(Canvas, Bar, RectF)` method. The existing implementation uses `Canvas.drawRect(...)` to
+draw bars. We could use a stroke with rounded edges, but this would cause the edges on both the top
+and the bottom to be rounded, which won't look right. We'll draw using a path instead:
+
+```java
+@Override
+protected void drawBar(Canvas canvas, Bar bar, RectF rect) {
+
+ // skip nulls:
+ if(bar.getY() == null) {
+ return;
+ }
+
+ RoundedBarFormatter formatter = getFormatter(bar.i, bar.series);
+ if(formatter == null) {
+ formatter = bar.formatter;
+ }
+
+ // don't need to draw if the bar lacks height or width:
+ if (rect.height() > 0 && rect.width() > 0) {
+ final Path path = new Path();
+ final float arcHeightPx = 20;
+ final float adjustedTop = rect.top + arcHeightPx;
+ final RectF cap = new RectF(rect.left, rect.top, rect.right,rect.top + 2 * arcHeightPx);
+
+ // start at bottom-left and move clock-wise around the shape:
+ path.moveTo(rect.left, rect.bottom);
+ path.lineTo(rect.left, adjustedTop);
+ path.arcTo(cap, 180, 180, false);
+ path.lineTo(rect.right, rect.bottom);
+ path.close();
+
+ canvas.drawPath(path, formatter.getFillPaint());
+ canvas.drawPath(path, formatter.getBorderPaint());
+ }
+}
+```
+
+## Use It
+Using the new custom renderer is as easy as adding a series to a plot using `RoundedBarFormatter`:
+
+```java
+plot.addSeries(series1, new RoundedBarFormatter(Color.RED));
+plot.addSeries(series2, new RoundedBarFormatter(Color.BLUE));
+```
+
+Here's what this would look like used in the demo app's `SimpleXYPlotActivity`:
+
+
diff --git a/docs/grouprenderer.md b/docs/grouprenderer.md
index 755b05eb..8a8ce9d1 100644
--- a/docs/grouprenderer.md
+++ b/docs/grouprenderer.md
@@ -1,11 +1,11 @@
# GroupRenderer
-GroupRenderer is a special implementation of XYSeriesRenderer that combines multiple instances
-of XYSeries into a single virtual series of s higher dimension. Examples of GroupRenderer are:
+`GroupRenderer` is a special implementation of `XYSeriesRenderer` that combines multiple instances
+of `XYSeries` into a single virtual series of s higher dimension. Examples of `GroupRenderer` are:
* BarRenderer
* CandlestickRenderer
-Let's take a quick look at CandlestickRenderer. A candlestick chart is basically a two two dimensional
+Let's take a quick look at `CandlestickRenderer`. A candlestick chart is basically a two two dimensional
representation of a complex value; for every value of x a single candlestick is drawn, but there are
four values (dimensions) that make up each candlestick:
@@ -14,5 +14,5 @@ four values (dimensions) that make up each candlestick:
* high
* low
-When a series is added to the plot with a formatter associated with a renderer that extends GroupRenderer,
+When a series is added to the plot with a formatter associated with a renderer that extends `GroupRenderer`,
all series added with formatters of the same type are automatically "grouped" together during rendering.
\ No newline at end of file
diff --git a/docs/images/bargroup_overlaid.png b/docs/images/bargroup_overlaid.png
new file mode 100644
index 00000000..39ed7a41
Binary files /dev/null and b/docs/images/bargroup_overlaid.png differ
diff --git a/docs/images/bargroup_side_by_side.png b/docs/images/bargroup_side_by_side.png
new file mode 100644
index 00000000..938c346a
Binary files /dev/null and b/docs/images/bargroup_side_by_side.png differ
diff --git a/docs/images/bargroup_spacing.png b/docs/images/bargroup_spacing.png
new file mode 100644
index 00000000..463cca62
Binary files /dev/null and b/docs/images/bargroup_spacing.png differ
diff --git a/docs/images/bargroup_stacked.png b/docs/images/bargroup_stacked.png
new file mode 100644
index 00000000..e2c1256e
Binary files /dev/null and b/docs/images/bargroup_stacked.png differ
diff --git a/docs/images/pie_donut_size.png b/docs/images/pie_donut_size.png
new file mode 100644
index 00000000..2fb20380
Binary files /dev/null and b/docs/images/pie_donut_size.png differ
diff --git a/docs/images/pie_extent.png b/docs/images/pie_extent.png
new file mode 100644
index 00000000..919e19ff
Binary files /dev/null and b/docs/images/pie_extent.png differ
diff --git a/docs/images/pie_inner_inset.png b/docs/images/pie_inner_inset.png
new file mode 100644
index 00000000..0fc8172c
Binary files /dev/null and b/docs/images/pie_inner_inset.png differ
diff --git a/docs/images/pie_offset.png b/docs/images/pie_offset.png
new file mode 100644
index 00000000..cf204470
Binary files /dev/null and b/docs/images/pie_offset.png differ
diff --git a/docs/images/pie_orientation.png b/docs/images/pie_orientation.png
new file mode 100644
index 00000000..1ee46acc
Binary files /dev/null and b/docs/images/pie_orientation.png differ
diff --git a/docs/images/pie_outer_inset.png b/docs/images/pie_outer_inset.png
new file mode 100644
index 00000000..d0ce79f3
Binary files /dev/null and b/docs/images/pie_outer_inset.png differ
diff --git a/docs/images/pie_radial_inset.png b/docs/images/pie_radial_inset.png
new file mode 100644
index 00000000..32ff6aa2
Binary files /dev/null and b/docs/images/pie_radial_inset.png differ
diff --git a/docs/images/screens/rounded_bar_renderer.png b/docs/images/screens/rounded_bar_renderer.png
new file mode 100644
index 00000000..4afd37d4
Binary files /dev/null and b/docs/images/screens/rounded_bar_renderer.png differ
diff --git a/docs/images/sizing/abs100x-abs100y.png b/docs/images/sizing/abs100x-abs100y.png
new file mode 100644
index 00000000..03176840
Binary files /dev/null and b/docs/images/sizing/abs100x-abs100y.png differ
diff --git a/docs/images/sizing/abs100x-abs150y.png b/docs/images/sizing/abs100x-abs150y.png
new file mode 100644
index 00000000..c1c8159d
Binary files /dev/null and b/docs/images/sizing/abs100x-abs150y.png differ
diff --git a/docs/images/sizing/abs100x-rel1y.png b/docs/images/sizing/abs100x-rel1y.png
new file mode 100644
index 00000000..b62cf708
Binary files /dev/null and b/docs/images/sizing/abs100x-rel1y.png differ
diff --git a/docs/images/sizing/fil50x-fil50y.png b/docs/images/sizing/fil50x-fil50y.png
new file mode 100644
index 00000000..6dd24012
Binary files /dev/null and b/docs/images/sizing/fil50x-fil50y.png differ
diff --git a/docs/images/sizing/rel075x-abs100y.png b/docs/images/sizing/rel075x-abs100y.png
new file mode 100644
index 00000000..d0328550
Binary files /dev/null and b/docs/images/sizing/rel075x-abs100y.png differ
diff --git a/docs/index.md b/docs/index.md
index ed9ab280..90a8a021 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -15,6 +15,7 @@ specific plot types explaining styling and other advanced topics.
* [Quickstart](quickstart.md) :star:
* [Quickstart (YouTube Video)](https://www.youtube.com/watch?v=wEFkzQY_wWI) :movie_camera:
* [Plot Composition](plot_composition.md)
+* [The Legend](legend.md)
* [XY Plots](xyplot.md)
* [Bar Charts](barchart.md)
* [Candlestick Charts](candlestick.md)
@@ -22,6 +23,7 @@ specific plot types explaining styling and other advanced topics.
* [Bubble Charts](bubblechart.md)
* [Dynamic Plots](dynamicdata.md)
* [Advanced XY Plot Topics](advanced_xy_plot.md)
+* [Custom Renderers](custom_renderer.md)
# Examples
Source code examples of the various plot types.
@@ -37,15 +39,16 @@ Source code examples of the various plot types.
* [An ECG Example](../demoapp/src/main/java/com/androidplot/demos/ECGExample.java)
* [f(x) Plot](../demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java)
+# XML Attributes
+A complete list of XML attributes is [available here](attrs.md).
# Javadoc
-The latest Javadocs are [available here](https://circleci.com/api/v1/project/halfhp/androidplot/latest/artifacts/0/$CIRCLE_ARTIFACTS/javadoc/index.html).
+Javadocs for versions through 1.5.7 are [available here](https://javadoc.io/doc/com.androidplot/androidplot-core). Due to a build issue
+producing Javadocs for the latest releases has been paused. There have been very
+few changes to the API since 1.5.7 so the Javadocs should still be useful in the mean time.
# Release Notes
Full release notes are [available here](release_notes.md)
-# Old Versions
-Documentation for Androidplot 0.9.8 and earlier [can be found here](http://androidplot.com/docs/).
-
# Contributing
_If you see something that isn't right or want to contribute, please [make a pull-request](https://help.github.com/articles/creating-a-pull-request/) - these docs
-live the main repo in the top-level `/docs` directory._
\ No newline at end of file
+live the main repo in the top-level `/docs` directory. For more info, see the [Contributing Source Code](docs/contributing.md) doc.
\ No newline at end of file
diff --git a/docs/legend.md b/docs/legend.md
new file mode 100644
index 00000000..c3ea90e4
--- /dev/null
+++ b/docs/legend.md
@@ -0,0 +1,63 @@
+# The Legend
+For `Plot` types that support it, the legend displays a list of elements in the plot along with
+a color coded icon. The color coded icon is automatically generated using the colors and line styles
+used to render the associated item. In the case of a `Series`, this is the `Formatter` you associated
+with the `Series` when you added it to your `Plot`.
+
+# Showing / Hiding the Legend
+Depending on the `Plot` type(s) you are using, the legend may or may not be visible by default. To
+can enable / disable the legend:
+
+```java
+plot.getLegend().setVisible(true|false);
+```
+
+# Hiding Series Items
+You can tell Androidplot not to generate a legend item for a `Series` by configuring it's associated
+`Formatter`:
+
+```java
+formatter.setLegendIconEnabled(false);
+```
+
+## The TableModel
+The `TableModel` controls how and where each item in the legend is drawn. Androidplot provides two
+default implementations; `DynamicTableModel` and `FixedTableModel` (detailed below). All `TableModel` implementations
+organize elements into a grid. This grid is populated with items based on the order which it's corresponding
+series was added to the plot. This ordering can be further controlled by setting the `TableModel`'s
+`TableOrder` param to either [ROW_MAJOR](https://en.wikipedia.org/wiki/Row-major_order) (items are added left-to-right, top-down)
+or `COLUMN_MAJOR` (items are added top-down, left-to-right).
+
+### DynamicTableModel
+The `DynamicTableModel` takes a desired of numbered rows and columns and evenly subdivides the `LegendWidget`'s
+visible space into cells. For example, A 2x2 legend using `ROW_MAJOR` ordering:
+
+```java
+plot.getLegend().setTableModel(new DynamicTableModel(2, 2, TableOrder.ROW_MAJOR));
+```
+
+### FixedTableModel
+The `FixedTableModel` takes a desired size of each cell in pixels and adds cells using the specified `TableOrder`.
+It automatically wraps to the next row or column (based on `TableOrder`) when the cell being added
+exceeds the legend's available space on a given axis. For example, A `FixedTableModel` using 300w*100h cells and
+a TableOrder of `COLUMN_MAJOR`:
+
+```java
+plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300),
+ PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR));
+```
+
+# Sorting Legend Entries
+You can control the order of Legend entries by setting a custom `Comparator` on the legend:
+
+```java
+Comparator<...> myComparator = ...
+plot.getLegend().setLegendItemComparator(myComparator);
+```
+
+Using a custom `Comparator` in conjunction with `ROW_MAJOR` and `COLUMN_MAJOR` properties on the `TableModel`
+(show above) gives you full control over the display ordering of your legend entries.
+
+# Positioning and Resizing
+The legend is just an implementation of a Widget and is positioned and resized in the same ways
+that all Widget instances are positioned. See the [Plot Composition](plot_composition.md) doc for details.
\ No newline at end of file
diff --git a/docs/piechart.md b/docs/piechart.md
index fa143897..8bbfbfbb 100644
--- a/docs/piechart.md
+++ b/docs/piechart.md
@@ -1,6 +1,6 @@
# Pie Charts
-Like all other primary chart and graph classes in Androidplot, PieChart is simply a composition
-of widgets along with convenience methods. The real work is actually done by PieWidget.
+Like all other primary chart and graph classes in Androidplot, `PieChart` is simply a composition
+of widgets along with convenience methods. The real work is actually done by `PieWidget`.

@@ -11,41 +11,93 @@ Pie charts are composed of one or more values called Segments which combines a v
Segment segment = new Segment("my segment", 10);
```
-Displaying a Segment to a pie chart also requires a SegmentFormatter. SegmentFormatter is what defines
-the visual characteristics of the associated segment; color, border thickness,
-segment offset, label text style (if any) etc. The below code instantiates a new SegmentFormatter
+Displaying a `Segment` to a pie chart also requires a `SegmentFormatter`. `SegmentFormatter` is what defines
+the visual characteristics of the associated `Segment`; color, border thickness,
+segment offset, label text style (if any) etc. The below code instantiates a new `SegmentFormatter`
with a segment color of red:
```java
SegmentFormatter formatter = new SegmentFormatter(Color.RED);
```
-Finally, the Segment must be added to the pie chart along with it's SegmentFormatter:
+Finally, the `Segment` must be added to the pie chart along with it's `SegmentFormatter`:
```java
pie.addSegment(segment, formatter);
```
+# Donut Size
+By default, `PieChart` draws it's self as a ring. The amount of empty space in the middle of
+the pie is called the `donutSize`. Setting a `donutSize` of 0 causes the `PieChart` to draw it's
+self as a full circle.
+
+
+
+Setting `donutSize` programmatically:
+
+```java
+// draw pie with an empty inner space radius equal to 50% of the pie's radius.
+pieRenderer.setDonutSize(0.5, PieRenderer.DonutMode.PERCENT);
+
+// donut size may also be expressed in pixels:
+pieRenderer.setDonutSize(PixelUtils.dpToPix(30), PieRenderer.DonutMode.PIXELS);
+```
+
+# Segment Orientation
+Each `Segment` is drawn in the order it was added to the `PieChart` clockwise from `startDegs`
+which by default is 0.
+
+
+
+Using the image above, to move Segment0 to the Northeast quadrant
+of the pie (currently occupied by Segment3) `startDeg` should be set to 90:
+
+```java
+pie.getRenderer(PieRenderer.class).setStartDegs(90);
+```
+# Extent
+The `extentDegs` value determines the number of degrees running clockwise from `startDegs` that
+represent 100% of the pie chart. The formal description is a little obtuse but is easy to understand
+through example. You could for instance use `extentDegs` in conjunction with `startDegs` to represent
+the full pie as a gauge:
+
+```java
+// start the first segment at 5 degrees;
+pie.getRenderer(PieRenderer.class).setStartDegs(165);
+pie.getRenderer(PieRenderer.class).setExtentDegs(150);
+```
+Which produces a pie chart profile like this:
+
+
+
# SegmentFormatter
-As mentioned above, SegmentFormatter defines how a Segment is visually represented in a PieChart.
+As mentioned above, `SegmentFormatter` defines how a `Segment` is visually represented in a `PieChart`.
In addition to basic color / border line styling params, there are several parameters available
to visually distinguish a segment:
## Offset
-Value in pixels used to "shift" the position of the segment relative to the center of the PieChart.
-The visual effect is equivalent to cutting up a pie and dragging one piece outward, away from the rest of the pie.
+Value in pixels used to "shift" the position of the segment outward relative to the center of the `PieChart`.
+The visual effect is equivalent to cutting up a pie and dragging one piece out and away from the rest of the pie:
+
+
## RadialInset
Value in degrees used to shrink the radial edges of the segment inward. The visual effect is equivalent
-to shaving the edges off of a slice of pie, resulting in a narrower slice of pie.
+to shaving the edges off of a slice of pie, resulting in a narrower slice of pie:
+
+
## InnerInset
Value in pixels used to shrink the inner section of the pie. The visual effect is equivalent to
-cutting the tip off of a slice of pie.
+cutting the tip off of a slice of pie:
+
+
## OuterInset
Value in pixels used to shrink the outer edge of the pie. The visual effect is equivalent to evenly
-shaving the crust off of a slice of pie.
+shaving the crust off of a slice of pie:
+
+
See the DemoApp's
[pie chart example source](../demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java) for a comprehensive usage example.
diff --git a/docs/plot_composition.md b/docs/plot_composition.md
index 20b80952..2c51e349 100644
--- a/docs/plot_composition.md
+++ b/docs/plot_composition.md
@@ -1,26 +1,26 @@
# Plot Composition
-All plots in Androidplot inherit from the abstract base class Plot which provides common behaviors
-for all Plot implementations.
+All plots in Androidplot inherit from the abstract base class `Plot` which provides common behaviors
+for all `Plot` implementations.
# Widgets
-Plots are composed of one or more Widgets. A Widget is an abstraction of a visual
+Plots are composed of one or more Widgets. A `Widget` is an abstraction of a visual
component that may be positioned and scaled within the visible area of a Plot. For example,
-an XY Plot is typically composed of these 5 Widgets:
+an `XYPlot` is typically composed of these 5 `Widgets`:
* Title
* Graph
* Domain Label
* Range Label
-* Legend
+* [Legend](legend.md)
-All Plot implementations will contain at least one default Widget providing the core
-behavior encapsulated by that Plot. In addition to moving and scaling these Widgets, developers may
-also extend them and replace the Plot's default instance with the derived implenentation in order to
+All implementations of `Plot` will contain at least one default `Widget` providing the core
+behavior encapsulated by that `Plot`. In addition to moving and scaling a `Widget`, developers may
+also extend them and replace the `Plot` instance's default instance with the derived implementation in order to
get custom behavior.
# The LayoutManager
-The LayoutManager provides the logic for visually positioning and scaling Widgets; all Plot implementations
-contain an instance of LayoutManager that can be retrieved via `Plot.getLayoutManager()`.
+The `LayoutManager` provides the logic for visually positioning and scaling Widgets; all `Plot` implementations
+contain an instance of `LayoutManager` that can be retrieved via `Plot.getLayoutManager()`.
## Z-Indexing
Z-indexing is a 2D drawing concept which associates each drawable entity with a value that determines
@@ -28,12 +28,12 @@ which elements get drawn onto the screen first, producing the visual effect that
on top of others.
While Androidplot uses the term "z-index" it's implemented internally as a linked list to prevent the possibility
-of duplicate index values and therefore ensuring that the drawing order of Widgets is always explicit.
+of duplicate index values and therefore ensuring that `Widget` drawing order is always explicit.
The [Layerable](../androidplot-core/src/main/java/com/androidplot/util/Layerable.java) interface
-defines methods used for manipulating the z-index of a Widget.
+defines methods used for manipulating the z-index of a `Widget`.
## Adding & Removing Widgets
-New Widgets can be added either to the front or back of the z-index using these methods:
+New `Widget` instances can be added either to the front or back of the z-index using these methods:
* `LayoutManager.addToTop(Widget)`
* `LayoutManager.addToBottom(Widget)`
@@ -76,6 +76,9 @@ Using example #1 above, an Anchor value of `Anchor.LEFT_TOP` means that the top
would be positioned at the screen coordinate [200, 50].
#### Examples
+The examples below illustrate positioning an `XYGraphWidget` of an `XYPlot`.
+
+xml:
```xml
ap:graphAnchor="right_bottom"
ap:graphHorizontalPositioning="absolute_from_right"
@@ -83,8 +86,16 @@ ap:graphHorizontalPosition="0dp"
ap:graphVerticalPositioning="relative_from_bottom"
ap:graphVerticalPosition="0dp"
```
+java:
+```java
+plot.getGraph().position(
+ 0, HorizontalPositioning.ABSOLUTE_FROM_RIGHT,
+ 0, VerticalPositioning.RELATIVE_TO_BOTTOM);
+```

***
+
+xml:
```xml
ap:graphAnchor="right_bottom"
ap:graphHorizontalPositioning="absolute_from_right"
@@ -92,8 +103,16 @@ ap:graphHorizontalPosition="10dp"
ap:graphVerticalPositioning="relative_from_bottom"
ap:graphVerticalPosition="10dp"
```
+java:
+```java
+plot.getGraph().position(
+ PixelUtils.dpToPix(10), HorizontalPositioning.ABSOLUTE_FROM_RIGHT,
+ PixelUtils.dpToPix(10), VerticalPositioning.RELATIVE_TO_BOTTOM);
+```

***
+
+xml:
```xml
ap:graphAnchor="right_bottom"
ap:graphHorizontalPositioning="absolute_from_left"
@@ -101,8 +120,16 @@ ap:graphHorizontalPosition="0dp"
ap:graphVerticalPositioning="relative_from_top"
ap:graphVerticalPosition="0dp"
```
-
+java:
+```java
+plot.getGraph().position(
+ 0, HorizontalPositioning.ABSOLUTE_FROM_LEFT,
+ 0, VerticalPositioning.RELATIVE_TO_TOP);
+```
+
***
+
+xml:
```xml
ap:graphAnchor="center"
ap:graphHorizontalPositioning="absolute_from_center"
@@ -110,23 +137,117 @@ ap:graphHorizontalPosition="0dp"
ap:graphVerticalPositioning="absolute_from_center"
ap:graphVerticalPosition="0dp"
```
+java:
+```java
+plot.getGraph().position(
+ 0, HorizontalPositioning.ABSOLUTE_FROM_CENTER,
+ 0, VerticalPositioning.ABSOLUTE_FROM_CENTER);
+```

-## Scaling Widgets
-TODO
+## Sizing Widgets
+The size and shape of a `Widget` is controlled by it's `setSize(Size)` method.
+
+### Size
+The `Size` parameter of `Widget.setSize(Size)` defines the height and width of the associated Widget.
+It is composed of two `SizeMetric` instances; one for height and one for width.
+`Size` provides two constructors: `Size(SizeMetric, SizeMetric)` or
+`Size(int, SizeMode, int, SizeMode)`. Constructor params represent height and width respectively.
### SizeMetric
-TODO
+A `SizeMetric` is composed of a `SizeMode` and a float value. There are three kinds of `SizeMode`:
+
+* ABSOLUTE - float value defines the size metric as an absolute value in pixels.
+* RELATIVE - float value defines the size of the metric relative to the size of the containing Plot along
+the associated axis, in the range of 0.0 to 1.0.
+* FILL - float value defines an absolute value in pixels to subtract from the size of the containing
+Plot along the associated axis and the SizeMetric "fills" the difference.
#### Examples
-TODO
+The examples below illustrate positioning an `XYGraphWidget` of an `XYPlot` and
+assume centered positioning is applied as described in the [Positioning Widgets](#positioning-widgets) section above.
+
+xml:
+```xml
+ap:graphHeightMode="absolute"
+ap:graphHeight="100dp"
+ap:graphWidthMode="absolute"
+ap:graphWidth="100dp"
+```
+java:
+```java
+plot.getGraph().setSize(new Size(
+ PixelUtils.dpToPix(100), SizeMode.ABSOLUTE,
+ PixelUtils.dpToPix(100), SizeMode.ABSOLUTE));
+```
+
+***
+xml:
+```xml
+ap:graphHeightMode="absolute"
+ap:graphHeight="150dp"
+ap:graphWidthMode="absolute"
+ap:graphWidth="100dp"
+```
+java:
+```java
+plot.getGraph().setSize(new Size(
+ PixelUtils.dpToPix(150), SizeMode.ABSOLUTE,
+ PixelUtils.dpToPix(100), SizeMode.ABSOLUTE));
+```
+
+***
+xml:
+```xml
+ap:graphHeightMode="relative"
+ap:graphHeight="1.0"
+ap:graphWidthMode="absolute"
+ap:graphWidth="100dp"
+```
+java:
+```java
+plot.getGraph().setSize(new Size(
+ 1.0f, SizeMode.RELATIVE,
+ PixelUtils.dpToPix(100), SizeMode.ABSOLUTE));
+```
+
+***
+xml:
+```xml
+ap:graphHeightMode="absolute"
+ap:graphHeight="100dp"
+ap:graphWidthMode="relative"
+ap:graphWidth="0.75"
+```
+java:
+```java
+plot.getGraph().setSize(new Size(
+ PixelUtils.dpToPix(100), SizeMode.ABSOLUTE,
+ 0.75f, SizeMode.RELATIVE));
+```
+
+***
+xml:
+```xml
+ap:graphHeightMode="fill"
+ap:graphHeight="50dp"
+ap:graphWidthMode="fill"
+ap:graphWidth="50dp"
+```
+java:
+```java
+plot.getGraph().setSize(new Size(
+ PixelUtils.dpToPix(50), SizeMode.FILL,
+ PixelUtils.dpToPix(50), SizeMode.FILL));
+```
+
## Margins and Padding
Every widget has a margin, padding and an optional border that can be drawn around it. These params behave
very similarly to those defined in the [CSS Box Model](http://www.w3schools.com/css/css_boxmodel.asp).
-## Markup Mode.
+## Markup Mode
If you're having trouble visualizing the effects of tweaking margins and padding, you can enable
markup mode which will highlight these spaces on each widget, as well as draw a green line around it's
absolute border.
@@ -160,7 +281,7 @@ text style, etc. to apply while drawing.
## Renderers
The Renderer is what renders Series data onto a Plot. Users can provide their own custom rendering behavior
by writing their own Renderer implementation along with a custom Formatter telling Androidplot about the
-Renderer via the `Formatter.getRendererClass()` method.
+Renderer via the `Formatter.getRendererClass()` method. See [Custom Renderer](custom_rnderer.md) documentation.
# XML Styling
Androidplot supports an increasing number of XML attributes. The two best resources for learning about
diff --git a/docs/quickstart.md b/docs/quickstart.md
index a6de866d..3bd87c3d 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -14,10 +14,13 @@ To use the library in your gradle project add the following to your build.gradle
```groovy
dependencies {
- compile "com.androidplot:androidplot-core:1.3.0"
+ implementation "com.androidplot:androidplot-core:1.5.11"
}
```
+*NOTE: As of version 1.5.8 Androidplot has migrated over from the Android Support Libraries to androidx.
+If you have a very old project and experience issues, may need to stay on version 1.5.7*
+
If you’re using Proguard obfuscation (Projects created by Android Studio do by default) you’ll also
want add this to your proguard-rules.pro file:
@@ -44,8 +47,8 @@ and add an XYPlot view:
ap:lineLabelRotationBottom="-45"/>
```
-This example uses a default style to decorate the plot. The full list of styleable attributes is
-[available here](../androidplot-core/src/main/res/attrs.xml). While new attributes are added regularly,
+This example uses a default style to decorate the plot. The full list of XML styleable attributes is
+[available here](attrs.md). While new attributes are added regularly,
not all configurable properties are yet available.
If something you need is missing, use [Fig Syntax](https://github.com/halfhp/fig)
@@ -55,6 +58,32 @@ directly within your Plot's XML, prefixing each property with "androidPlot". Ex
androidPlot.title="My Plot"
```
+Add these files to your **/res/xml** directory:
+
+#### /res/xml/line_point_formatter_with_labels.xml
+```xml
+
+
+```
+
+#### /res/xml/line_point_formatter_with_labels_2.xml
+```xml
+
+
+```
+
# Create an Activity
Now let's create an Activity to display the XYPlot we just defined in `simple_xy_plot_example.xml`.
The basic steps are:
@@ -151,7 +180,6 @@ public class SimpleXYPlotActivity extends Activity {
}
```
-
One potentially confusing section of the code above are the initializations of LineAndPointFormatter
You probably noticed that they take a mysterious reference to an xml resource file. This is actually
using [Fig](https://github.com/halfhp/fig) to configure the instance properties from XML.
@@ -173,31 +201,5 @@ In general XML configuration should be used over programmatic configuration when
more flexibility in terms of defining properties by screen density etc.. For more details on how to
programmatically configure Formatters etc. consult the latest Javadocs.
-Continuing with the original example above, add these files to your **/res/xml** directory:
-
-#### /res/xml/line_point_formatter_with_labels.xml
-```xml
-
-
-```
-
-#### /res/xml/line_point_formatter_with_labels_2.xml
-```xml
-
-
-```
-
# Whats Next?
-Learn about [Plot Composition](plot_composition.md)
\ No newline at end of file
+Learn about [Plot Composition](plot_composition.md) or continue with [XYPlots](xyplot.md).
\ No newline at end of file
diff --git a/docs/release_notes.md b/docs/release_notes.md
index fcdc37e5..50d52c11 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -1,3 +1,116 @@
+# Androidplot Versioning
+For details on what to expect in general when updating to a new version of Androiplot, check out the
+[versioning doc](versioning.md).
+
+# 1.5.11
+* Update project to latest gradle / build tools
+* Fix issue with jetifier flagging an outdated dependency
+
+# 1.5.10
+* Update project to use latest gradle / build tools
+* (#114) fix `setLinesPerRangeLabel` & `setLinesPerDomainLabel`
+
+# 1.5.9
+* (#107) Fix ambiguous ordinal for render mode attributes
+* (#104) Fix issue with background rendering in RecyclerView.
+* Adds a RecyclerView example to demo app.
+
+# 1.5.8
+* Maintenance release - update dependences, get off jcenter, etc.
+
+# 1.5.7
+* (#94) Potential fix / better error logging for a crash caused by a buffered canvas resize with illegal arguments.
+* (#93) Fix Android 9 compiler warnings.
+* (#83) Fix NPE when attempting to recycle an already null buffered canvas instance.
+* Remove unused `PlotRenderException`.
+* Added `IN_ORDER` BarRenderer mode.
+
+# 1.5.6
+* Adds convenience methods for saving / restoring `PanZoom` state.
+* (#80) Targets SDK 28, fixing compatibility issues.
+
+# 1.5.5
+
+* (#76) Fixed a bug that could cause a deadlock when grid steps are much larger than actual plot range.
+* (#78) Fixed a bug where setting insets on XYGraphWidget would have no effect after the plot was drawn.
+* XYGraphWidget.drawMarkerText is now marked `protected`.
+
+# 1.5.4
+
+* (#69) Fixed a bug in `SimpleXYPlot` preventing the resizing of `Y_VALS_ONLY` formatted series.
+* (#73) Fixed a bug where dynamically resizing a Plot (by marking a sibling view as `GONE`, etc.) would not resize the graph widget.
+
+# 1.5.3
+
+* Minor cleanup of Widget example source.
+* (#67) Fixed Javadoc link
+
+# 1.5.2
+
+_This version is pickier than it's predecessors about proper XML configuration. Where
+previous versions would silently ignore illegal XML attrs, a `RuntimeException` will be thrown._
+* Added [sizing documentation](plot_composition.md#sizing-widgets)
+* Added [custom renderer documentation](custom_renderer.md)
+* Fixed (#61) Bug in `XYGraphWidget.screenToSeriesY(...)`.
+* Fixed (#63) Fixed compatibility issue with Gradle 3.x.x that caused issues with XML parsing.
+
+# 1.5.1
+
+* (#52) Fixed minor NPE issue
+* (#55) Fixed bug with `PieRenderer.getContainingSegment` not working for very large segments.
+
+# 1.5.0
+
+_Updates to legend functionality in this version may result in changes to the display order
+of legend items in some cases. A custom `Comparator` can be used to resolve this if necessary;
+see the [legend doc](legend.md) for implementation details._
+
+* Added [legend doc](legend.md)
+* Added legend support to `PieChart`
+* Added configurable legend item sorting
+* (#45) Auto range boundaries calculation fix for when using a fixed domain range and a `FastXYSeries`
+* Minor Performance Optimizations
+
+# 1.4.3
+
+* (#39) `FastLineAndPointRenderer` now renders vertices for legend items.
+* Added [XML Attrs reference doc](attrs.md).
+
+# 1.4.2
+
+* (#32) New step mode: `INCREMENT_BY_FIT`.
+* (#33) `PanZoom` support for `INCREMENT_BY_FIT`.
+* (#34) Removed examples and documentation for serializing `SeriesRegistry` to preserve state.
+
+# 1.4.1
+
+* (#26) Fixed an NPE issue when drawing null values with a `PointLabeler`.
+* Fixed a broken link in Quickstart doc.
+
+# 1.4.0
+
+* Moderate refactor of `PieRenderer`. [Documentation](piechart.md) has been updated to reflect these changes.
+* Major refactor of `BarRenderer`. [Documentation](barchart.md) has been updated to reflect these changes.
+* Added `ScalingXYSeries` which wraps other instances of `XYSeries` to be dynamically scaled. This is
+particularly useful for creating animated intros using `XYSeries` data.
+* Added [AnimatedXYPlotActivity](../demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java)
+demonstrating the use of `ScalingXYSeries` to create an animated intro.
+* `XYPlot.getXVal(..)` and `XYPlot.getYVal(...)` methods have been deprecated and will be removed in 1.5.0.
+`XYPlot.screenToSeries(...)` and `XYPlot.seriesToScreen(...)` should be used instead.
+* Domain and range cursors are now disabled by default. To enable, set a valid cursor position using
+`XYGraphWidget.setCursorPosition(float, float)`. Cursor position values are expressed in screen coordinates;
+you can convert between screen and series values using `XYPlot.screenToSeries(...)` and `XYPlot.seriesToScreen(...)`.
+
+# 1.3.1
+
+* Added [NormedXYSeries](advanced_xy_plot.md#normedxyseries) wrapper to simplify the process of normalizing xy series data.
+* Added [DualScaleActivity](../demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java)
+demonstrating `NormedXYSeries` usage to present dual range scales.
+* LineAndPointRenderer options for cases where two or mode series' of different size have been added.
+* Fixed a bug causing points scrolled off-screen to occasionally accumulate and render along the left edge of the graph.
+* Fixed a bug that could cause render jitter when extreme zoom levels were applied.
+* Fixed a bug that prevented `PanZoom` from working properly on plots with an undefined outer limit.
+
# 1.3.0
* Added sampling support. See the [Advanced XY Plot](advanced_xy_plot.md) doc for details.
@@ -51,6 +164,7 @@ See the [pie chart documentation](piechart.md) for usage details.
* Removed InteractiveXYPlot as PanZoom makes it obsolete.
# 1.0.0
+
This is a factor of several core elements of the Androidplot lib. The general theme was to
make class and method names more intuitive and to make xml styling more powerful.
diff --git a/docs/versioning.md b/docs/versioning.md
new file mode 100644
index 00000000..b65b0c6f
--- /dev/null
+++ b/docs/versioning.md
@@ -0,0 +1,15 @@
+# Versioning
+Androidplot's versioning scheme is Major.Minor.Rev. For example, in the version 1.3.2 the Major is 1
+the Minor is 3 and the Rev is 2.
+
+Revisional releases (releases where only the Rev value has increased from the previous release, ie 1.3.1 -> 1.3.2)
+may include new features and bug fixes but will be fully backwards compatible with releases of the same minor version.
+**Updating to the latest revisional release should require no code changes in your app.**
+
+Minor Releases (releases where the Minor value has increased but the Major value has not) may contain
+new features, bug fixes, removed methods deprecated in the previous minor release
+and other moderate refactoring. **Updating to a new minor release may require code changes in your app.**
+
+Major releases (releases where the Major value has increased) are typically complete rewrites of core
+elements of the library. Unless otherwise specified in the release notes, **updating to a new major release
+will require significant code changes to your app.**
\ No newline at end of file
diff --git a/docs/xyplot.md b/docs/xyplot.md
index 6e375010..ce7e1526 100644
--- a/docs/xyplot.md
+++ b/docs/xyplot.md
@@ -1,18 +1,18 @@
# XYPlot
-An XYPlot renders one or more XYSeries onto an instance of XYGraphWidget. It also includes two instances
-of TextLabelWidget used to display an optional title for the domain and range axis, and an instance
-of [XYLegendWidget](#legend) which by default will automatically display legend items for each XYSeries added to the plot.
+An `XYPlot` renders one or more XYSeries onto an instance of XYGraphWidget. It also includes two instances
+of `TextLabelWidget` used to display an optional title for the domain and range axis, and an instance
+of [XYLegendWidget](#legend) which by default will automatically display legend items for each `XYSeries` added to the plot.

# XYSeries
-XYSeries is the interface Androidplot uses to retrieve your numeric data. You may either create your own
-implementation of XYSeries or use SimpleXYSeries if you don't have tight performance requirements or
+`XYSeries` is the interface Androidplot uses to retrieve your numeric data. You may either create your own
+implementation of `XYSeries` or use `SimpleXYSeries` if you don't have tight performance requirements or
if your numeric data is easily accessed as an array or list of values.
## SimpleXYSeries
-As a convenience, Androidplot provides an all-purpose implementation of the XYSeries interface called
-SimpleXYSeries. SimpleXYSeries is used to wrap your raw data with an implementation of the XYSeries interface.
+As a convenience, Androidplot provides an all-purpose implementation of the `XYSeries` interface called
+`SimpleXYSeries`. `SimpleXYSeries` is used to wrap your raw data with an implementation of the `XYSeries` interface.
You can supply your data in several ways:
@@ -37,21 +37,21 @@ Number[] yVals = {5, 2, 10, 5, 20, 10, 40, 20, 80, 40};
XYSeries series = new SimpleXYSeries(xVals, yVals, "my series");
```
-Keep in mind that SimpleXYSeries is designed to be easy to use for a broad number of applications; it's not
+Keep in mind that `SimpleXYSeries` is designed to be easy to use for a broad number of applications; it's not
optimized for any specific scenario; if you are dynamically displaying data that needs to be refreshed more than several
-times a second, consider building your own implementation of XYSeries designed for your app's
+times a second, consider building your own implementation of `XYSeries` designed for your app's
specific needs.
# The Graph
-XYGraphWidget encapsulates XYPlot's graphing functionality. Given an instance of XYPlot, a reference
-to XYGraphWidget can be retrieve via `XYPlot.getGraph()`.
+`XYGraphWidget` encapsulates XYPlot's graphing functionality. Given an instance of `XYPlot`, a reference
+to `XYGraphWidget` can be retrieve via `XYPlot.getGraph()`.
## Domain & Range Boundaries
-By default, Androidplot will analyze all XYSeries instances registered with the Plot, determine the
-min/max values for domain and range and adjust the Plot's boundaries to match those values. If your
+By default, Androidplot will analyze all `XYSeries` instances registered with the `XYPlot`, determine the
+min/max values for domain and range and adjust the `XYPlot` boundaries to match those values. If your
plot contains dynamic data, especially if your plot can periodically contain either no series data
or data with no resolution on one or both axis (all identical values for either x or y) then you may
-want to manually set your XYPlot's domain and range boundaries.
+want to manually set your `XYPlot`'s domain and range boundaries.
To set your plot's boundaries use:
@@ -64,19 +64,19 @@ modes, pass in null.
### BoundaryMode
Androidplot provides four boundary modes:
-#### Fixed
+#### FIXED
The plot's boundaries on the specified axis are fixed to user defined values.
-#### Auto (default)
+#### AUTO (default)
The plot's boundaries auto adjust to the min/max values for the defined axis.
-#### Grow
+#### GROW
The plot's boundaries automatically increase to the max value encountered by the plot. The initial
-determines the starting boundaries from which the Grow behavior will be based.
+determines the starting boundaries from which the `BoundaryMode.GROW` behavior will be based.
-#### Shrink
+#### SHRINK
The plot's boundaries automatically shrink to the min value encountered by the plot. The initial
-determines the starting boundaries from which the Shrink behavior will be based.
+determines the starting boundaries from which the shrink behavior will be based.
## Domain & Range Lines
These are the horizontal lines drawn on a graph. These lines are configured via:
@@ -87,14 +87,14 @@ These are the horizontal lines drawn on a graph. These lines are configured via
Androidplot provides these step modes:
### Subdivide
-When using `BoundaryMode.SUBDIVIDE`, the graph is subdivided into the specified number of sections.
+When using `StepMode.SUBDIVIDE`, the graph is subdivided into the specified number of sections.
### IncrementByValue
-`BoundaryMode.INCREMENT_BY_VALUE` instructs Androidplot draw grid lines at the specified interval. This
+`StepMode.INCREMENT_BY_VALUE` instructs Androidplot draw grid lines at the specified interval. This
is the most commonly used modes as is produces an easy to read result.
### IncrementByPixels
-`BoundaryMode.INCREMENT_BY_PIXELS` behaves identically to `BoundaryMode.INCREMENT_BY_VALUE` except that
+`StepMode.INCREMENT_BY_PIXELS` behaves identically to `StepMode.INCREMENT_BY_VALUE` except that
the increment quantity is expressed in pixels.
## Domain & Range Labels
@@ -107,18 +107,61 @@ Insets are used to control where line labels are drawn in relation to the graph
can be obtained via `XYPlot.getGraph().getLineLabelInsets()`. For example, to
move the range labels on the left of the graph further to the left by 5dp:
-**Programmatically:**
+xml
+```xml
+ap:lineLabelInsetLeft="-5dp"
+```
+
+java
```java
plot.getGraph().getLineLabelInsets().setLeft(PixelUtils.dpToPix(-5));
```
-**XML:**
+### Dual Axis Labels
+Androidplot provides methods for enabling / disabling axis labels along all edges of the graph. By
+default, only the left and bottom edge labels are enabled. To enable labels on the right edge:
+
+xml
```xml
-ap:lineLabelInsetLeft="-5dp"
+ap:lineLabels="left|bottom|right"
```
-### Dual Axis Labels
-Sometimes it is desirable to display additional labels for a single axis, each using it's own scale.
+java
+```java
+plot.getGraph().setLineLabelEdges(
+ XYGraphWidget.Edge.BOTTOM,
+ XYGraphWidget.Edge.LEFT,
+ XYGraphWidget.Edge.RIGHT);
+```
+
+Once the edge has been enabled, text formatting can be controlled by enabling a custom formatter
+for the desired edge:
+
+```java
+plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.RIGHT).setFormat(new Format() {
+ @Override
+ public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
+ // obj contains the raw Number value representing the position of the label being drawn.
+ // customize the labeling however you want here:
+ int i = Math.round(((Number) obj).floatValue());
+ return toAppendTo.append(i + " thingies");
+ }
+
+ @Override
+ public Object parseObject(String source, ParsePosition pos) {
+ // unused
+ return null;
+ }
+});
+```
+
+You're likely also going to need to normalize your series data in order to get it to display properly. To help simplify
+this step, Androidplot provides [NormedXYSeries](advanced_xy_plot.md#normedxyseries)
+which can be used to wrap any `XYSeries` to provide the normalized representation of it's data.
+
+There's a full [reference implementation](../demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java)
+of a dual scale plot using a custom `Formatter` and `NormedXYSeries` in the DemoApp.
+
### Line Label Interval
Androidplot allows you to configure the interval at which labels are rendered for domain and range lines:
@@ -174,6 +217,25 @@ then the plot will pan and zoom on both axes infinitely. To set the plot's oute
plot.getOuterLimits().set(0, 100, 0, 100);
```
+# Saving & Restoring PanZoom State
+The `PanZoom` class provides convenience methods for saving and restoring state from your `Activity`:
+
+```java
+// save the current pan/zoom state
+@Override
+public void onSaveInstanceState(Bundle bundle) {
+ bundle.putSerializable("pan-zoom-state", panZoom.getState());
+}
+
+// restore the previously saved pan/zoom state
+@Override
+public void onRestoreInstanceState(Bundle bundle) {
+ PanZoom.State state = (PanZoom.State) bundle.getSerializable("pan-zoom-state");
+ panZoom.setState(state);
+ plot.redraw();
+}
+```
+
For a more detailed look at pan & zoom behavior, check out the [Touch Zoom Example source code](../demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java).
# Series Renderers
@@ -184,8 +246,8 @@ There are several renderers available for XYPlots:
* CandlestickRenderer
When you add a new series to your plot, you tell Androidplot how to render it by passing
-in a subclass of XYSeriesFormatter that corresponds to the desired renderer. For example, to use
-LineAndPointRenderer, you'd register your series with an instance of LineAndPointFormatter:
+in a subclass of `XYSeriesFormatter` that corresponds to the desired renderer. For example, to use
+`LineAndPointRenderer`, you'd register your series with an instance of `LineAndPointFormatter`:
```java
LineAndPointFormatter format = new LineAndPointFormatter();
@@ -193,23 +255,51 @@ plot.addSeries(series, format);
```
If you need special behavior not provided by an existing renderer you can create
-your own by either extending XYSeriesRenderer or one of the above implementations. You'll also need
-to create a matching implementation of XYSeriesFormatter that returns your XYSeriesRenderer's class
+your own by either extending `XYSeriesRenderer` or one of the above implementations. You'll also need
+to create a matching implementation of `XYSeriesFormatter` that returns your renderer's class
from it's `getRendererClass()` method.
## LineAndPointRenderer
-TODO
-
-Androidplot includes two additional variations of LineAndPointRenderer:
+`LineAndPointRenderer` is the go-to renderer when it comes to `XYSeries`. It provides the most robust
+feature set of any `XYSeriesRenderer` and has been the most carefully optimized for performance. Having
+said that, `LineAndPointRenderer` isn't always the best tool for the job. Androidplot includes two
+additional variations of `LineAndPointRenderer`:
* **FastLineAndPointRenderer** - intended for use in apps displaying large amounts of dynamic data where
fast refresh rates are important.
* **AdvancedLineAndPointRenderer** - provides capabilities for dynamically coloring individual line
-segments, etc. See the ECG source in the demo app for a full source example.
-within a
+segments, etc. See the [ECGExample source](../demoapp/src/main/java/com/androidplot/demos/ECGExample.java)
+in the demo app for a complete example.
### Labeling Points
-TODO
+Most implementations of `XYSeriesRenderer` support labeling rendered points with text. This functionality
+is activated by setting the `PointLabelFormatter` property in the associated `XYSeriesFormatter`. For
+example, to enable point labels for a `LineAndPointFormatter`:
+
+// create a new `PointLabelFormatter` with a text size of 12sp and a color of red:
+
+```java
+PointLabelFormatter plf = new PointLabelFormatter();
+plf.getTextPaint().setTextSize(PixelUtils.spToPix(12));
+plf.getTextPaint().setColor(Color.RED);
+lineAndPointFormatter.setPointLabelFormatter(plf);
+```
+
+By default this will enable labels for all points using a string representation of the yVal of each
+point. This behavior can be customized by setting a custom instance of `PointLabeler` on the `XYSeriesFormatter`:
+
+```java
+formatter.setPointLabeler(new PointLabeler() {
+ @Override
+ public String getLabel(XYSeries series, int index) {
+ // draw labels on even indexes only:
+ if(index % 2 == 0) {
+ return "Y=" + series.getY(index).doubleValue();
+ }
+ return null;
+ }
+});
+```
## BarRenderer
See the [barcharts documentation](barchart.md).
@@ -221,49 +311,30 @@ See the [candlestick documentation](candlestick.md)
Smooth lines can be created by applying the
[Catmull-Rom interpolator](http://androidplot.com/smooth-curves-and-androidplot/) to your series' Format.
-# The Legend
-By default, Androidplot will automatically produce a legend for your Plot. You however choose to hide the legend
-or you can customize it to suit your needs.
+# The Legend
+By default, Androidplot will automatically produce a legend for your `XYPlot`. See [the legend](legend.md) doc
+for usage details.
-# Hiding Legend Items
-As mentioned above, Androidplot automatically produces a legend for your Plot. This "auto legend" includes
-items for each series added to the plot. If you wish to omit a series from the legend:
+# Graph Rotation
+Androidplot provides the `Widget.setRotation(Widget.Rotation)` method for controlling the orientation
+of Widgets. For example, if you wanted to create a bar graph where the bars extended across the screen
+from left to right:
-```java
-formatter.setLegendIconEnabled(false);
+xml
+```xml
+ap:graphRotation="ninety_degrees"
```
-## The TableModel
-The TableModel controls how and where each item in the legend is drawn. Androidplot provides two
-default implementations; DynamicTableModel and FixedTableModel (detailed below). All TableModel implementations
-organize elements into a grid. This grid is populated with items based on the order which it's corresponding
-series was added to the plot. This ordering can be further controlled by setting the TableModel's
-TableOrder param to either [ROW_MAJOR](https://en.wikipedia.org/wiki/Row-major_order) (items are added left-to-right, top-down) or COLUMN_MAJOR (items are added top-down, left-to-right).
-
-### DynamicTableModel
-The DynamicTableModel takes a desired of numbered rows and columns and evenly subdivides the LegendWidget's
-visible space into cells. For example, A 2x2 legend using ROW_MAJOR ordering:
-
+java
```java
-plot.getLegend().setTableModel(new DynamicTableModel(2, 2, TableOrder.ROW_MAJOR));
+plot.getGraph().setRotation(Widget.Rotation.NINETY_DEGREES);
```
-### FixedTableModel
-The FixedTableModel takes a desired size of each cell in pixels and adds cells using the specified TableOrder.
-It automatically wraps to the next row or column (based on TableOrder) when the cell being added
-exceeds the legend's available space on a given axis. For example, A FixedTableModel using 300w*100h cells and
-a TableOrder of COLUMN_MAJOR:
-
-```java
-plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300),
- PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR));
-```
-
# Optimization Tips
Here are a few suggestions to improve performance when plotting dynamic data:
-* Create your own implementation of XYSeries to work with your data in it's rawest form.
-* Use FastLineAndPointRenderer instead of LineAndPointRenderer:
+* Create your own implementation of `XYSeries` to work with your data in it's rawest form.
+* Use `FastLineAndPointRenderer` instead of `LineAndPointRenderer`:
```java
plot.addSeries(azimuthHistorySeries,
@@ -272,11 +343,49 @@ plot.addSeries(azimuthHistorySeries,
```
* Consider averaging or subsampling very large datasets before rendering. If you have the time and
-inclination, [the LTTB algorithm](http://skemman.is/stream/get/1946/15343/37285/3/SS_MSthesis.pdf) is particularly well suited for downsampling XY Series data.
+inclination, [the LTTB algorithm](http://skemman.is/stream/get/1946/15343/37285/3/SS_MSthesis.pdf) is
+particularly well suited for downsampling `XYSeries` data.
* If possible, avoid rendering vertices (points).
-* Disable anti-aliasing on your Formatter's paint values:
+* Disable anti-aliasing on your `XYSeriesFormatter`'s paint values:
```java
LineAndPointFormatter format = new LineAndPointFormatter(...);
format.getLinePaint().setAntiAlias(false);
```
+# Converting Values
+Because the coordinate system used by your `XYSeries` data is almost always different than the screen
+coordinate system upon which the data is rendered, you'll often need to convert from one system to
+the other. `XYPlot` provides convenience methods for this purpose:
+
+## Screen to Series Conversion
+```java
+// x
+float screenX = ...
+Number x = plot.screenToSeriesX(screenX);
+
+// y
+float screenY = ...
+Number y = plot.screenToSeriesY(screenY);
+
+// x and y
+PointF screenCoords = ...
+XYCoords xy = plot.screenToSeries(screenCoords)
+```
+
+## Series to Screen Conversion
+```java
+// x
+Number x = ...
+float screenX = plot.seriesToScreenX(x);
+
+// y
+Number y = ...
+float screenY = plot.seriesToScreenY(y);
+
+// x and y
+XYCoords xy = ...
+PointF screenCoords = plot.series.Screen(xy);
+```
+
+# Whats Next?
+Explore [Advanced XYPlot Topics](advanced_xy_plot.md)
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..d015431a
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+android.useAndroidX=true
+android.enableJetifier=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f7eef2f8..e9defc20 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,22 +1,6 @@
-#
-# Copyright 2015 AndroidPlot.com
-#
-# 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.
-#
-
-#Wed Dec 02 22:16:29 CST 2015
+#Fri Jul 19 09:24:31 CDT 2019
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-8.7-all.zip
diff --git a/misc/download_keystore.sh b/misc/download_keystore.sh
deleted file mode 100755
index 99fd503a..00000000
--- a/misc/download_keystore.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-# use curl to download a keystore from $KEYSTORE_URI, if set,
-# to the path/filename set in $KEYSTORE.
-if [[ $KEYSTORE && ${KEYSTORE} && $KEYSTORE_URI && ${KEYSTORE_URI} ]]
-then
- echo "Keystore detected - downloading..."
- curl -L -o ${KEYSTORE} ${KEYSTORE_URI}
-else
- echo "Keystore uri not set. .APK artifact will not be signed."
-fi
\ No newline at end of file