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/.idea/copyright/AndroidPlot_Apache_2_0.xml b/.idea/copyright/AndroidPlot_Apache_2_0.xml deleted file mode 100644 index af0b58d2..00000000 --- a/.idea/copyright/AndroidPlot_Apache_2_0.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml index cb90b4d2..8f7b02e7 100644 --- a/.idea/copyright/profiles_settings.xml +++ b/.idea/copyright/profiles_settings.xml @@ -1,5 +1,5 @@ - + 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 9124365b..9617a26e 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,48 @@ -# 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. +# ![image](docs/images/aplogo_small.png) Androidplot [![AndroidPlot](https://www.appbrain.com/stats/libraries/shield/androidplot.svg)](https://www.appbrain.com/stats/libraries/details/androidplot/androidplot) [![Codix](http://codix.io/gh/badge/halfhp/androidplot)](http://codix.io/gh/repo/halfhp/androidplot) [![codecov](https://codecov.io/gh/halfhp/androidplot/branch/master/graph/badge.svg)](https://codecov.io/gh/halfhp/androidplot) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/androidplot.svg?style=social&label=Follow%20Us)](https://twitter.com/androidplot) -**Supports:** -* Line Charts -* Scatter Charts -* Bar Charts -* Pie Charts -* Step Charts +A library for creating dynamic and static charts in Android apps. -##### Website http://androidplot.com -##### Bitbucket Repo: (Primary) https://bitbucket.org/androidplot/androidplot -##### Github Repo: https://github.com/halfhp/androidplot +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. -![image](http://androidplot.com/wp-content/uploads/2012/11/simplexyscreen.png) +If you enjoy the lib, please [rate us on codix.io](http://codix.io/gh/repo/halfhp/androidplot)! -## Demo App -[Play Store Listing](https://play.google.com/store/apps/details?id=com.androidplot.demos&hl=en) + -[Source Code](https://bitbucket.org/androidplot/androidplot/src/1538c5dfa56aed0d2cfdcbc7cdc6173e605543cd/demoapp/?at=master) +**Features:** -## Usage -To use the library in your gradle project add the following to your `build.gradle`: +* Line Charts +* Scatter Charts +* Bar Charts +* Pie Charts +* Step Charts +* Candlestick Charts +* Bubble Charts +* Dynamic plots +* Pan & Zoom -```groovy -dependencies { - compile 'com.androidplot:androidplot-core:0.9.6' -} -``` +# Usage -#### Quickstart -http://androidplot.com/docs/quickstart +* **[Quickstart](docs/quickstart.md)** :star: +* [Full Documentation](docs/index.md) -#### Documentation -http://androidplot.com/docs/ +# Links -## Bugs -We use Jira as our issue tracker - report bugs on https://androidplot.jira.com +* [Website](http://androidplot.com) +* [Github Repo](https://github.com/halfhp/androidplot) +* [Bitbucket Repo](https://bitbucket.org/androidplot/androidplot) +* [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](docs/contributing.md) -## Help +# 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). -## Contribute -http://androidplot.com/docs/working-with-androidplot-source/ +# License +Androidplot has been made available under the Apache 2.0 license: -## License - Copyright 2015 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 1d02906f..b37b2b4a 100644 --- a/androidplot-core/build.gradle +++ b/androidplot-core/build.gradle @@ -14,121 +14,176 @@ * limitations under the License. */ -apply plugin: 'android-sdk-manager' -apply plugin: 'com.android.library' -apply plugin: 'com.github.dcendents.android-maven' -apply plugin: 'com.jfrog.bintray' +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' version = theVersionName def siteUrl = 'http://androidplot.com' -def gitUrl = 'https://bitbucket.org/androidplot/androidplot.git' +def gitUrl = 'https://github.com/halfhp/androidplot.git' dependencies { - testCompile group: 'org.jmockit', name: 'jmockit', version: '1.20' - testCompile group: 'junit', name: 'junit', version: '4.8.1' - testCompile "org.robolectric:robolectric:3.0" -} -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://androidplot.jira.com" - websiteUrl = siteUrl - vcsUrl = gitUrl - licenses = ["Apache-2.0"] - } -} - -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/xy/XYGraphBounds.java b/androidplot-core/src/main/java/com/androidplot/LineLabelFormatter.java similarity index 64% rename from androidplot-core/src/main/java/com/androidplot/xy/XYGraphBounds.java rename to androidplot-core/src/main/java/com/androidplot/LineLabelFormatter.java index b0bc295b..ec92556b 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphBounds.java +++ b/androidplot-core/src/main/java/com/androidplot/LineLabelFormatter.java @@ -1,27 +1,30 @@ -/* - * 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; - -public class XYGraphBounds { - - - private Number minX; - private Number maxX; - private Number minY; - private Number maxY; - -} +/* + * Copyright 2016 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.graphics.Paint; + +public interface LineLabelFormatter { + + + /** + * + * @param value The value being rendered by this formatter. + * @return Paint instance that should be used to render the specified value. + */ + Paint getPaint(Number value); +} diff --git a/androidplot-core/src/main/java/com/androidplot/LineRegion.java b/androidplot-core/src/main/java/com/androidplot/LineRegion.java deleted file mode 100644 index 69993855..00000000 --- a/androidplot-core/src/main/java/com/androidplot/LineRegion.java +++ /dev/null @@ -1,101 +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; - -/** - * A one dimensional region represented by a starting and ending value. - */ -public class LineRegion { - private Number minVal; - private Number maxVal; - - public LineRegion(Number val1, Number v2) { - if (val1.doubleValue() < v2.doubleValue()) { - this.setMinVal(val1); - this.setMaxVal(v2); - } else { - this.setMinVal(v2); - this.setMaxVal(val1); - } - } - - public static Number measure(Number val1, Number val2) { - return new LineRegion(val1, val2).length(); - } - - public Number length() { - return maxVal.doubleValue() - minVal.doubleValue(); - } - - /** - * Tests whether a value is within the given range - * @param value - * @return - */ - public boolean contains(Number value) { - return value.doubleValue() >= minVal.doubleValue() && value.doubleValue() <= maxVal.doubleValue(); - } - - public boolean intersects(LineRegion lineRegion) { - return intersects(lineRegion.getMinVal(), lineRegion.getMaxVal()); - } - - /** - * Tests whether this segment intersects another - * @param line2Min - * @param line2Max - * @return - */ - public boolean intersects(Number line2Min, Number line2Max) { - - //double l1min = getMinVal() == null ? Double.NEGATIVE_INFINITY : getMinVal().doubleValue(); - //double l1max = getMaxVal() == null ? Double.POSITIVE_INFINITY : getMaxVal().doubleValue(); - - //double l2min = line2Min == null ? Double.NEGATIVE_INFINITY : line2Min.doubleValue(); - //double l2max = line2Max == null ? Double.POSITIVE_INFINITY : line2Max.doubleValue(); - - - // is this line completely within line2? - if(line2Min.doubleValue() <= this.minVal.doubleValue() && line2Max.doubleValue() >= this.maxVal.doubleValue()) { - return true; - // is line1 partially within line2 - } else return contains(line2Min) || contains(line2Max); - } - - public Number getMinVal() { - return minVal; - } - - public void setMinVal(Number minVal) { - if(minVal == null) { - throw new NullPointerException("Region values can never be null."); - } - this.minVal = minVal; - } - - public Number getMaxVal() { - return maxVal; - } - - public void setMaxVal(Number maxVal) { - if(maxVal == null) { - throw new NullPointerException("Region values can never be null."); - } - this.maxVal = maxVal; - } -} diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index 02e93078..63d2e831 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -1,993 +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.TextOrientationType; -import com.androidplot.ui.widget.TextLabelWidget; -import com.androidplot.ui.SeriesRenderer; -import com.androidplot.util.AttrUtils; -import com.androidplot.util.Configurator; -import com.androidplot.util.DisplayDimensions; -import com.androidplot.util.PixelUtils; -import com.androidplot.ui.XLayoutStyle; -import com.androidplot.ui.YLayoutStyle; - -import java.lang.reflect.Field; -import java.util.*; - -/** - * Base class for all Plot implementations. - */ -public abstract class Plot - 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 ArrayList> getSeriesRegistry() { - return seriesRegistry; - } - - 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(3, 3, 3, 3, 3, 3, 3, 3); - private BorderStyle borderStyle = Plot.BorderStyle.SQUARE; - private float borderRadiusX = 15; - private float borderRadiusY = 15; - private Paint borderPaint; - private Paint backgroundPaint; - private LayoutManager layoutManager; - private TextLabelWidget titleWidget; - 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 ArrayList> seriesRegistry; - - private final ArrayList listeners; - - private Thread renderThread; - private boolean keepRunning = false; - private boolean isIdle = true; - - { - listeners = new ArrayList<>(); - seriesRegistry = new ArrayList<>(); - 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 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); - } - } - - - /** - * 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(null, null, 0); - setTitle(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 should 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 abstract void onPreInit(); - - - private void init(Context context, AttributeSet attrs, int defStyle) { - PixelUtils.init(getContext()); - layoutManager = new LayoutManager(); - titleWidget = new TextLabelWidget(layoutManager, new Size(25, - SizeLayoutType.ABSOLUTE, 100, - SizeLayoutType.ABSOLUTE), - TextOrientationType.HORIZONTAL); - titleWidget.position(0, XLayoutStyle.RELATIVE_TO_CENTER, 0, - YLayoutStyle.ABSOLUTE_FROM_TOP, AnchorPosition.TOP_MIDDLE); - - // initialize attr defaults: - titleWidget.getLabelPaint().setTextSize( - PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP)); - - onPreInit(); - // make sure the title widget is always the topmost widget: - layoutManager.moveToTop(titleWidget); - if(context != null && attrs != null) { - loadAttrs(attrs, defStyle); - } - - 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; - } - } - } - } - } - }); - } - } - - /** - * 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) { - - // renderMode - RenderMode renderMode = RenderMode.values() - [attrs.getInt(R.styleable.Plot_renderMode, getRenderMode().ordinal())]; - if(renderMode != getRenderMode()) { - setRenderMode(renderMode); - } - - // title - setTitle(attrs.getString(R.styleable.Plot_label)); - getTitleWidget().getLabelPaint().setTextSize( - attrs.getDimension(R.styleable.Plot_labelTextSize, - PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP))); - - getTitleWidget().getLabelPaint().setColor(attrs.getColor( - R.styleable.Plot_labelTextColor, getTitleWidget().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; - - final String appPkg = getContext().getPackageName(); - Class styleableClass = null; - try { - /** - * Need to retrieve R$styleable.class dynamically to avoid exceptions - * in environments where this class does not exist, such as .jar users - * that have not merged the library's attrs.xml with their own. - */ - styleableClass = Class.forName(appPkg + ".R$styleable"); - - } catch (ClassNotFoundException e) { - // when running as a preview in IntelliJ or AndroidStudio it seems that the package of R is - // something else; this fixes that issue: - if(isInEditMode()) { - styleableClass = R.styleable.class; - } - } - - if(styleableClass != null) { - String styleableName = getClass().getName().substring(BASE_PACKAGE.length()); - styleableName = styleableName.replace('.', '_'); - try { - /** - * Use reflection to safely check for the existence of styleable defs for Plot - * and it's derivatives. This safety check 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 check 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)); - } - } - Configurator.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 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); - } - } - - /** - * 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) { - Class rendererClass = formatter.getRendererClass(); - - if(getSeries(series, rendererClass) != null) { - return false; - } - - // 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); - } - - getSeriesRegistry().add(new SeriesAndFormatter<>(series, formatter)); - return true; - } - - /** - * - * @param series - * @param rendererClass - * @return The {@link SeriesAndFormatter} that matches the series and rendererClass params, or null if one is not found. - */ - protected SeriesAndFormatter getSeries(SeriesType series, Class rendererClass) { - for(SeriesAndFormatter thisPair : getSeriesRegistry()) { - if(thisPair.getSeries() == series && thisPair.getFormatter().getRendererClass() == rendererClass) { - return thisPair; - } - } - return null; - } - - /** - * - * @param series - * @return A List of {@link SeriesAndFormatter} instances that reference series. - */ - protected List> getSeries(SeriesType series) { - List> results = - new ArrayList>(); - for(SeriesAndFormatter thisPair : getSeriesRegistry()) { - if(thisPair.getSeries() == series) { - results.add(thisPair); - } - } - return results; - } - - /** - * - * 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 The SeriesAndFormatterPair that was removed or null if nothing was removed. - */ - public synchronized SeriesAndFormatter removeSeries(SeriesType series, Class rendererClass) { - - List> results = getSeries(series); - SeriesAndFormatter result = null; - for(SeriesAndFormatter thisPair : results) { - if(thisPair.getFormatter().getRendererClass() == rendererClass) { - result = thisPair; - getSeriesRegistry().remove(result); - break; - } - } - - // if series implements PlotListener and is not assigned to any other renderers remove it as a listener: - if(series instanceof PlotListener && results.size() == 1) { - removeListener((PlotListener) series); - } - - return result; - } - - /** - * Remove all occurrences of series regardless of the associated Renderer. - * @param series - */ - public synchronized void removeSeries(SeriesType series) { - - for(Iterator> it = getSeriesRegistry().iterator(); it.hasNext();) { - if(it.next().getSeries() == series) { - it.remove(); - } - } - - // if series implements PlotListener, remove it from listeners: - if (series instanceof PlotListener) { - removeListener((PlotListener) series); - } - } - - /** - * Remove all series from the plot. - */ - public void clear() { - for(Iterator> it = getSeriesRegistry().iterator(); it.hasNext();) { - it.next(); - it.remove(); - } - } - - public boolean isEmpty() { - return getSeriesRegistry().isEmpty(); - } - - /** - * - * @param series - * @param rendererClass - * @return The Formatter instance corresponding to the specified series / renderer pair. - */ - public FormatterType getFormatter(SeriesType series, Class rendererClass) { - return getSeries(series, rendererClass).getFormatter(); - } - - public RendererType getRenderer(Class rendererClass) { - return 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) { - 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 check 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); - } - } finally { - 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); - } - } - - - /** - * 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: - canvas.drawRect(dims, paint); - break; - default: - } - } - - /** - * - * @return The displayed title of this Plot. - */ - public String getTitle() { - return getTitleWidget().getText(); - } - - /** - * - * @param title The title to display on this Plot. - */ - public void setTitle(String title) { - titleWidget.setText(title); - } - - public LayoutManager getLayoutManager() { - return layoutManager; - } - - public void setLayoutManager(LayoutManager layoutManager) { - this.layoutManager = layoutManager; - } - - public TextLabelWidget getTitleWidget() { - return titleWidget; - } - - public void setTitleWidget(TextLabelWidget titleWidget) { - this.titleWidget = titleWidget; - } - - 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 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 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 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/PlotListener.java b/androidplot-core/src/main/java/com/androidplot/PlotListener.java index 1c793a33..b2400a0a 100644 --- a/androidplot-core/src/main/java/com/androidplot/PlotListener.java +++ b/androidplot-core/src/main/java/com/androidplot/PlotListener.java @@ -31,7 +31,7 @@ public interface PlotListener { * @param source * @param canvas */ - public void onBeforeDraw(Plot source, Canvas canvas); + void onBeforeDraw(Plot source, Canvas canvas); /** * Fired immediately after the Plot "source" is drawn onto canvas. @@ -41,5 +41,5 @@ public interface PlotListener { * @param source * @param canvas */ - public void onAfterDraw(Plot source, Canvas canvas); + void onAfterDraw(Plot source, Canvas canvas); } diff --git a/androidplot-core/src/main/java/com/androidplot/Region.java b/androidplot-core/src/main/java/com/androidplot/Region.java new file mode 100644 index 00000000..f22f9115 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/Region.java @@ -0,0 +1,268 @@ +/* + * 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.util.FastNumber; + +/** + * A one dimensional region represented by a starting and ending value. + */ +public class Region { + private FastNumber min; + private FastNumber max; + private FastNumber cachedLength; + + private Region defaults = this; + + public Region() {} + + public static Region withDefaults(Region defaults) { + if(defaults == null || !defaults.isDefined()) { + throw new IllegalArgumentException("When specifying default min and max must both be non-null values"); + } + Region r = new Region(); + r.defaults = defaults; + return r; + } + + public Region(Number v1, Number v2) { + if (v1 != null && v2 != null && v1.doubleValue() < v2.doubleValue()) { + this.setMin(v1); + this.setMax(v2); + } else { + this.setMin(v2); + this.setMax(v1); + } + } + + public void setMinMax(Region region) { + setMin(region.getMin()); + setMax(region.getMax()); + } + + /** + * + * @param v1 + * @param v2 + * @return The distance between val1 and val2 or null if either parameters are null. + * @since 0.9.7 + */ + public static Number measure(Number v1, Number v2) { + return new Region(v1, v2).length(); + } + + public Number length() { + if(cachedLength == null) { + Number l = getMax() == null || getMin() == null ? + null : getMax().doubleValue() - getMin().doubleValue(); + if(l != null) { + cachedLength = FastNumber.orNull(l); + } + } + return cachedLength; + } + + /** + * Tests whether a value is within the given range + * @param value + * @return + */ + public boolean contains(Number value) { + return value.doubleValue() >= getMin().doubleValue() && value.doubleValue() <= getMax().doubleValue(); + } + + public boolean intersects(Region region) { + return intersects(region.getMin(), region.getMax()); + } + + /** + * + * @return Middle value within this region + */ + public Number center() { + return getMax().doubleValue() - (length().doubleValue() / 2); + } + + /** + * Transform a value relative to this region into it's corresponding value relative to the + * specified region. + * @param value + * @param region2 + * @return + */ + public Number transform(double value, Region region2) { + return transform(value, region2, false); + } + + public Number transform(double value, Region region2, boolean flip) { + return transform(value, region2.getMin().doubleValue(), region2.getMax().doubleValue(), flip); + } + + public double transform(double value, double min, double max, boolean flip) { + double range = length().doubleValue(); + final double r2 = max - min; + + // TODO: refactor to use ratio here + final double scale = r2 / range; + if(!flip) { + return min + (scale * (value - this.getMin().doubleValue())); + } else { + return max - (scale * (value - this.getMin().doubleValue())); + } + } + + public Number ratio(Region r2) { + return ratio(r2.getMin().doubleValue(), r2.getMax().doubleValue()); + } + + /** + * + * @param min + * @param max + * @return length of this series divided by the length of the distance between min and max. + */ + public double ratio(double min, double max) { + return length().doubleValue() / (max - min); + } + + + public void union(Number value) { + if(value == null) { + return; + } + double val = value.doubleValue(); + if(getMin() == null || + val < getMin().doubleValue()) { + setMin(value); + } + if(getMax() == null || val > + getMax().doubleValue()) { + setMax(value); + } + } + + /** + * Compares the input bounds min/max against this instance's current min/max. + * If the input.min is less than this.min then this.min will be set to input.min. + * If the input.max is greater than this.max then this.max will be set to input.max + * + * The result of a union will always be an equal or larger size region. + * @param input + */ + public void union(Region input) { + union(input.getMin()); + union(input.getMax()); + } + + /** + * The result of an intersect will always be an equal or smaller size region. + * @param input + */ + public void intersect(Region input) { + if(getMin().doubleValue() < input.getMin().doubleValue()) { + setMin(input.getMin()); + } + + if(getMax().doubleValue() > input.getMax().doubleValue()) { + setMax(input.getMax()); + } + } + + /** + * Tests whether this segment intersects another + * @param line2Min + * @param line2Max + * @return + */ + public boolean intersects(Number line2Min, Number line2Max) { + + // is this line completely within line2? + if(line2Min.doubleValue() <= getMin().doubleValue() && line2Max.doubleValue() >= getMax().doubleValue()) { + return true; + // is line1 partially within line2 + } else return contains(line2Min) || contains(line2Max); + } + + public boolean isMinSet() { + return min != null; + } + + public Number getMin() { + return isMinSet() ? min : defaults.min; + } + + public void setMin(Number min) { + cachedLength = null; + if(min == null) { + if(defaults == null) { + throw new NullPointerException( + "Region values cannot be null unless defaults have been set."); + } else { + this.min = null; + } + } else if (this.min == null || !this.min.equals(min)) { + this.min = FastNumber.orNull(min); + } + } + + public boolean isMaxSet() { + return max != null; + } + + public Number getMax() { + return isMaxSet() ? max : defaults.max; + } + + public void setMax(Number max) { + cachedLength = null; + if(max == null) { + if(defaults == null) { + throw new NullPointerException( + "Region values can never be null unless defaults have been set."); + } else { + this.max = null; + } + } else if (this.max == null || !this.max.equals(max)) { + this.max = FastNumber.orNull(max); + } + } + + /** + * + * @return True if both min and max values are non-null, false otherwise. Does *not* consider defaults. + */ + 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/Series.java b/androidplot-core/src/main/java/com/androidplot/Series.java index 92cbb005..a3c26e7b 100644 --- a/androidplot-core/src/main/java/com/androidplot/Series.java +++ b/androidplot-core/src/main/java/com/androidplot/Series.java @@ -25,6 +25,6 @@ public interface Series { * * @return The title of this Series. */ - public String getTitle(); + String getTitle(); } diff --git a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java new file mode 100644 index 00000000..7a7b3023 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016 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.ui.Formatter; +import com.androidplot.ui.SeriesBundle; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Manages a list of {@link Series} and their associated {@link Formatter} in the context of a {@link Plot}. + * @since 0.9.7 + */ +public abstract class SeriesRegistry + , + SeriesType extends Series, FormatterType extends Formatter> implements Serializable { + + private ArrayList registry = new ArrayList<>(); + + public List getSeriesAndFormatterList() { + return registry; + } + public List getSeriesList() { + List result = new ArrayList<>(registry.size()); + for(SeriesBundle sfPair : registry) { + result.add(sfPair.getSeries()); + } + return result; + } + + public int size() { + return registry.size(); + } + + public boolean isEmpty() { + return registry.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)); + } + + protected abstract BundleType newSeriesBundle(SeriesType series, FormatterType formatter); + + /** + * + * @param series + * @return A List of {@link SeriesBundle} instances that reference series. + */ + protected List> get(SeriesType series) { + List> results = + new ArrayList<>(); + for(SeriesBundle thisPair : registry) { + if(thisPair.getSeries() == series) { + results.add(thisPair); + } + } + return results; + } + + public synchronized List remove(SeriesType series, Class rendererClass) { + ArrayList removedItems = new ArrayList<>(); + for(Iterator it = registry.iterator(); it.hasNext();) { + BundleType b = it.next(); + if(b.getSeries() == series && b.getFormatter().getRendererClass() == rendererClass) { + it.remove(); + removedItems.add(b); + } + } + return removedItems; + } + + /** + * Remove all occurrences of series regardless of the associated Renderer. + * @param series + */ + public synchronized boolean remove(SeriesType series) { + boolean result = false; + for(Iterator it = registry.iterator(); it.hasNext();) { + if(it.next().getSeries() == series) { + it.remove(); + result = true; + } + } + return result; + } + + /** + * Remove all series from the plot. + */ + public void clear() { + for(Iterator it + = registry.iterator(); it.hasNext();) { + it.next(); + it.remove(); + } + } + + public List> getLegendEnabledItems() { + List> sfList = new ArrayList<>(); + for(SeriesBundle sf : registry) { + if(sf.getFormatter().isLegendIconEnabled()) { + sfList.add(sf); + } + } + return sfList; + } + + public boolean contains(SeriesType series, Class 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/SimpleLineLabelFormatter.java b/androidplot-core/src/main/java/com/androidplot/SimpleLineLabelFormatter.java new file mode 100644 index 00000000..6be02003 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/SimpleLineLabelFormatter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 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.graphics.Color; +import android.graphics.Paint; +import com.androidplot.util.PixelUtils; + +/** + * A basic implementation of a {@link LineLabelFormatter}. + */ +public class SimpleLineLabelFormatter implements LineLabelFormatter { + + private static final int DEFAULT_TEXT_SIZE_SP = 12; + private static final int DEFAULT_STROKE_SIZE_DP = 2; + private Paint paint; + + public SimpleLineLabelFormatter() { + this(new Paint()); + getPaint().setColor(Color.WHITE); + getPaint().setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_SP)); + getPaint().setStrokeWidth(PixelUtils.dpToPix(DEFAULT_STROKE_SIZE_DP)); + } + + public SimpleLineLabelFormatter(int color) { + this(); + getPaint().setColor(color); + } + + public SimpleLineLabelFormatter(Paint paint) { + this.paint = paint; + } + + public Paint getPaint() { + return paint; + } + + public void setPaint(Paint paint) { + this.paint = paint; + } + + @Override + public Paint getPaint(Number value) { + return getPaint(); + } +} 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 0c542964..35e5e416 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java @@ -19,17 +19,17 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; -import com.androidplot.Plot; -import com.androidplot.R; -import com.androidplot.ui.AnchorPosition; -import com.androidplot.ui.SizeLayoutType; -import com.androidplot.ui.Size; + +import com.androidplot.*; +import com.androidplot.ui.*; import com.androidplot.util.AttrUtils; import com.androidplot.util.PixelUtils; -import com.androidplot.ui.XLayoutStyle; -import com.androidplot.ui.YLayoutStyle; -public class PieChart extends Plot { +/** + * Basic representation of a Pie Chart that displays a title and pie widget. + */ +public class PieChart extends Plot { private static final int DEFAULT_PIE_WIDGET_H_DP = 18; private static final int DEFAULT_PIE_WIDGET_W_DP = 10; @@ -37,12 +37,22 @@ public class PieChart extends Plot { private static final int DEFAULT_PIE_WIDGET_Y_OFFSET_DP = 0; private static final int DEFAULT_PIE_WIDGET_X_OFFSET_DP = 0; - public void setPieWidget(PieWidget pieWidget) { - this.pieWidget = pieWidget; - } + private static final int DEFAULT_LEGEND_WIDGET_H_DP = 30; + private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 18; + private static final int DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP = 0; + private static final int DEFAULT_LEGEND_WIDGET_X_OFFSET_DP = 40; + + private static final int DEFAULT_PADDING_DP = 5; @SuppressWarnings("FieldCanBeLocal") - private PieWidget pieWidget; + private PieWidget pie; + + private PieLegendWidget legend; + + @Override + protected SegmentRegistry getRegistryInstance() { + return new SegmentRegistry(); + } public PieChart(Context context, String title) { super(context, title); @@ -58,28 +68,46 @@ public PieChart(Context context, AttributeSet attributes) { @Override protected void onPreInit() { - pieWidget = new PieWidget( + pie = new PieWidget( getLayoutManager(), this, new Size( PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_H_DP), - SizeLayoutType.FILL, + SizeMode.FILL, PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_W_DP), - SizeLayoutType.FILL)); + SizeMode.FILL)); - pieWidget.position( + pie.position( PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_X_OFFSET_DP), - XLayoutStyle.ABSOLUTE_FROM_CENTER, + HorizontalPositioning.ABSOLUTE_FROM_CENTER, PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_Y_OFFSET_DP), - YLayoutStyle.ABSOLUTE_FROM_CENTER, - AnchorPosition.CENTER); + VerticalPositioning.ABSOLUTE_FROM_CENTER, + Anchor.CENTER); - pieWidget.setPadding(10, 10, 10, 10); - - // TODO: can't remember why this getClass() check is neccessary. test if it actually is... - /*if (getClass().equals(PieChart.class) && attrs != null) { - loadAttrs(context, attrs); - }*/ + legend = new PieLegendWidget( + getLayoutManager(), + this, + new Size( + PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_H_DP), + SizeMode.ABSOLUTE, 0.5f, SizeMode.RELATIVE), + new DynamicTableModel(0, 1), + new Size( + PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP), + SizeMode.ABSOLUTE, + PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP), + SizeMode.ABSOLUTE)); + + legend.position( + PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_X_OFFSET_DP), + HorizontalPositioning.ABSOLUTE_FROM_RIGHT, + PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP), + VerticalPositioning.ABSOLUTE_FROM_BOTTOM, + Anchor.RIGHT_BOTTOM); + + legend.setVisible(false); + + final float padding = PixelUtils.dpToPix(DEFAULT_PADDING_DP); + pie.setPadding(padding, padding, padding, padding); } @Override @@ -90,8 +118,12 @@ protected void processAttrs(TypedArray attrs) { R.styleable.pie_PieChart_pieBorderColor, R.styleable.pie_PieChart_pieBorderThickness); } - public PieWidget getPieWidget() { - return pieWidget; + public void setPie(PieWidget pie) { + this.pie = pie; + } + + public PieWidget getPie() { + return pie; } public void addSegment(Segment segment, SegmentFormatter formatter) { @@ -101,4 +133,12 @@ public void addSegment(Segment segment, SegmentFormatter formatter) { public void removeSegment(Segment segment) { removeSeries(segment); } + + public PieLegendWidget getLegend() { + return legend; + } + + public void setLegend(PieLegendWidget legend) { + this.legend = legend; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieLegendItem.java b/androidplot-core/src/main/java/com/androidplot/pie/PieLegendItem.java new file mode 100644 index 00000000..669b188b --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieLegendItem.java @@ -0,0 +1,26 @@ +package com.androidplot.pie; + + +import androidx.annotation.NonNull; + +import com.androidplot.ui.widget.LegendItem; + +/** + * An item in a {@link PieLegendWidget} corresponding to a {@link Segment} in a {@link PieChart}. + */ +public class PieLegendItem implements LegendItem { + + public SegmentFormatter formatter; + + public Segment segment; + + public PieLegendItem(@NonNull Segment segment, @NonNull SegmentFormatter formatter) { + this.segment = segment; + this.formatter = formatter; + } + + @Override + public String getTitle() { + return segment.getTitle(); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieLegendWidget.java b/androidplot-core/src/main/java/com/androidplot/pie/PieLegendWidget.java new file mode 100644 index 00000000..4d1ccd03 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieLegendWidget.java @@ -0,0 +1,41 @@ +package com.androidplot.pie; + +import android.graphics.Canvas; +import android.graphics.RectF; +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.LegendWidget; + +import java.util.ArrayList; +import java.util.List; + +public class PieLegendWidget extends LegendWidget { + + 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 055de598..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,19 +18,25 @@ import android.graphics.*; -import com.androidplot.exception.PlotRenderException; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.SeriesRenderer; import com.androidplot.ui.RenderStack; import java.util.List; +/** + * Basic renderer for drawing pie charts. + */ 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; @@ -38,7 +44,6 @@ public class PieRenderer extends SeriesRenderer sfPair : getSeriesList()) { + for (SeriesBundle 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); - float cx = bounds.centerX(); - float cy = bounds.centerY(); + PointF translated = calculateLineEnd( + bounds.centerX(), bounds.centerY(), f.getOffset(), halfSweepEndAngle); + + final float cx = translated.x; + 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); } - // do we have a pie chart of less than 100% - if(Math.abs(sweep - 360f) > Float.MIN_VALUE) { + final float outerRad = rad - f.getOuterInset(); + final float innerRad = donutSizePx == 0 ? 0 : donutSizePx + f.getInnerInset(); + + // 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, rad, startAngle); - PointF r1Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle); + PointF r1Outer = calculateLineEnd(cx, cy, outerRad, startAngle); + PointF r1Inner = calculateLineEnd(cx, cy, innerRad, startAngle); // vertices of the second radial: - PointF r2Outer = calculateLineEnd(cx, cy, rad, startAngle + sweep); - PointF r2Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle + sweep); + PointF r2Outer = calculateLineEnd(cx, cy, outerRad, startAngle + sweep); + PointF r2Inner = calculateLineEnd(cx, cy, innerRad, startAngle + sweep); Path clip = new Path(); + // outer arc: // leave plenty of room on the outside for stroked borders; // necessary because the clipping border is ugly // and cannot be easily anti aliased. Really we only care about masking off the // radial edges. - clip.arcTo(new RectF(bounds.left - rad, - bounds.top - rad, - bounds.right + rad, - bounds.bottom + rad), + clip.arcTo(new RectF(bounds.left - outerRad, + bounds.top - outerRad, + bounds.right + outerRad, + bounds.bottom + outerRad), startAngle, sweep); clip.lineTo(cx, cy); clip.close(); canvas.clipPath(clip); Path p = new Path(); - p.arcTo(bounds, startAngle, sweep); + + // outer arc: + p.arcTo(new RectF( + 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 - donutSizePx, - cy - donutSizePx, - cx + donutSizePx, - cy + donutSizePx), + cx - innerRad, + cy - innerRad, + cx + innerRad, + cy + innerRad), startAngle + sweep, -sweep); p.close(); @@ -140,36 +168,37 @@ 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, rad, Path.Direction.CW); + chart.addCircle(cx, cy, outerRad, Path.Direction.CW); Path inside = new Path(); - inside.addCircle(cx, cy, donutSizePx, Path.Direction.CW); + inside.addCircle(cx, cy, innerRad, Path.Direction.CW); canvas.clipPath(inside, Region.Op.DIFFERENCE); canvas.drawPath(chart, f.getFillPaint()); canvas.restore(); } // draw inner line: - canvas.drawCircle(cx, cy, donutSizePx, f.getInnerEdgePaint()); + canvas.drawCircle(cx, cy, innerRad, f.getInnerEdgePaint()); // draw outer line: - canvas.drawCircle(cx, cy, rad, f.getOuterEdgePaint()); + canvas.drawCircle(cx, cy, outerRad, f.getOuterEdgePaint()); canvas.restore(); PointF labelOrigin = calculateLineEnd(cx, cy, - (rad-((rad- donutSizePx)/2)), startAngle + (sweep/2)); + (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. - drawSegmentLabel(canvas, labelOrigin, seg, f); + 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()); } @@ -186,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() { - List> seriesList = getSeriesList(); - double[] result = new double[seriesList.size()]; - int i = 0; - for (SeriesAndFormatter sfPair : seriesList) { - result[i] = sfPair.getSeries().getValue().doubleValue(); - i++; - } - return result; - } + + /** + * 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; + } protected PointF calculateLineEnd(float x, float y, float rad, float deg) { return calculateLineEnd(new PointF(x, y), rad, deg); @@ -209,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); @@ -217,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."); } @@ -237,60 +280,123 @@ 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. */ public Segment getContainingSegment(PointF point) { - RectF plotArea = getPlot().getPieWidget().getWidgetDimensions().marginatedRect; + RectF plotArea = getPlot().getPie().getWidgetDimensions().marginatedRect; // figure out the angle in degrees of the line between the clicked point // and the origin of the plotArea: PointF origin = new PointF(plotArea.centerX(), plotArea.centerY()); 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 // the angle calculated above - List> seriesList = getSeriesList(); + List> seriesList = getSeriesAndFormatterList(); int i = 0; double[] values = getValues(); double scale = calculateScale(values); - float offset = startDeg; - for (SeriesAndFormatter sfPair : seriesList) { + 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; + } } - public float getStartDeg() { - return startDeg; + /** + * 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; } - - public void setEndDeg(float deg) { - endDeg = deg; + + /** + * 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 getStartDegs() { + return startDegs; + } + + /** + * 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/pie/SegmentBundle.java b/androidplot-core/src/main/java/com/androidplot/pie/SegmentBundle.java new file mode 100644 index 00000000..465fd69b --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/pie/SegmentBundle.java @@ -0,0 +1,14 @@ +package com.androidplot.pie; + +import com.androidplot.ui.*; + +/** + * Manages the association between a given {@link Segment} and the {@link SegmentFormatter} that + * will be used to render it. + */ +public class SegmentBundle extends SeriesBundle { + + public SegmentBundle(Segment series, SegmentFormatter formatter) { + super(series, formatter); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java b/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java index 390de996..04368a01 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java @@ -16,6 +16,7 @@ package com.androidplot.pie; +import android.content.*; import android.graphics.Color; import android.graphics.Paint; import com.androidplot.ui.SeriesRenderer; @@ -38,6 +39,11 @@ public class SegmentFormatter extends Formatter { private Paint labelPaint; private Paint labelMarkerPaint; + private float offset; + private float radialInset; + private float innerInset; + private float outerInset; + { setFillPaint(new Paint()); // outer edge: @@ -64,7 +70,6 @@ public class SegmentFormatter extends Formatter { getLabelPaint().setTextSize(DEFAULT_LABEL_FONT_SIZE); getLabelPaint().setAntiAlias(true); getLabelPaint().setTextAlign(Paint.Align.CENTER); - //getLabelPaint().setShadowLayer(5, 4, 4, Color.BLACK); // label marker paint: setLabelMarkerPaint(new Paint()); @@ -72,11 +77,6 @@ public class SegmentFormatter extends Formatter { getLabelMarkerPaint().setStrokeWidth(DEFAULT_LABEL_MARKER_THICKNESS); } - /** - * Should only be used in conjunction with calls to configure()... - */ - public SegmentFormatter() {} - public SegmentFormatter(Integer fillColor) { if(fillColor != null) { getFillPaint().setColor(fillColor); @@ -85,6 +85,10 @@ public SegmentFormatter(Integer fillColor) { } } + public SegmentFormatter(Context context, int xmlCfgId) { + configure(context, xmlCfgId); + } + public SegmentFormatter(Integer fillColor, Integer borderColor) { this(fillColor); getInnerEdgePaint().setColor(borderColor); @@ -95,8 +99,6 @@ public SegmentFormatter(Integer fillColor, Integer borderColor) { public SegmentFormatter(Integer fillColor, Integer outerEdgeColor, Integer innerEdgeColor, Integer radialEdgeColor) { this(fillColor); - - if(getOuterEdgePaint() != null) { getOuterEdgePaint().setColor(outerEdgeColor); } else { @@ -125,7 +127,7 @@ public Class getRendererClass() { } @Override - public SeriesRenderer getRendererInstance(PieChart plot) { + public SeriesRenderer doGetRendererInstance(PieChart plot) { return new PieRenderer(plot); } @@ -176,4 +178,46 @@ public Paint getLabelMarkerPaint() { public void setLabelMarkerPaint(Paint labelMarkerPaint) { this.labelMarkerPaint = labelMarkerPaint; } + + public float getOffset() { + return offset; + } + + /** + * Set an offset relative to the center of the pie chart at which this segment should be drawn; + * generally used to highlight specific segments. + * @param offset + */ + public void setOffset(float offset) { + this.offset = offset; + } + + public float getRadialInset() { + return radialInset; + } + + /** + * Set an inset in degrees for the radial edges of this segment. + * generally used to highlight specific segments. + * @param radialInset + */ + public void setRadialInset(float radialInset) { + this.radialInset = radialInset; + } + + public float getInnerInset() { + return innerInset; + } + + public void setInnerInset(float innerInset) { + this.innerInset = innerInset; + } + + public float getOuterInset() { + return outerInset; + } + + public void setOuterInset(float outerInset) { + this.outerInset = outerInset; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/pie/SegmentRegistry.java b/androidplot-core/src/main/java/com/androidplot/pie/SegmentRegistry.java new file mode 100644 index 00000000..1cbf905f --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/pie/SegmentRegistry.java @@ -0,0 +1,14 @@ +package com.androidplot.pie; + +import com.androidplot.*; + +/** + * SeriesRegistry implementation to be used in a {@link PieChart}. + */ +public class SegmentRegistry extends SeriesRegistry { + + @Override + protected SegmentBundle newSeriesBundle(Segment series, SegmentFormatter formatter) { + return new SegmentBundle(series, formatter); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/AnchorPosition.java b/androidplot-core/src/main/java/com/androidplot/ui/Anchor.java similarity index 94% rename from androidplot-core/src/main/java/com/androidplot/ui/AnchorPosition.java rename to androidplot-core/src/main/java/com/androidplot/ui/Anchor.java index 1ba69f6b..e5986556 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/AnchorPosition.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Anchor.java @@ -1,33 +1,33 @@ -/* - * 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; - -/** - * Enumeration of possible anchor positions that a {@link com.androidplot.ui.widget.Widget} can use. There are a total - * 8 possible anchor positions representing each corner of the Widget and the point exactly between each corner. - */ -public enum AnchorPosition { - TOP_MIDDLE, - LEFT_TOP, // default - LEFT_MIDDLE, - LEFT_BOTTOM, - RIGHT_TOP, - RIGHT_MIDDLE, - RIGHT_BOTTOM, - BOTTOM_MIDDLE, - CENTER -} +/* + * 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; + +/** + * Enumeration of possible anchor positions that a {@link com.androidplot.ui.widget.Widget} can use. There are a total + * 8 possible anchor positions representing each corner of the Widget and the point exactly between each corner. + */ +public enum Anchor { + TOP_MIDDLE, + LEFT_TOP, // default + LEFT_MIDDLE, + LEFT_BOTTOM, + RIGHT_TOP, + RIGHT_MIDDLE, + RIGHT_BOTTOM, + BOTTOM_MIDDLE, + CENTER +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/BoxModel.java b/androidplot-core/src/main/java/com/androidplot/ui/BoxModel.java index 298f565e..51ab2292 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/BoxModel.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/BoxModel.java @@ -74,7 +74,7 @@ public RectF getMarginatedRect(RectF boundsRect) { */ public RectF getPaddedRect(RectF marginRect) { return new RectF(marginRect.left + getPaddingLeft(), - marginRect.top+getPaddingTop(), + marginRect.top + getPaddingTop(), marginRect.right - getPaddingRight(), marginRect.bottom - getPaddingBottom()); } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/BoxModelable.java b/androidplot-core/src/main/java/com/androidplot/ui/BoxModelable.java index ffdf6b9e..9c953b45 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/BoxModelable.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/BoxModelable.java @@ -19,9 +19,11 @@ import android.graphics.RectF; /** - * Encapsulates the functionality of a BoxModel. - * See http://www.w3.org/TR/CSS21/box.html for a good explanation of how - * the box model works. + * Defines the properties of a BoxModel as used + * by Androidplot. Essentially, the BoxModel composes three nested (but not necessarily concentric) rectangles: + * * The bounding box, which is the outer-most box. + * * The marginated box, which is calculated by applying the margin insets to the bounding box. + * * The padded box, which is calculated by applying the padding insets to the marginated box. */ public interface BoxModelable { /** 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 f6db15e8..ff973b9d 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java @@ -35,21 +35,9 @@ */ public class DynamicTableModel extends TableModel { - //private float cellWidth; - //private float cellHeight; - //private TableSizingMethod rowSizingMethod; - //private TableSizingMethod columnSizingMethod; - - private int numRows; private int numColumns; - private Float cellWidth; - private Float cellHeight; - - private CellSizingMethod rowSizingMethod; - private CellSizingMethod columnSizingMethod; - /** * Convenience method. Sets order to ROW_MAJOR. * @param numColumns @@ -63,30 +51,9 @@ public DynamicTableModel(int numColumns, int numRows) { public DynamicTableModel(int numColumns, int numRows, TableOrder order) { super(order); this.numColumns = numColumns; - //this.cellWidth = cellWidth; - //this.rowSizingMethod = rowSizingMethod; this.numRows = numRows; - //this.cellHeight = cellHeight; - //this.columnSizingMethod = columnSizingMethod; - //this.order = order; } - /*public DynamicTableModel(Number colVal, CellSizingMethod colSzMethod, Number rowVal, CellSizingMethod rowSzMethod, TableOrder order) { - if(colVal == null || rowVal == null) { - throw new NullPointerException(); - } - columnSizingMethod = colSzMethod; - switch(columnSizingMethod) { - case FILL: - numColumns = colVal.intValue(); - break; - case FIXED: - cellWidth = colVal.floatValue(); - break; - } - rowSzMethod = rowSzMethod; - }*/ - @Override public TableModelIterator getIterator(RectF tableRect, int totalElements) { return new TableModelIterator(this, tableRect, totalElements); @@ -103,9 +70,7 @@ public RectF getCellRect(RectF tableRect, int numElements) { RectF cellRect = new RectF(); cellRect.left = tableRect.left; cellRect.top = tableRect.top; - //cellRect.bottom = getElementHeightPix(tableRect); cellRect.bottom = tableRect.top + calculateCellSize(tableRect, TableModel.Axis.ROW, numElements); - //cellRect.right = getElementWidthPix(tableRect); cellRect.right = tableRect.left + calculateCellSize(tableRect, TableModel.Axis.COLUMN, numElements); return cellRect; } @@ -120,24 +85,19 @@ public RectF getCellRect(RectF tableRect, int numElements) { private float calculateCellSize(RectF tableRect, Axis axis, int numElementsInTable) { - //float elementSizeInPix = 0; int axisElements = 0; float axisSizePix = 0; switch (axis) { case ROW: - //elementSizeInPix = cellHeight; axisElements = numRows; axisSizePix = tableRect.height(); break; case COLUMN: - //elementSizeInPix = cellWidth; axisElements = numColumns; axisSizePix = tableRect.width(); break; } - //if (elementSizeInPix != 0) { - // return elementSizeInPix; if(axisElements != 0) { return axisSizePix / axisElements; } else { @@ -163,22 +123,6 @@ public void setNumColumns(int numColumns) { this.numColumns = numColumns; } -/* public void setCellWidth(Float cellWidth) { - this.cellWidth = cellWidth; - } - - public Float getCellWidth() { - return cellWidth; - } - - public Float getCellHeight() { - return cellHeight; - } - - public void setCellHeight(Float cellHeight) { - this.cellHeight = cellHeight; - }*/ - private class TableModelIterator implements Iterator { private boolean isOk = true; int lastColumn = 0; // most recent column iterated @@ -205,17 +149,15 @@ 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) { - //order = TableOrder.ROW_MAJOR; 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; calculatedColumns = totalElements; } else { - //order = dynamicTableModel.getOrder(); calculatedRows = dynamicTableModel.getNumRows(); calculatedColumns = dynamicTableModel.getNumColumns(); } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/FixedTableModel.java b/androidplot-core/src/main/java/com/androidplot/ui/FixedTableModel.java index a10ca2a3..5bce9e0b 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/FixedTableModel.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/FixedTableModel.java @@ -23,7 +23,8 @@ public class FixedTableModel extends TableModel { private float cellWidth; private float cellHeight; - protected FixedTableModel(float cellWidth, float cellHeight, TableOrder order) { + + public FixedTableModel(float cellWidth, float cellHeight, TableOrder order) { super(order); setCellWidth(cellWidth); setCellHeight(cellHeight); 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 51b1a94a..d920ab36 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java @@ -18,7 +18,7 @@ import android.content.Context; import com.androidplot.Plot; -import com.androidplot.util.Configurator; +import com.halfhp.fig.*; /** * Base class of all Formatters. Encapsulates visual elements of a series; line style, color etc. @@ -39,23 +39,54 @@ */ public abstract class Formatter { - public Formatter configure(Context ctx, int xmlCfgId) { - Configurator.configure(ctx, this, xmlCfgId); - return this; + private boolean isLegendIconEnabled = true; + + public Formatter() {} + + public Formatter(Context ctx, int xmlCfgId) { + configure(ctx, xmlCfgId); } + public void configure(Context ctx, int xmlCfgId) { + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } + } + /** + * + * @param plot + * @param + * @return @return An instance of SeriesRenderer constructed with the specified plot. + */ + public T getRendererInstance(PlotType plot) { + return (T) doGetRendererInstance(plot); + } /** * - * @return The Class of SeriesRenderer that should be used. + * @return The Class of SeriesRenderer that should be used when rendering series associated + * with instances of this formatter. */ public abstract Class getRendererClass(); /** * - * @return An instance of SeriesRenderer that took plot as an argument to its constructor. + * @return An instance of SeriesRenderer constructed with the specified plot. */ - public abstract SeriesRenderer getRendererInstance(PlotType plot); + protected abstract SeriesRenderer doGetRendererInstance(PlotType plot); + + public boolean isLegendIconEnabled() { + return isLegendIconEnabled; + } + /** + * Sets whether or not a legend icon should be drawn for the series associated with this formatter. + * @param legendIconEnabled + */ + public void setLegendIconEnabled(boolean legendIconEnabled) { + this.isLegendIconEnabled = legendIconEnabled; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/XPositionMetric.java b/androidplot-core/src/main/java/com/androidplot/ui/HorizontalPosition.java similarity index 84% rename from androidplot-core/src/main/java/com/androidplot/ui/XPositionMetric.java rename to androidplot-core/src/main/java/com/androidplot/ui/HorizontalPosition.java index 3f93bd0a..5d8b1255 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/XPositionMetric.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/HorizontalPosition.java @@ -16,25 +16,18 @@ package com.androidplot.ui; +public class HorizontalPosition extends PositionMetric { -import com.androidplot.ui.PositionMetric; -import com.androidplot.ui.XLayoutStyle; - -public class XPositionMetric extends PositionMetric { - - //private XLayoutStyle layoutType; - - public XPositionMetric(float value, XLayoutStyle layoutStyle) { + public HorizontalPosition(float value, HorizontalPositioning layoutStyle) { super(value, layoutStyle); validatePair(value, layoutStyle); - //this.layoutStyle = layoutStyle; } /** * Throws IllegalArgumentException if there is a problem. * @param value */ - protected void validatePair(float value, XLayoutStyle layoutStyle) { + protected void validatePair(float value, HorizontalPositioning layoutStyle) { switch(layoutStyle) { case ABSOLUTE_FROM_LEFT: case ABSOLUTE_FROM_RIGHT: @@ -69,7 +62,7 @@ public float getPixelValue(float size) { } @Override - public void setLayoutType(XLayoutStyle layoutType) { + public void setLayoutType(HorizontalPositioning layoutType) { super.setLayoutType(layoutType); } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/XLayoutStyle.java b/androidplot-core/src/main/java/com/androidplot/ui/HorizontalPositioning.java similarity index 95% rename from androidplot-core/src/main/java/com/androidplot/ui/XLayoutStyle.java rename to androidplot-core/src/main/java/com/androidplot/ui/HorizontalPositioning.java index b8f9a610..b7125ea3 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/XLayoutStyle.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/HorizontalPositioning.java @@ -16,7 +16,7 @@ package com.androidplot.ui; -public enum XLayoutStyle { +public enum HorizontalPositioning { ABSOLUTE_FROM_LEFT, ABSOLUTE_FROM_RIGHT, ABSOLUTE_FROM_CENTER, diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Insets.java b/androidplot-core/src/main/java/com/androidplot/ui/Insets.java new file mode 100644 index 00000000..85325bfe --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/ui/Insets.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 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; + +/** + * A set of insets for a rect space. + */ +public class Insets { + + private float top; + private float bottom; + private float left; + private float right; + + public Insets() {} + + public Insets(float top, float bottom, float left, float right) { + this.top = top; + this.bottom = bottom; + this.left = left; + this.right = right; + } + + public float getTop() { + return top; + } + + public void setTop(float top) { + this.top = top; + } + + public float getBottom() { + return bottom; + } + + public void setBottom(float bottom) { + this.bottom = bottom; + } + + public float getLeft() { + return left; + } + + public void setLeft(float left) { + this.left = left; + } + + public float getRight() { + return right; + } + + public void setRight(float right) { + this.right = right; + } +} 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 c5d9e743..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,15 +16,20 @@ 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.ZLinkedList; +import com.androidplot.util.LinkedLayerList; -public class LayoutManager extends ZLinkedList +public class LayoutManager extends LinkedLayerList implements View.OnTouchListener, Resizable { private boolean drawAnchorsEnabled = false; private Paint anchorPaint; @@ -38,17 +43,15 @@ public class LayoutManager extends ZLinkedList private Paint paddingPaint; private DisplayDimensions displayDims = new DisplayDimensions(); - // cache of widget rects - //private HashMap widgetRects; - { - //widgetRects = new HashMap(); anchorPaint = new Paint(); anchorPaint.setStyle(Paint.Style.FILL); anchorPaint.setColor(Color.GREEN); outlinePaint = new Paint(); outlinePaint.setColor(Color.GREEN); outlinePaint.setStyle(Paint.Style.STROKE); + outlinePaint.setAntiAlias(true); + outlinePaint.setStrokeWidth(2); marginPaint = new Paint(); marginPaint.setColor(Color.YELLOW); marginPaint.setStyle(Paint.Style.FILL); @@ -79,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); } @@ -87,19 +90,15 @@ 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); - //RectF widgetRect = new RectF(coords.x, coords.y, coords.x + elementWidth, coords.y + elementHeight); - //DisplayDimensions dims = widgetRects.get(widget); DisplayDimensions dims = widget.getWidgetDimensions(); - //RectF widgetRect = widgetRects.get(widget); if (drawOutlineShadowsEnabled) { canvas.drawRect(dims.canvasRect, outlineShadowPaint); @@ -109,14 +108,9 @@ public void draw(Canvas canvas) throws PlotRenderException { // so this is necessary to avoid clipping borders. I suspect that its a floating point // jitter issue. if (widget.isClippingEnabled()) { - //RectF clipRect = new RectF(l-1, t-1, r + 1, b + 1); - //canvas.clipRect(clipRect, Region.Op.REPLACE); canvas.clipRect(dims.canvasRect, Region.Op.INTERSECT); } - widget.draw(canvas, dims.canvasRect); - - //RectF marginatedWidgetRect = widget.getMarginatedRect(dims.canvasRect); - //RectF paddedWidgetRect = widget.getPaddedRect(marginatedWidgetRect); + widget.draw(canvas); if (drawMarginsEnabled) { drawSpacing(canvas, dims.canvasRect, dims.marginatedRect, getMarginPaint()); @@ -135,23 +129,19 @@ public void draw(Canvas canvas) throws PlotRenderException { if (drawOutlinesEnabled) { - outlinePaint.setAntiAlias(true); canvas.drawRect(dims.canvasRect, outlinePaint); } } finally { - //canvas.restoreToCount(canvasState); // restore clipping etc. canvas.restore(); } } } - private void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) { - //int saved = canvas.save(Canvas.ALL_SAVE_FLAG); + 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); - //canvas.restoreToCount(saved); } finally { canvas.restore(); } @@ -253,7 +243,6 @@ public boolean onTouch(View v, MotionEvent event) { * to call this method as it is a relatively slow operation. */ public void refreshLayout() { - //widgetRects.clear(); for (Widget widget : elements()) { widget.layout(displayDims); } 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 c96c98a3..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,38 +16,40 @@ package com.androidplot.ui; +import androidx.annotation.NonNull; + public class PositionMetrics implements Comparable { - private XPositionMetric xPositionMetric; - private YPositionMetric yPositionMetric; - private AnchorPosition anchor; + private HorizontalPosition horizontalPosition; + private VerticalPosition verticalPosition; + private Anchor anchor; private float layerDepth; - public PositionMetrics(float x, XLayoutStyle xLayoutStyle, float y, YLayoutStyle yLayoutStyle, AnchorPosition anchor) { - setXPositionMetric(new XPositionMetric(x, xLayoutStyle)); - setYPositionMetric(new YPositionMetric(y, yLayoutStyle)); + public PositionMetrics(float x, HorizontalPositioning horizontalPositioning, float y, VerticalPositioning verticalPositioning, Anchor anchor) { + setXPositionMetric(new HorizontalPosition(x, horizontalPositioning)); + setYPositionMetric(new VerticalPosition(y, verticalPositioning)); setAnchor(anchor); } - public YPositionMetric getYPositionMetric() { - return yPositionMetric; + public VerticalPosition getYPositionMetric() { + return verticalPosition; } - public void setYPositionMetric(YPositionMetric yPositionMetric) { - this.yPositionMetric = yPositionMetric; + public void setYPositionMetric(VerticalPosition verticalPosition) { + this.verticalPosition = verticalPosition; } - public AnchorPosition getAnchor() { + public Anchor getAnchor() { return anchor; } - public void setAnchor(AnchorPosition anchor) { + public void setAnchor(Anchor anchor) { this.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) { @@ -57,11 +59,11 @@ public int compareTo(PositionMetrics o) { } } - public XPositionMetric getXPositionMetric() { - return xPositionMetric; + public HorizontalPosition getXPositionMetric() { + return horizontalPosition; } - public void setXPositionMetric(XPositionMetric xPositionMetric) { - this.xPositionMetric = xPositionMetric; + public void setXPositionMetric(HorizontalPosition horizontalPosition) { + this.horizontalPosition = horizontalPosition; } } 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 ff570475..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 { @@ -40,20 +40,20 @@ public ArrayList> getElements() { * An element on the render stack. */ public class StackElement { - private SeriesAndFormatter seriesAndFormatter; + private SeriesBundle seriesBundle; private boolean isEnabled = true; - public StackElement(SeriesAndFormatter seriesAndFormatter) { - set(seriesAndFormatter); + public StackElement(SeriesBundle seriesBundle) { + set(seriesBundle); } - public SeriesAndFormatter get() { - return seriesAndFormatter; + public SeriesBundle get() { + return seriesBundle; } - public void set(SeriesAndFormatter seriesAndFormatter) { - this.seriesAndFormatter = seriesAndFormatter; + public void set(SeriesBundle seriesBundle) { + this.seriesBundle = seriesBundle; } public boolean isEnabled() { @@ -72,7 +72,7 @@ public void setEnabled(boolean isEnabled) { public RenderStack(Plot plot) { this.plot = plot; - elements = new ArrayList<>(plot.getSeriesRegistry().size()); + elements = new ArrayList<>(plot.getRegistry().size()); } /** @@ -80,12 +80,10 @@ 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.getSeriesRegistry(); - for(SeriesAndFormatter thisPair: pairList) { + List> pairList + = plot.getRegistry().getSeriesAndFormatterList(); + for(SeriesBundle thisPair: pairList) { getElements().add(new StackElement<>(thisPair)); } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Resizable.java b/androidplot-core/src/main/java/com/androidplot/ui/Resizable.java index a3f7d5ed..fea8ad15 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Resizable.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Resizable.java @@ -34,5 +34,5 @@ public interface Resizable { * Resizables in likely undesired ways. * @param dims */ - public void layout(final DisplayDimensions dims); + void layout(final DisplayDimensions dims); } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/SeriesAndFormatter.java b/androidplot-core/src/main/java/com/androidplot/ui/SeriesBundle.java similarity index 69% rename from androidplot-core/src/main/java/com/androidplot/ui/SeriesAndFormatter.java rename to androidplot-core/src/main/java/com/androidplot/ui/SeriesBundle.java index 94c3c8c4..6a5e8481 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SeriesAndFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SeriesBundle.java @@ -19,14 +19,15 @@ import com.androidplot.Series; /** - * Defines an association between a Series and a Formatter. + * Defines a relationship between a Series instance and other elements needed to unique render that instance + * such as a Formatter etc. */ -public class SeriesAndFormatter { +public class SeriesBundle { private final SeriesType series; private final FormatterType formatter; - public SeriesAndFormatter(SeriesType series, FormatterType formatter) { + public SeriesBundle(SeriesType series, FormatterType formatter) { this.series = series; this.formatter = formatter; } @@ -38,4 +39,8 @@ public SeriesType getSeries() { public FormatterType getFormatter() { return formatter; } + + public boolean rendersWith(SeriesRenderer renderer) { + return getFormatter().getRendererClass() == renderer.getClass(); + } } 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 d4105d8d..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, SeriesAndFormatter sfPair, RenderStack stack) throws PlotRenderException { + public void render(Canvas canvas, RectF plotArea, SeriesBundle sfPair, RenderStack stack) { onRender(canvas, plotArea, sfPair.getSeries(), sfPair.getFormatter(), stack); } @@ -67,10 +65,9 @@ public void render(Canvas canvas, RectF plotArea, SeriesAndFormatter> getSeriesList() { - List> results = new ArrayList<>(); - ArrayList sfList = getPlot().getSeriesRegistry(); - - for(SeriesAndFormatter thisPair : sfList) { - if(thisPair.getFormatter().getRendererClass() == getClass()) { + public List> getSeriesAndFormatterList() { + List> results = new ArrayList<>(); + List sfList = getPlot().getRegistry().getSeriesAndFormatterList(); + for(SeriesBundle thisPair : sfList) { + if(thisPair.rendersWith(this)) { results.add(thisPair); } } return results; } + + /** + * + * @return + * @since 0.9.7 + */ + public List getSeriesList() { + List results = new ArrayList<>(); + List sfList = getPlot().getRegistry().getSeriesAndFormatterList(); + + for(SeriesBundle thisPair : sfList) { + if(thisPair.rendersWith(this)) { + results.add(thisPair.getSeries()); + } + } + return results; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Size.java b/androidplot-core/src/main/java/com/androidplot/ui/Size.java index c75febcf..a5461962 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Size.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Size.java @@ -25,7 +25,7 @@ public class Size { // convenience value; sets size to 100% width and height of the widget container. - public static Size FILL = new Size(0, SizeLayoutType.FILL, 0, SizeLayoutType.FILL); + public static Size FILL = new Size(0, SizeMode.FILL, 0, SizeMode.FILL); private SizeMetric height; private SizeMetric width; @@ -37,7 +37,7 @@ public class Size { * @param width Width value used algorithm to calculate the width of the associated widget(s). * @param widthLayoutType Algorithm used to calculate the width of the associated widget(s). */ - public Size(float height, SizeLayoutType heightLayoutType, float width, SizeLayoutType widthLayoutType) { + public Size(float height, SizeMode heightLayoutType, float width, SizeMode widthLayoutType) { this.height = new SizeMetric(height, heightLayoutType); this.width = new SizeMetric(width, widthLayoutType); } @@ -78,16 +78,6 @@ public RectF getRectF(RectF canvasRect) { height.getPixelValue(canvasRect.height())); } - /** - * Same as getRectF but with edges rounded to the nearest full pixel. - * @param canvasRect - * @return - */ - public RectF getRoundedRect(RectF canvasRect) { - return PixelUtils.nearestPixRect(0, 0, width.getPixelValue(canvasRect.width()), - height.getPixelValue(canvasRect.height())); - } - public void setWidth(SizeMetric width) { this.width = width; } 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 98ff5a7d..d7f3e58b 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java @@ -19,16 +19,16 @@ /** * Encapsulates a sizing algorithm and an associated value. * - * The available algorithms list are stored in the {@link SizeLayoutType} enumeration. + * The available algorithms list are stored in the {@link SizeMode} enumeration. * */ -public class SizeMetric extends LayoutMetric { +public class SizeMetric extends LayoutMetric { - public SizeMetric(float value, SizeLayoutType layoutType) { + public SizeMetric(float value, SizeMode layoutType) { super(value, layoutType); } - protected void validatePair(float value, SizeLayoutType layoutType) { + protected void validatePair(float value, SizeMode layoutType) { switch(layoutType) { case RELATIVE: if(value < 0 || value > 1) { @@ -43,7 +43,6 @@ protected void validatePair(float value, SizeLayoutType layoutType) { @Override public float getPixelValue(float size) { - //switch(layoutType) switch(getLayoutType()) { case ABSOLUTE: return getValue(); @@ -57,7 +56,7 @@ public float getPixelValue(float size) { } @Override - public void setLayoutType(SizeLayoutType layoutType) { + public void setLayoutType(SizeMode layoutType) { super.setLayoutType(layoutType); } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/SizeLayoutType.java b/androidplot-core/src/main/java/com/androidplot/ui/SizeMode.java similarity index 87% rename from androidplot-core/src/main/java/com/androidplot/ui/SizeLayoutType.java rename to androidplot-core/src/main/java/com/androidplot/ui/SizeMode.java index 14498711..0d158133 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SizeLayoutType.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SizeMode.java @@ -1,34 +1,34 @@ -/* - * 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; - -/** - * SizeLayoutType is an enumeration of algorithms available for calculating an arbitrary dimension of a widget. - * Each algorithm also takes a single value called "val" in this doc. - * ABSOLUTE - Val is treated as absolute. If val is 5 then the size of the widget along the associated axis is 5 pixels. - * - * RELATIVE - Val represents the percentage of the display that the widget should fill along the associated axis. For example, - * if the total size of the owning plot is 120 pixels and val is set to 50 then the size of the widget along the associated axis - * is 60; 50% of 120 = 60. - * - * FILL - Widget completely fills along the associated axis, minus the input size value - */ -public enum SizeLayoutType { - ABSOLUTE, - RELATIVE, - FILL +/* + * 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; + +/** + * Algorithms available for calculating an arbitrary dimension of a widget. + * Each algorithm also takes a single value called "val" in this doc. + * ABSOLUTE - Val is treated as absolute. If val is 5 then the size of the widget along the associated axis is 5 pixels. + * + * RELATIVE - Val represents the percentage of the display that the widget should fill along the associated axis. For example, + * if the total size of the owning plot is 120 pixels and val is set to 50 then the size of the widget along the associated axis + * is 60; 50% of 120 = 60. + * + * FILL - Widget completely fills along the associated axis, minus the input size value + */ +public enum SizeMode { + ABSOLUTE, + RELATIVE, + FILL } \ No newline at end of file diff --git a/androidplot-core/src/main/java/com/androidplot/ui/TextOrientationType.java b/androidplot-core/src/main/java/com/androidplot/ui/TextOrientation.java similarity index 92% rename from androidplot-core/src/main/java/com/androidplot/ui/TextOrientationType.java rename to androidplot-core/src/main/java/com/androidplot/ui/TextOrientation.java index 92fd5d18..219e64e4 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/TextOrientationType.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/TextOrientation.java @@ -1,23 +1,23 @@ -/* - * 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; - -public enum TextOrientationType { - HORIZONTAL, - VERTICAL_ASCENDING, - VERTICAL_DESCENDING -} +/* + * 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; + +public enum TextOrientation { + HORIZONTAL, + VERTICAL_ASCENDING, + VERTICAL_DESCENDING +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/YPositionMetric.java b/androidplot-core/src/main/java/com/androidplot/ui/VerticalPosition.java similarity index 67% rename from androidplot-core/src/main/java/com/androidplot/ui/YPositionMetric.java rename to androidplot-core/src/main/java/com/androidplot/ui/VerticalPosition.java index e0994eca..f8c8b78f 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/YPositionMetric.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/VerticalPosition.java @@ -16,55 +16,17 @@ package com.androidplot.ui; -import com.androidplot.ui.PositionMetric; -import com.androidplot.ui.YLayoutStyle; +public class VerticalPosition extends PositionMetric { -public class YPositionMetric extends PositionMetric { - /* - public enum YLayoutStyle { - ABSOLUTE_FROM_TOP, - ABSOLUTE_FROM_BOTTOM, - ABSOLUTE_FROM_CENTER, - RELATIVE_TO_TOP, - RELATIVE_TO_BOTTOM, - RELATIVE_TO_CENTER - } - */ - - //private YLayoutStyle layoutType; - - public YPositionMetric(float value, YLayoutStyle layoutStyle) { + public VerticalPosition(float value, VerticalPositioning layoutStyle) { super(value, layoutStyle); - //this.layoutStyle = layoutStyle; - - - } - - /* - @Override - public void set(float value, YLayoutStyle layoutType) { - validatePair(value, layoutType); - super.set(value, layoutType); - } - - @Override - public void setLayoutType(YLayoutStyle layoutType) { - validatePair(getValue(), layoutType); - super.setLayoutType(layoutType); - } - - @Override - public void setValue(float value) { - validatePair(value, getLayoutType()); - super.setValue(value); } - */ /** * Throws IllegalArgumentException if there is a problem. * @param value */ - protected void validatePair(float value, YLayoutStyle layoutStyle) { + protected void validatePair(float value, VerticalPositioning layoutStyle) { switch(layoutStyle) { case ABSOLUTE_FROM_TOP: case ABSOLUTE_FROM_BOTTOM: @@ -99,7 +61,7 @@ public float getPixelValue(float size) { } @Override - public void setLayoutType(YLayoutStyle layoutType) { + public void setLayoutType(VerticalPositioning layoutType) { super.setLayoutType(layoutType); } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/YLayoutStyle.java b/androidplot-core/src/main/java/com/androidplot/ui/VerticalPositioning.java similarity index 95% rename from androidplot-core/src/main/java/com/androidplot/ui/YLayoutStyle.java rename to androidplot-core/src/main/java/com/androidplot/ui/VerticalPositioning.java index 2df7be1a..630b8772 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/YLayoutStyle.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/VerticalPositioning.java @@ -16,7 +16,7 @@ package com.androidplot.ui; -public enum YLayoutStyle { +public enum VerticalPositioning { ABSOLUTE_FROM_TOP, ABSOLUTE_FROM_BOTTOM, ABSOLUTE_FROM_CENTER, 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 8e86f8cf..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 TextOrientationType orientation; - + private TextOrientation orientation; private boolean autoPackEnabled = true; { @@ -35,19 +31,20 @@ public class TextLabelWidget extends Widget { labelPaint.setColor(Color.WHITE); labelPaint.setAntiAlias(true); labelPaint.setTextAlign(Paint.Align.CENTER); + setClippingEnabled(false); } public TextLabelWidget(LayoutManager layoutManager, Size size) { - this(layoutManager, size, TextOrientationType.HORIZONTAL); + this(layoutManager, size, TextOrientation.HORIZONTAL); } - public TextLabelWidget(LayoutManager layoutManager, String title, Size size, TextOrientationType orientation) { + public TextLabelWidget(LayoutManager layoutManager, String title, Size size, TextOrientation orientation) { this(layoutManager, size, orientation); setText(title); } - public TextLabelWidget(LayoutManager layoutManager, Size size, TextOrientationType orientation) { - super(layoutManager, new Size(0, SizeLayoutType.ABSOLUTE, 0, SizeLayoutType.ABSOLUTE)); + public TextLabelWidget(LayoutManager layoutManager, Size size, TextOrientation orientation) { + super(layoutManager, new Size(0, SizeMode.ABSOLUTE, 0, SizeMode.ABSOLUTE)); setSize(size); this.orientation = orientation; } @@ -66,25 +63,21 @@ 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) { case HORIZONTAL: - setSize(new Size(size.height(), SizeLayoutType.ABSOLUTE, size.width()+2, SizeLayoutType.ABSOLUTE)); + setSize(new Size(size.height(), SizeMode.ABSOLUTE, size.width()+2, SizeMode.ABSOLUTE)); break; case VERTICAL_ASCENDING: case VERTICAL_DESCENDING: - setSize(new Size(size.width(), SizeLayoutType.ABSOLUTE, size.height()+2, SizeLayoutType.ABSOLUTE)); + setSize(new Size(size.width(), SizeMode.ABSOLUTE, size.height()+2, SizeMode.ABSOLUTE)); break; } refreshLayout(); @@ -102,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, - AnchorPosition.CENTER); - - // BEGIN ROTATION CALCULATION - //int canvasState = canvas.save(Canvas.ALL_SAVE_FLAG); + Anchor.CENTER); try { - canvas.save(Canvas.ALL_SAVE_FLAG); + canvas.save(); canvas.translate(start.x, start.y); switch (orientation) { case HORIZONTAL: @@ -128,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() { @@ -142,18 +129,18 @@ public Paint getLabelPaint() { public void setLabelPaint(Paint labelPaint) { this.labelPaint = labelPaint; - // when paint changes, packing params change too so check + // when paint changes, packing params change too so run // to see if we need to resize: if(autoPackEnabled) { pack(); } } - public TextOrientationType getOrientation() { + public TextOrientation getOrientation() { return orientation; } - public void setOrientation(TextOrientationType orientation) { + public void setOrientation(TextOrientation orientation) { this.orientation = orientation; if(autoPackEnabled) { pack(); @@ -172,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 bda3f47c..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,11 +17,13 @@ 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.XLayoutStyle; -import com.androidplot.ui.YLayoutStyle; +import com.androidplot.ui.HorizontalPositioning; +import com.androidplot.ui.VerticalPositioning; import com.androidplot.util.PixelUtils; /** @@ -32,7 +34,7 @@ public abstract class Widget implements BoxModelable, Resizable { private Paint borderPaint; private Paint backgroundPaint; - private boolean clippingEnabled = true; + private boolean clippingEnabled = false; private BoxModel boxModel = new BoxModel(); private Size size; private DisplayDimensions plotDimensions = new DisplayDimensions(); @@ -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)); } @@ -56,37 +68,38 @@ public DisplayDimensions getWidgetDimensions() { return widgetDimensions; } - public AnchorPosition getAnchor() { + public Anchor getAnchor() { return getPositionMetrics().getAnchor(); } - public void setAnchor(AnchorPosition anchor) { + public void setAnchor(Anchor anchor) { getPositionMetrics().setAnchor(anchor); } /** - * Same as {@link #position(float, com.androidplot.ui.XLayoutStyle, float, com.androidplot.ui.YLayoutStyle, com.androidplot.ui.AnchorPosition)} + * Same as {@link #position(float, HorizontalPositioning, float, VerticalPositioning, Anchor)} * but with the anchor parameter defaulted to the upper left corner. + * * @param x - * @param xLayoutStyle + * @param horizontalPositioning * @param y - * @param yLayoutStyle + * @param verticalPositioning */ - public void position(float x, XLayoutStyle xLayoutStyle, float y, YLayoutStyle yLayoutStyle) { - position(x, xLayoutStyle, y, yLayoutStyle, AnchorPosition.LEFT_TOP); + public void position(float x, HorizontalPositioning horizontalPositioning, float y, VerticalPositioning verticalPositioning) { + position(x, horizontalPositioning, y, verticalPositioning, Anchor.LEFT_TOP); } /** - * @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1. - * @param xLayoutStyle 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 yLayoutStyle LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate. - * @param anchor The point of reference used by this positioning call. + * @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. */ - public void position(float x, XLayoutStyle xLayoutStyle, float y, - YLayoutStyle yLayoutStyle, AnchorPosition anchor) { - setPositionMetrics(new PositionMetrics(x, xLayoutStyle, y, yLayoutStyle, anchor)); + public void position(float x, HorizontalPositioning horizontalPositioning, float y, + VerticalPositioning verticalPositioning, Anchor anchor) { + setPositionMetrics(new PositionMetrics(x, horizontalPositioning, y, verticalPositioning, anchor)); layoutManager.addToTop(this); } @@ -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); } @@ -130,7 +142,7 @@ public void setWidth(float width) { size.getWidth().setValue(width); } - public void setWidth(float width, SizeLayoutType layoutType) { + public void setWidth(float width, SizeMode layoutType) { size.getWidth().set(width, layoutType); } @@ -138,7 +150,7 @@ public void setHeight(float height) { size.getHeight().setValue(height); } - public void setHeight(float height, SizeLayoutType layoutType) { + public void setHeight(float height, SizeMode layoutType) { size.getHeight().set(height, layoutType); } @@ -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,79 +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 getAnchorOffset(float width, float height, AnchorPosition anchorPosition) { - PointF point = new PointF(); - switch (anchorPosition) { - 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: " + anchorPosition); - } - return point; + 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; + } - public static PointF getAnchorCoordinates(RectF widgetRect, AnchorPosition anchorPosition) { - return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top), - getAnchorOffset(widgetRect.width(), widgetRect.height(), anchorPosition)); - } + public static PointF getAnchorCoordinates(RectF widgetRect, Anchor 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, AnchorPosition anchorPosition) { - return getAnchorCoordinates(new RectF(x, y, x+width, y+height), anchorPosition); + private void checkSize(@NonNull RectF widgetRect) { + if (lastWidgetRect == null || !lastWidgetRect.equals(widgetRect)) { + onResize(lastWidgetRect, widgetRect); } + lastWidgetRect = widgetRect; + } - public void draw(Canvas canvas, RectF widgetRect) throws PlotRenderException { - //outlineRect = widgetRect; + /** + * 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); } - - /* RectF marginatedRect = new RectF(outlineRect.left + marginLeft, - outlineRect.top + marginTop, - outlineRect.right - marginRight, - outlineRect.bottom - marginBottom);*/ - - /*RectF marginatedRect = boxModel.getMarginatedRect(widgetRect); - RectF paddedRect = boxModel.getPaddedRect(marginatedRect);*/ - 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); } @@ -373,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; @@ -414,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/APTrace.java b/androidplot-core/src/main/java/com/androidplot/util/APTrace.java new file mode 100644 index 00000000..88e6317a --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/util/APTrace.java @@ -0,0 +1,26 @@ +package com.androidplot.util; + +import android.os.*; + +/** + * Wraps {@link Trace} to provide API-safe methods as well as an easy target for runtime removal + * via obfuscation. + */ +public abstract class APTrace { + + public static void begin(final String sectionName) { + if(Build.VERSION.SDK_INT >= 18) { + Trace.beginSection(sectionName); + } else { + // TODO: alternate impl? + } + } + + public static void end() { + if(Build.VERSION.SDK_INT >= 18) { + Trace.endSection(); + } else { + // TODO: alternate impl? + } + } +} 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 318687e0..853ae25a 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java @@ -18,11 +18,13 @@ import android.content.res.TypedArray; import android.graphics.Paint; -import android.util.TypedValue; +import android.util.*; + import com.androidplot.ui.*; +import com.androidplot.ui.Size; import com.androidplot.ui.widget.Widget; -import com.androidplot.xy.XYStepMode; -import com.androidplot.xy.XYStepModel; +import com.androidplot.xy.StepMode; +import com.androidplot.xy.StepModel; /** * Methods for applying styleable attributes. @@ -30,6 +32,16 @@ */ public class AttrUtils { + private static final String TAG = AttrUtils.class.getName(); + + public static void configureInsets(TypedArray attrs, Insets insets, + int topAttr, int bottomAttr, int leftAttr, int rightAttr) { + insets.setTop(attrs.getDimension(topAttr, insets.getTop())); + insets.setBottom(attrs.getDimension(bottomAttr, insets.getBottom())); + insets.setLeft(attrs.getDimension(leftAttr, insets.getLeft())); + insets.setRight(attrs.getDimension(rightAttr, insets.getRight())); + } + /** * Configure a {@link Paint} instance used for drawing text from xml attrs. * @param attrs @@ -37,10 +49,44 @@ public class AttrUtils { * @param colorAttr * @param textSizeAttr */ - public static void configureTextPaint(TypedArray attrs, Paint paint, int colorAttr, int textSizeAttr) { + public static void configureTextPaint(TypedArray attrs, Paint paint, + int colorAttr, int textSizeAttr) { + configureTextPaint(attrs, paint, colorAttr, textSizeAttr, null); + } + + /** + * Configure a {@link Paint} instance used for drawing text from xml attrs. + * @param attrs + * @param paint + * @param colorAttr + * @param textSizeAttr + * @param alignAttr + */ + public static void configureTextPaint(TypedArray attrs, Paint paint, int colorAttr, + int textSizeAttr, Integer alignAttr) { if(attrs != null) { setColor(attrs, paint, colorAttr); setTextSize(attrs, paint, textSizeAttr); + + if(alignAttr != null && attrs.hasValue(alignAttr)) { + configureTextAlign(attrs, paint, alignAttr); + } + } + } + + /** + * Configure {@link Paint} text alignment from xml attrs. + * @param attrs + * @param paint + * @param alignAttr + */ + public static void configureTextAlign(TypedArray attrs, Paint paint, int alignAttr) { + if (attrs != null) { + //if(attrs.hasValue(alignAttr)) { + final Paint.Align alignment = Paint.Align.values() + [attrs.getInt(alignAttr, paint.getTextAlign().ordinal())]; + paint.setTextAlign(alignment); + //} } } @@ -58,11 +104,15 @@ public static void configureLinePaint(TypedArray attrs, Paint paint, int colorAt } } - private static void setColor(TypedArray attrs, Paint paint, int attrId) { - paint.setColor(attrs.getColor(attrId, paint.getColor())); + public static void setColor(TypedArray attrs, Paint paint, int attrId) { + if(paint == null) { + Log.w(TAG, "Attempt to configure null Paint property for attrId: " + attrId); + } else { + paint.setColor(attrs.getColor(attrId, paint.getColor())); + } } - private static void setTextSize(TypedArray attrs, Paint paint, int attrId) { + public static void setTextSize(TypedArray attrs, Paint paint, int attrId) { paint.setTextSize(attrs.getDimension(attrId, paint.getTextSize())); } @@ -115,14 +165,14 @@ public static void configureSize(TypedArray attrs, Size model, int heightSizeLay private static void configureSizeMetric(TypedArray attrs, SizeMetric model, int typeAttr, int valueAttr) { final float value = getIntFloatDimenValue(attrs, valueAttr, model.getValue()).floatValue(); - final SizeLayoutType sizeLayoutType = + final SizeMode sizeMode = getSizeLayoutType(attrs, typeAttr, model.getLayoutType()); - model.set(value, sizeLayoutType); + model.set(value, sizeMode); } - private static SizeLayoutType getSizeLayoutType(TypedArray attrs, int attr, SizeLayoutType defaultValue) { - return SizeLayoutType.values()[attrs.getInt(attr, defaultValue.ordinal())]; + private static SizeMode getSizeLayoutType(TypedArray attrs, int attr, SizeMode defaultValue) { + return SizeMode.values()[attrs.getInt(attr, defaultValue.ordinal())]; } public static void configureWidget(TypedArray attrs, Widget widget, int heightSizeLayoutTypeAttr, int heightAttr, @@ -138,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 @@ -151,7 +207,7 @@ public static void configureWidget(TypedArray attrs, Widget widget, int heightSi public static void configurePositionMetrics(TypedArray attrs, PositionMetrics metrics, int xLayoutStyleAttr, int xLayoutValueAttr, int yLayoutStyleAttr, int yLayoutValueAttr, int anchorPositionAttr) { - if(attrs != null) { + if(attrs != null && metrics != null) { metrics.getXPositionMetric().set( getIntFloatDimenValue(attrs, xLayoutValueAttr, metrics.getXPositionMetric().getValue()).floatValue(), getXLayoutStyle(attrs, xLayoutStyleAttr, metrics.getXPositionMetric().getLayoutType())); @@ -181,27 +237,31 @@ 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; } - private static XLayoutStyle getXLayoutStyle(TypedArray attrs, int attr, XLayoutStyle defaultValue) { - return XLayoutStyle.values()[attrs.getInt(attr, defaultValue.ordinal())]; + private static HorizontalPositioning getXLayoutStyle(TypedArray attrs, int attr, HorizontalPositioning defaultValue) { + return HorizontalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())]; + } + + private static VerticalPositioning getYLayoutStyle(TypedArray attrs, int attr, VerticalPositioning defaultValue) { + return VerticalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())]; } - private static YLayoutStyle getYLayoutStyle(TypedArray attrs, int attr, YLayoutStyle defaultValue) { - return YLayoutStyle.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 AnchorPosition getAnchorPosition(TypedArray attrs, int attr, AnchorPosition defaultValue) { - return AnchorPosition.values()[attrs.getInt(attr, defaultValue.ordinal())]; + private static Anchor getAnchorPosition(TypedArray attrs, int attr, Anchor defaultValue) { + return Anchor.values()[attrs.getInt(attr, defaultValue.ordinal())]; } - public static void configureStep(TypedArray attrs, XYStepModel model, int stepModeAttr, int stepValueAttr) { + public static void configureStep(TypedArray attrs, StepModel model, int stepModeAttr, int stepValueAttr) { if(attrs != null) { - model.setMode(XYStepMode.values()[attrs.getInt(stepModeAttr, model.getMode().ordinal())]); + model.setMode(StepMode.values()[attrs.getInt(stepModeAttr, model.getMode().ordinal())]); model.setValue(getIntFloatDimenValue(attrs, stepValueAttr, model.getValue()).doubleValue()); } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/Configurator.java b/androidplot-core/src/main/java/com/androidplot/util/Configurator.java deleted file mode 100644 index ac2eb063..00000000 --- a/androidplot-core/src/main/java/com/androidplot/util/Configurator.java +++ /dev/null @@ -1,346 +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.util; - -import android.content.Context; -import android.content.res.XmlResourceParser; -import android.graphics.Color; -import android.util.Log; -import android.util.TypedValue; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.HashMap; - -/** - * Utility class for "configuring" objects via XML config files. Supports the following field types: - * String - * Enum - * int - * float - * boolean - *

- * Config files should be stored in /res/xml. Given the XML configuration /res/xml/myConfig.xml, one can apply the - * configuration to an Object instance as follows: - *

- * MyObject obj = new MyObject(); - * Configurator.configure(obj, R.xml.myConfig); - *

- * WHAT IT DOES: - * Given a series of parameters stored in an XML file, Configurator iterates through each parameter, using the name - * as a map to the field within a given object. For example: - *

- *

- * {@code
- * 
- * }
- * 
- *

- * Given a Car instance car and assuming the method setCondition(String) exists within the SparkPlug class, - * Configurator does the following: - *

- *

- * {@code
- * car.getEngine().getSparkPlug().setCondition("poor");
- * }
- * 
- *

- * Now let's pretend that setCondition takes an instance of the Condition enum as it's argument. - * Configurator then does the following: - *

- * car.getEngine().getSparkPlug().setCondition(Condition.valueOf("poor"); - *

- * Now let's look at how ints are handled. Given the following xml: - *

- * - *

- * would result in: - * car.getEngine.setMiles(Integer.ParseInt("100000"); - *

- * That's pretty straight forward. But colors are expressed as ints too in Android - * but can be defined using hex values or even names of colors. When Configurator - * attempts to parse a parameter for a method that it knows takes an int as it's argument, - * Configurator will first attempt to parse the parameter as a color. Only after this - * attempt fails will Configurator resort to Integer.ParseInt. So: - *

- * - *

- * would result in: - * car.getHood().getPaint().setColor(Color.parseColor("Red"); - *

- * Next lets talk about float. Floats can appear in XML a few different ways in Android, - * especially when it comes to defining dimensions: - *

- * - *

- * Configurator will correctly parse each of these into their corresponding real pixel value expressed as a float. - *

- * One last thing to keep in mind when using Configurator: - * Values for Strings and ints can be assigned to localized values, allowing - * a cleaner solution for those developing apps to run on multiple form factors - * or in multiple languages: - *

- * - */ -@SuppressWarnings("WeakerAccess") -public abstract class Configurator { - - private static final String TAG = Configurator.class.getName(); - protected static final String CFG_ELEMENT_NAME = "config"; - - protected static int parseResId(Context ctx, String prefix, String value) { - String[] split = value.split("/"); - // is this a localized resource? - if (split.length > 1 && split[0].equalsIgnoreCase(prefix)) { - String pack = split[0].replace("@", ""); - String name = split[1]; - return ctx.getResources().getIdentifier(name, pack, ctx.getPackageName()); - } else { - throw new IllegalArgumentException(); - } - } - - protected static int parseIntAttr(Context ctx, String value) { - try { - return ctx.getResources().getColor(parseResId(ctx, "@color", value)); - } catch (IllegalArgumentException e1) { - try { - return Color.parseColor(value); - } catch (IllegalArgumentException e2) { - // wasn't a color so try parsing as a plain old int: - return Integer.parseInt(value); - } - } - } - - /** - * Treats value as a float parameter. First value is tested to see whether - * it contains a resource identifier. Failing that, it is tested to see whether - * a dimension suffix (dp, em, mm etc.) exists. Failing that, it is evaluated as - * a plain old float. - * @param ctx - * @param value - * @return - */ - protected static float parseFloatAttr(Context ctx, String value) { - try { - return ctx.getResources().getDimension(parseResId(ctx, "@dimen", value)); - } catch (IllegalArgumentException e1) { - try { - return PixelUtils.stringToDimension(value); - } catch (Exception e2) { - return Float.parseFloat(value); - } - } - } - - protected static String parseStringAttr(Context ctx, String value) { - try { - return ctx.getResources().getString(parseResId(ctx, "@string", value)); - } catch (IllegalArgumentException e1) { - return value; - } - } - - - protected static Method getSetter(Class clazz, final String fieldId) throws NoSuchMethodException { - Method[] methods = clazz.getMethods(); - - String methodName = "set" + fieldId; - for (Method method : methods) { - if (method.getName().equalsIgnoreCase(methodName)) { - return method; - } - } - throw new NoSuchMethodException("No such public method (case insensitive): " + - methodName + " in " + clazz); - } - - @SuppressWarnings("unchecked") - protected static Method getGetter(Class clazz, final String fieldId) throws NoSuchMethodException { - Log.d(TAG, "Attempting to find getter for " + fieldId + " in class " + clazz.getName()); - String firstLetter = fieldId.substring(0, 1); - String methodName = "get" + firstLetter.toUpperCase() + fieldId.substring(1, fieldId.length()); - return clazz.getMethod(methodName); - } - - /** - * Returns the object containing the field specified by path. - * @param obj - * @param path Path through member hierarchy to the destination field. - * @return null if the object at path cannot be found. - * @throws java.lang.reflect.InvocationTargetException - * - * @throws IllegalAccessException - */ - protected static Object getObjectContaining(Object obj, String path) throws - InvocationTargetException, IllegalAccessException, NoSuchMethodException { - if(obj == null) { - throw new NullPointerException("Attempt to call getObjectContaining(Object obj, String path) " + - "on a null Object instance. Path was: " + path); - } - Log.d(TAG, "Looking up object containing: " + path); - int separatorIndex = path.indexOf("."); - - // not there yet, descend deeper: - if (separatorIndex > 0) { - String lhs = path.substring(0, separatorIndex); - String rhs = path.substring(separatorIndex + 1, path.length()); - - // use getter to retrieve the instance - Method m = getGetter(obj.getClass(), lhs); - if(m == null) { - throw new NullPointerException("No getter found for field: " + lhs + " within " + obj.getClass()); - } - Log.d(TAG, "Invoking " + m.getName() + " on instance of " + obj.getClass().getName()); - Object o = m.invoke(obj); - // delve into o - return getObjectContaining(o, rhs); - //} catch (NoSuchMethodException e) { - // TODO: log a warning - // return null; - //} - } else { - // found it! - return obj; - } - } - - @SuppressWarnings("unchecked") - private static Object[] inflateParams(Context ctx, Class[] params, String[] vals) throws NoSuchMethodException, - InvocationTargetException, IllegalAccessException { - Object[] out = new Object[params.length]; - int i = 0; - for (Class param : params) { - if (Enum.class.isAssignableFrom(param)) { - out[i] = param.getMethod("valueOf", String.class).invoke(null, vals[i].toUpperCase()); - } else if (param.equals(Float.TYPE)) { - out[i] = parseFloatAttr(ctx, vals[i]); - } else if (param.equals(Integer.TYPE)) { - out[i] = parseIntAttr(ctx, vals[i]); - } else if (param.equals(Boolean.TYPE)) { - out[i] = Boolean.valueOf(vals[i]); - } else if (param.equals(String.class)) { - out[i] = parseStringAttr(ctx, vals[i]); - } else { - throw new IllegalArgumentException( - "Error inflating XML: Setter requires param of unsupported type: " + param); - } - i++; - } - return out; - } - - /** - * - * @param ctx - * @param obj - * @param xmlFileId ID of the XML config file within /res/xml - */ - public static void configure(Context ctx, Object obj, int xmlFileId) { - XmlResourceParser xrp = ctx.getResources().getXml(xmlFileId); - try { - HashMap params = new HashMap(); - while (xrp.getEventType() != XmlResourceParser.END_DOCUMENT) { - xrp.next(); - String name = xrp.getName(); - if (xrp.getEventType() == XmlResourceParser.START_TAG) { - if (name.equalsIgnoreCase(CFG_ELEMENT_NAME)) - for (int i = 0; i < xrp.getAttributeCount(); i++) { - params.put(xrp.getAttributeName(i), xrp.getAttributeValue(i)); - } - break; - } - } - configure(ctx, obj, params); - } catch (XmlPullParserException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - xrp.close(); - } - } - - public static void configure(Context ctx, Object obj, HashMap params) { - for (String key : params.keySet()) { - try { - configure(ctx, obj, key, params.get(key)); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - Log.w(TAG, "Error inflating XML: Setter for field \"" + key + "\" does not exist. "); - e.printStackTrace(); - } - } - } - - /** - * Recursively descend into an object using key as the pathway and invoking the corresponding setter - * if one exists. - * - * @param key - * @param value - */ - protected static void configure(Context ctx, Object obj, String key, String value) - throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { - Object o = getObjectContaining(obj, key); - if (o != null) { - int idx = key.lastIndexOf("."); - String fieldId = idx > 0 ? key.substring(idx + 1, key.length()) : key; - - Method m = getSetter(o.getClass(), fieldId); - Class[] paramTypes = m.getParameterTypes(); - // TODO: add support for generic type params - if (paramTypes.length >= 1) { - - // split on "|" - // TODO: add support for String args containing a | - String[] paramStrs = value.split("\\|"); - if (paramStrs.length == paramTypes.length) { - - Object[] oa = inflateParams(ctx, paramTypes, paramStrs); - Log.d(TAG, "Invoking " + m.getName() + " with arg(s) " + argArrToString(oa)); - m.invoke(o, oa); - } else { - throw new IllegalArgumentException("Error inflating XML: Unexpected number of argments passed to \"" - + m.getName() + "\". Expected: " + paramTypes.length + " Got: " + paramStrs.length); - } - } else { - // Obvious this is not a setter - throw new IllegalArgumentException("Error inflating XML: no setter method found for param \"" + - fieldId + "\"."); - } - } - } - - protected static String argArrToString(Object[] args) { - String out = ""; - for(Object obj : args) { - out += (obj == null ? (out += "[null] ") : - ("[" + obj.getClass() + ": " + obj + "] ")); - } - return out; - } -} - diff --git a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java new file mode 100644 index 00000000..4fa11d8a --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java @@ -0,0 +1,119 @@ +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 { + + @NonNull private final Number number; + private boolean hasDoublePrimitive; + private boolean hasFloatPrimitive; + private boolean hasIntPrimitive; + + private double doublePrimitive; + private float floatPrimitive; + private int intPrimitive; + + /** + * 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) { + FastNumber rhs = (FastNumber) number; + this.number = rhs.number; + this.hasDoublePrimitive = rhs.hasDoublePrimitive; + this.hasFloatPrimitive = rhs.hasFloatPrimitive; + this.hasIntPrimitive = rhs.hasIntPrimitive; + this.doublePrimitive = rhs.doublePrimitive; + this.floatPrimitive = rhs.floatPrimitive; + this.intPrimitive = rhs.intPrimitive; + } else { + this.number = number; + } + } + + @Override + public int intValue() { + if(!hasIntPrimitive) { + intPrimitive = number.intValue(); + hasIntPrimitive = true; + } + return intPrimitive; + } + + @Override + public long longValue() { + // TODO: optimize me! + return number.longValue(); + } + + @Override + public float floatValue() { + if(!hasFloatPrimitive) { + floatPrimitive = number.floatValue(); + hasFloatPrimitive = true; + } + return floatPrimitive; + } + + @Override + public double doubleValue() { + if(!hasDoublePrimitive) { + doublePrimitive = number.doubleValue(); + hasDoublePrimitive = true; + } + 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/FontUtils.java b/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java index b790dc81..edae4d30 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java @@ -16,8 +16,7 @@ package com.androidplot.util; -import android.graphics.Paint; -import android.graphics.Rect; +import android.graphics.*; public class FontUtils { @@ -64,4 +63,18 @@ public static Rect getStringDimensions(String text, Paint paint) { return size; } + /** + * Draws text vertically centered on the specified coordinates + * @param canvas + * @param paint + * @param text + * @param cx + * @param cy + */ + public static void drawTextVerticallyCentered(Canvas canvas, String text, float cx, float cy, Paint paint) { + Rect textBounds = new Rect(); + paint.getTextBounds(text, 0, text.length(), textBounds); + canvas.drawText(text, cx, cy - textBounds.exactCenterY(), paint); + } + } diff --git a/androidplot-core/src/main/java/com/androidplot/util/ZHash.java b/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java similarity index 82% rename from androidplot-core/src/main/java/com/androidplot/util/ZHash.java rename to androidplot-core/src/main/java/com/androidplot/util/LayerHash.java index 81ad5e53..f11737a6 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/ZHash.java +++ b/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java @@ -1,156 +1,180 @@ -/* - * 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 java.util.HashMap; -import java.util.List; - -/** - * An implementation of {@link ZIndexable}. Provides fast element retrieval via hash key in addition to - * mutable ordering (z indexing) of elements. - */ -public class ZHash implements ZIndexable { - - private HashMap hash; - private ZLinkedList zlist; - - { - hash = new HashMap<>(); - zlist = new ZLinkedList<>(); - } - - public int size() { - return zlist.size(); - } - - - public ValueType get(KeyType key) { - return hash.get(key); - } - - public List getKeysAsList() { - return zlist; - } - - /** - * If key already exists within the structure, it's value is replaced with the new value and - * it's existing order is maintained. - * @param key - * @param value - */ - public synchronized void addToTop(KeyType key, ValueType value) { - if(hash.containsKey(key)) { - hash.put(key, value); - } else { - hash.put(key, value); - zlist.addToTop(key); - } - } - - /** - * If key already exists within the structure, it's value is replaced with the new value and - * it's existing order is maintained. - * @param key - * @param value - */ - public synchronized void addToBottom(KeyType key, ValueType value) { - if(hash.containsKey(key)) { - hash.put(key, value); - } else { - hash.put(key, value); - zlist.addToBottom(key); - } - } - - public synchronized boolean moveToTop(KeyType element) { - if(!hash.containsKey(element)) { - return false; - } else { - return zlist.moveToTop(element); - } - } - - public synchronized boolean moveAbove(KeyType objectToMove, KeyType reference) { - if(objectToMove == reference) { - throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B."); - } - if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) { - return false; - } else { - return zlist.moveAbove(objectToMove, reference); - } - } - - public synchronized boolean moveBeneath(KeyType objectToMove, KeyType reference) { - if(objectToMove == reference) { - throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B."); - } - if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) { - return false; - } else { - return zlist.moveBeneath(objectToMove, reference); - } - } - - public synchronized boolean moveToBottom(KeyType key) { - if(!hash.containsKey(key)) { - return false; - } else { - return zlist.moveToBottom(key); - } - } - - public synchronized boolean moveUp(KeyType key) { - if (!hash.containsKey(key)) { - return false; - } else { - return zlist.moveUp(key); - } - } - - public synchronized boolean moveDown(KeyType key) { - if (!hash.containsKey(key)) { - return false; - } else { - return zlist.moveDown(key); - } - } - - @Override - public List elements() { - return zlist; - } - - /** - * - * @return Ordered list of keys. - */ - public List keys() { - return elements(); - } - - - public synchronized boolean remove(KeyType key) { - if(hash.containsKey(key)) { - hash.remove(key); - zlist.remove(key); - return true; - } else { - return false; - } - } -} +/* + * 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 java.util.HashMap; +import java.util.List; + +/** + * An implementation of {@link Layerable}. Provides fast element retrieval via hash key in addition to + * mutable ordering (z indexing) of elements. + */ +public class LayerHash implements Layerable { + + private HashMap hash; + private LinkedLayerList zlist; + + { + hash = new HashMap<>(); + zlist = new LinkedLayerList<>(); + } + + public int size() { + return zlist.size(); + } + + + public ValueType get(KeyType key) { + return hash.get(key); + } + + public List getKeysAsList() { + return zlist; + } + + /** + * If key already exists within the structure, it's value is replaced with the new value and + * it's existing order is maintained. + * @param key + * @param value + */ + public synchronized void addToTop(KeyType key, ValueType value) { + if(hash.containsKey(key)) { + hash.put(key, value); + } else { + hash.put(key, value); + zlist.addToTop(key); + } + } + + /** + * If key already exists within the structure, it's value is replaced with the new value and + * it's existing order is maintained. + * @param key + * @param value + */ + public synchronized void addToBottom(KeyType key, ValueType value) { + if(hash.containsKey(key)) { + hash.put(key, value); + } else { + hash.put(key, value); + zlist.addToBottom(key); + } + } + + public synchronized boolean moveToTop(KeyType element) { + if(!hash.containsKey(element)) { + return false; + } else { + return zlist.moveToTop(element); + } + } + + public synchronized boolean moveAbove(KeyType objectToMove, KeyType reference) { + if(objectToMove == reference) { + throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B."); + } + if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) { + return false; + } else { + return zlist.moveAbove(objectToMove, reference); + } + } + + public synchronized boolean moveBeneath(KeyType objectToMove, KeyType reference) { + if(objectToMove == reference) { + throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B."); + } + if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) { + return false; + } else { + return zlist.moveBeneath(objectToMove, reference); + } + } + + public synchronized boolean moveToBottom(KeyType key) { + if(!hash.containsKey(key)) { + return false; + } else { + return zlist.moveToBottom(key); + } + } + + public synchronized boolean moveUp(KeyType key) { + if (!hash.containsKey(key)) { + return false; + } else { + return zlist.moveUp(key); + } + } + + public synchronized boolean moveDown(KeyType key) { + if (!hash.containsKey(key)) { + return false; + } else { + return zlist.moveDown(key); + } + } + + @Override + public List elements() { + return zlist; + } + + /** + * + * @return Ordered list of keys. + */ + public List keys() { + return elements(); + } + + + public synchronized boolean remove(KeyType key) { + if(hash.containsKey(key)) { + hash.remove(key); + zlist.remove(key); + return true; + } else { + 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/ListOrganizer.java b/androidplot-core/src/main/java/com/androidplot/util/LayerListOrganizer.java similarity index 92% rename from androidplot-core/src/main/java/com/androidplot/util/ListOrganizer.java rename to androidplot-core/src/main/java/com/androidplot/util/LayerListOrganizer.java index 67bb2a71..1080a223 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/ListOrganizer.java +++ b/androidplot-core/src/main/java/com/androidplot/util/LayerListOrganizer.java @@ -1,118 +1,118 @@ -/* - * 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 java.util.List; - -/** - * Utility class providing additional element organization operations. - * @param - */ -public class ListOrganizer implements ZIndexable { - - private static final int ZERO = 0; - private static final int ONE = 1; - - private List list; - - public ListOrganizer(List list) { - this.list = list; - } - - - public boolean moveToTop(ElementType element) { - if(list.remove(element)) { - list.add(list.size(), element); - return true; - } else { - return false; - } - } - - public boolean moveAbove(ElementType objectToMove, ElementType reference) { - if(objectToMove == reference) { - throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B."); - } - - - list.remove(objectToMove); - int refIndex = list.indexOf(reference); - list.add(refIndex + ONE, objectToMove); - return true; - } - - public boolean moveBeneath(ElementType objectToMove, ElementType reference) { - if (objectToMove == reference) { - throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B."); - } - - list.remove(objectToMove); - int refIndex = list.indexOf(reference); - list.add(refIndex, objectToMove); - return true; - - } - - public boolean moveToBottom(ElementType key) { - list.remove(key); - list.add(ZERO, key); - return true; - } - - public boolean moveUp(ElementType key) { - int widgetIndex = list.indexOf(key); - if(widgetIndex == - ONE) { - // key not found: - return false; - } - if(widgetIndex >= list.size() - ONE) { - // already at the top: - return true; - } - - ElementType widgetAbove = list.get(widgetIndex + ONE); - return moveAbove(key, widgetAbove); - } - - public boolean moveDown(ElementType key) { - int widgetIndex = list.indexOf(key); - if(widgetIndex == - ONE) { - // key not found: - return false; - } - if(widgetIndex <= ZERO) { - // already at the bottom: - return true; - } - - ElementType widgetBeneath = list.get(widgetIndex - ONE); - return moveBeneath(key, widgetBeneath); - } - - @Override - public List elements() { - return list; - } - - public void addToBottom(ElementType element) { - list.add(ZERO, element); - } - - public void addToTop(ElementType element) { - list.add(list.size(), element); - } -} +/* + * 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 java.util.List; + +/** + * Utility class providing additional element organization operations. + * @param + */ +public class LayerListOrganizer implements Layerable { + + private static final int ZERO = 0; + private static final int ONE = 1; + + private List list; + + public LayerListOrganizer(List list) { + this.list = list; + } + + + public boolean moveToTop(ElementType element) { + if(list.remove(element)) { + list.add(list.size(), element); + return true; + } else { + return false; + } + } + + public boolean moveAbove(ElementType objectToMove, ElementType reference) { + if(objectToMove == reference) { + throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B."); + } + + + list.remove(objectToMove); + int refIndex = list.indexOf(reference); + list.add(refIndex + ONE, objectToMove); + return true; + } + + public boolean moveBeneath(ElementType objectToMove, ElementType reference) { + if (objectToMove == reference) { + throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B."); + } + + list.remove(objectToMove); + int refIndex = list.indexOf(reference); + list.add(refIndex, objectToMove); + return true; + + } + + public boolean moveToBottom(ElementType key) { + list.remove(key); + list.add(ZERO, key); + return true; + } + + public boolean moveUp(ElementType key) { + int widgetIndex = list.indexOf(key); + if(widgetIndex == - ONE) { + // key not found: + return false; + } + if(widgetIndex >= list.size() - ONE) { + // already at the top: + return true; + } + + ElementType widgetAbove = list.get(widgetIndex + ONE); + return moveAbove(key, widgetAbove); + } + + public boolean moveDown(ElementType key) { + int widgetIndex = list.indexOf(key); + if(widgetIndex == - ONE) { + // key not found: + return false; + } + if(widgetIndex <= ZERO) { + // already at the bottom: + return true; + } + + ElementType widgetBeneath = list.get(widgetIndex - ONE); + return moveBeneath(key, widgetBeneath); + } + + @Override + public List elements() { + return list; + } + + public void addToBottom(ElementType element) { + list.add(ZERO, element); + } + + public void addToTop(ElementType element) { + list.add(list.size(), element); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/util/ZIndexable.java b/androidplot-core/src/main/java/com/androidplot/util/Layerable.java similarity index 88% rename from androidplot-core/src/main/java/com/androidplot/util/ZIndexable.java rename to androidplot-core/src/main/java/com/androidplot/util/Layerable.java index 19c0d6b6..f1af65e6 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/ZIndexable.java +++ b/androidplot-core/src/main/java/com/androidplot/util/Layerable.java @@ -1,80 +1,80 @@ -/* - * 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 java.util.List; - -/** - * Encapsulates the concept of z-indexable objects; Each object is stored above or below each other object and may - * be moved up and down in the queue relative to other elements in the hash or absolutely to the front or back of the queue. - * - * Note that the method names correspond to the order of items drawn directly on top of one another using an iterator; - * the first element drawn (lowest z-index) is effectively the "bottom" element. - * @param - */ -public interface ZIndexable { - - /** - * Move above all other elements - * @param element - * @return - */ - boolean moveToTop(ElementType element); - - - /** - * Move above the specified element - * @param objectToMove - * @param reference - * @return - */ - boolean moveAbove(ElementType objectToMove, ElementType reference); - - - /** - * Move beneath the specified element - * - * @param objectToMove - * @param reference - * @return - */ - boolean moveBeneath(ElementType objectToMove, ElementType reference); - - /** - * Move beneath all other elements - * @param key - * @return - */ - boolean moveToBottom(ElementType key); - - - /** - * Move up by one element - * @param key - * @return - */ - boolean moveUp(ElementType key); - - /** - * Move down by one element - * @param key - * @return - */ - boolean moveDown(ElementType key); - - List elements(); +/* + * 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 java.util.List; + +/** + * Encapsulates the concept of "layerable" objects; Each object is stored above or below each other object and may + * be moved up and down in the queue relative to other elements in the hash or absolutely to the front or back of the queue. + * + * Note that the method names correspond to the order of items drawn directly on top of one another using an iterator; + * the first element drawn (lowest layer) is effectively the "bottom" element. + * @param + */ +public interface Layerable { + + /** + * Move above all other elements + * @param element + * @return + */ + boolean moveToTop(ElementType element); + + + /** + * Move above the specified element + * @param objectToMove + * @param reference + * @return + */ + boolean moveAbove(ElementType objectToMove, ElementType reference); + + + /** + * Move beneath the specified element + * + * @param objectToMove + * @param reference + * @return + */ + boolean moveBeneath(ElementType objectToMove, ElementType reference); + + /** + * Move beneath all other elements + * @param key + * @return + */ + boolean moveToBottom(ElementType key); + + + /** + * Move up by one element + * @param key + * @return + */ + boolean moveUp(ElementType key); + + /** + * Move down by one element + * @param key + * @return + */ + boolean moveDown(ElementType key); + + List elements(); } \ No newline at end of file diff --git a/androidplot-core/src/main/java/com/androidplot/util/ZLinkedList.java b/androidplot-core/src/main/java/com/androidplot/util/LinkedLayerList.java similarity index 86% rename from androidplot-core/src/main/java/com/androidplot/util/ZLinkedList.java rename to androidplot-core/src/main/java/com/androidplot/util/LinkedLayerList.java index 2cc3d774..2f02603c 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/ZLinkedList.java +++ b/androidplot-core/src/main/java/com/androidplot/util/LinkedLayerList.java @@ -1,71 +1,75 @@ -/* - * 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 java.util.LinkedList; -import java.util.List; - -public class ZLinkedList extends LinkedList implements ZIndexable { - - private ListOrganizer organizer = new ListOrganizer<>(this); - - @Override - public boolean moveToTop(Type element) { - return organizer.moveToTop(element); - } - - @Override - public boolean moveAbove(Type objectToMove, Type reference) { - return organizer.moveAbove(objectToMove, reference); - } - - @Override - public boolean moveBeneath(Type objectToMove, Type reference) { - return organizer.moveBeneath(objectToMove, reference); - } - - @Override - public boolean moveToBottom(Type key) { - return organizer.moveToBottom(key); - } - - @Override - public boolean moveUp(Type key) { - return organizer.moveUp(key); - } - - @Override - public boolean moveDown(Type key) { - return organizer.moveDown(key); - } - - @Override - public List elements() { - return organizer.elements(); - } - - public void addToBottom(Type element) { - organizer.addToBottom(element); - } - - public void addToTop(Type element) { - organizer.addToTop(element); - } - - - -} +/* + * 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 java.util.LinkedList; +import java.util.List; + +/** + * A implementation of {@link Layerable} backed by a {@link LinkedList}. + * @param + */ +public class LinkedLayerList extends LinkedList implements Layerable { + + private LayerListOrganizer organizer = new LayerListOrganizer<>(this); + + @Override + public boolean moveToTop(Type element) { + return organizer.moveToTop(element); + } + + @Override + public boolean moveAbove(Type objectToMove, Type reference) { + return organizer.moveAbove(objectToMove, reference); + } + + @Override + public boolean moveBeneath(Type objectToMove, Type reference) { + return organizer.moveBeneath(objectToMove, reference); + } + + @Override + public boolean moveToBottom(Type key) { + return organizer.moveToBottom(key); + } + + @Override + public boolean moveUp(Type key) { + return organizer.moveUp(key); + } + + @Override + public boolean moveDown(Type key) { + return organizer.moveDown(key); + } + + @Override + public List elements() { + return organizer.elements(); + } + + public void addToBottom(Type element) { + organizer.addToBottom(element); + } + + public void addToTop(Type element) { + organizer.addToTop(element); + } + + + +} diff --git a/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java b/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java index 1686583b..8e3260b7 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java @@ -30,7 +30,6 @@ public class PixelUtils { private static DisplayMetrics metrics; - private static final float FLOAT_INT_AVG_NUDGE = 0.5f; /** * Recalculates scale value etc. Should be called when an application starts or @@ -48,26 +47,6 @@ public static PointF sub(PointF lhs, PointF rhs) { return new PointF(lhs.x - rhs.x, lhs.y - rhs.y); } - /** - * Converts a sub-pixel accurate RectF to - * a single pixel accurate rect. This is helpful - * for clipping operations which dont do a good job with - * subpixel vals. - * @param in - * @return - */ - public static RectF sink(RectF in) { - return nearestPixRect(in.left, in.top, in.right, in.bottom); - } - - public static RectF nearestPixRect(float left, float top, float right, float bottom) { - return new RectF( - (int) (left + FLOAT_INT_AVG_NUDGE), - (int) (top + FLOAT_INT_AVG_NUDGE), - (int) (right + FLOAT_INT_AVG_NUDGE), - (int) (bottom + FLOAT_INT_AVG_NUDGE)); - } - /** * Converts a dp value to pixels. * @param dp @@ -90,29 +69,6 @@ public static float spToPix(float sp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics); } - - /** - * - * @param fraction A float value between 0 and 1. - * @return Number of pixels fraction represents on the current device's display. - */ - public static float fractionToPixH(float fraction) { - checkMetrics(); - return metrics.heightPixels * fraction; - - } - - /** - * - * @param fraction A float value between 0 and 1. - * @return Number of pixels fraction represents on the current device's display. - */ - public static float fractionToPixW(float fraction) { - checkMetrics(); - return metrics.widthPixels * fraction; - } - - /** * * CODE BELOW IS ADAPTED IN PART FROM MINDRIOT'S SAMPLE CODE HERE: @@ -178,7 +134,7 @@ public InternalDimension(float value, int unit) { } /** - * Safety check to hopefully help clarify what could otherwise be a confusing NPE. + * Safety run to hopefully help clarify what could otherwise be a confusing NPE. */ private static void checkMetrics() { if(metrics == 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 016f2f32..3915e2ad 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java @@ -18,6 +18,8 @@ import android.graphics.RectF; +import com.androidplot.ui.*; + /** * Convenience methods for dealing with {@link android.graphics.RectF} */ @@ -38,4 +40,43 @@ public static boolean areIdentical(RectF r1, RectF r2) { r1.right == r2.right && r1.bottom == r2.bottom; } + + /** + * Calculates a new {@link RectF} by applying insets to rect. + * @param rect + * @param insets + * @return The {@link RectF} created as a result of applying insets, or the passed in + * instance, if the insets were null. + */ + public static RectF applyInsets(RectF rect, Insets insets) { + if (insets != null) { + return new RectF( + rect.left + insets.getLeft(), + rect.top + insets.getTop(), + rect.right - insets.getRight(), + rect.bottom - insets.getBottom()); + } else { + 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 38db8107..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,10 +17,12 @@ 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; /** @@ -33,11 +35,17 @@ public class Redrawer implements Runnable { private static final String TAG = Redrawer.class.getName(); - private List plots; + private List> plots; private long sleepTime; + + // used to temporarily pause rendering without disposing of the run thread private boolean keepRunning; + + // when set to false, run thread will be allowed to exit the main run loop private boolean keepAlive; + private Thread thread; + /** * * @param plots List of Plot instances to be redrawn @@ -45,16 +53,20 @@ public class Redrawer implements Runnable { * @param startImmediately If true, invokes run() immediately after construction. */ public Redrawer(List plots, float maxRefreshRate, boolean startImmediately) { - this.plots = plots; + this.plots = new ArrayList<>(plots.size()); + for(Plot plot : plots) { + this.plots.add(new WeakReference<>(plot)); + } setMaxRefreshRate(maxRefreshRate); - new Thread(this).start(); + thread = new Thread(this, "Androidplot Redrawer"); + thread.start(); if(startImmediately) { - run(); + start(); } } public Redrawer(Plot plot, float maxRefreshRate, boolean startImmediately) { - this(Arrays.asList(new Plot[]{plot}), maxRefreshRate, startImmediately); + this(Collections.singletonList(plot), maxRefreshRate, startImmediately); } /** @@ -97,8 +109,8 @@ public void run() { // TODO: record start and end timestamps and // TODO: calculate sleepTime from that, in order to more accurately // TODO: meet desired refresh rate. - for(Plot plot : plots) { - plot.redraw(); + for(WeakReference plotRef : plots) { + plotRef.get().redraw(); } synchronized (this) { wait(sleepTime); @@ -110,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 9e51b6f0..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,26 +16,256 @@ package com.androidplot.util; +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.List; + /** - * Created by nick_f on 7/24/14. + * Utilities for dealing with Series data. */ public class SeriesUtils { + public static RectRegion minMax(List seriesList) { + return minMax(null, seriesList); + } + + 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 + * @return + * @since 0.9.7 + */ + public static RectRegion minMax(XYConstraints constraints, List seriesList) { + return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()])); + } + + /** + * @param constraints May be null. + * @param seriesArray + * @return + * @since 0.9.7 + */ + public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArray) { + + final RectRegion bounds = new RectRegion(); + + // make sure there is series data to iterate over: + if (seriesArray != null && seriesArray.length > 0) { + + // iterate over each series + for (XYSeries series : seriesArray) { + + // if this is an advanced xy series then minMax have already been calculated for us: + if (series instanceof FastXYSeries) { + final RectRegion b = ((FastXYSeries) series).minMax(); + 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); + continue; + } + } + 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); + } + } + } + } + return bounds; + } + /** * + * @param bounds Starting minMax values to work from; only lists values that are greater than or less + * than those in bounds will be be used. + * @param lists lists to be evaluated for min/max values. + * @return the original bounds instance passed in + */ + public static Region minMax(Region bounds, List... lists) { + for (final List list : lists) { + for (final Number i : list) { + bounds.union(i); + } + } + return bounds; + } + + /** + * Compute the range of visible i-vals in the specified series. Assumes that x-vals are + * in strict ascending order; behavior is undefined otherwise. * @param series - * @return The largest yVal in the series or null if the series contains no non-null yVals. + * @param visibleBounds The visible constraints of the plot + * @return */ - public static Number getMaxY(XYSeries series) { - Number max = null; - for(int i = 0; i < series.size(); i++) { - Number thisNumber = series.getY(i); - if(max == null || thisNumber != null && thisNumber.doubleValue() > max.doubleValue()) { - max = thisNumber; + public static Region iBounds(XYSeries series, RectRegion visibleBounds) { + final float step = series.size() >= 200 ? 50 : 1; + final int iBoundsMin = iBoundsMin(series, visibleBounds.getMinX().doubleValue(), step); + final int iBoundsMax = iBoundsMax(series, visibleBounds.getMaxX().doubleValue(), step); + return new Region(iBoundsMin, iBoundsMax); + } + + /** + * TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing + * TODO a binary search algorithm that also supports nulls is not trivial and would not likely + * TODO result in any noticeable performance increase here. It's a task for another day! + * @param series + * @param visibleMax + * @param step + * @return The index of the smallest non-null value that is greater than visibleMax, or the index + * of the last element if no such value exists. + */ + protected static int iBoundsMax(XYSeries series, double visibleMax, float step) { + int max = series.size() - 1; + final int seriesSize = series.size(); + final int steps = (int) Math.ceil(seriesSize / step); + for (int stepIndex = steps; stepIndex >= 0; stepIndex--) { + final int i = stepIndex * (int) step; + for (int ii = 0; ii < step; ii++) { + final int iii = i + ii; + if(iii < seriesSize) { + final Number thisX = series.getX(iii); + if (thisX != null) { + final double thisDouble = thisX.doubleValue(); + if (thisDouble > visibleMax) { + // this is the smallest non-null value in this block, so skip + // to the next block: + max = iii; + break; + } else if (thisDouble == visibleMax) { + return iii; + } else { + return max; + } + } + } } } return max; } + + /** + * TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing + * TODO a binary search algorithm that also supports nulls is not trivial and would not likely + * TODO result in any noticeable performance increase here. It's a task for another day! + * @param series + * @param visibleMin + * @param step + * @return The index of the largest non-null value that is less than visible, or 0 + * (the first element index) if no such value exists. + */ + protected static int iBoundsMin(XYSeries series, double visibleMin, float step) { + int min = 0; + final int steps = (int) Math.ceil(series.size() / step); + for (int stepIndex = 1; stepIndex <= steps; stepIndex++) { + final int i = stepIndex * (int) step; + for (int ii = 1; ii <= step; ii++) { + final int iii = i - ii; + if(iii < 0) { + break; + } + if(iii < series.size()) { + final Number thisX = series.getX(iii); + if (thisX != null) { + if (thisX.doubleValue() < visibleMin) { + // this is the largest non-null value in this block, so skip + // to the next block: + min = iii; + break; + } else if (thisX.doubleValue() == visibleMin) { + return iii; + } else { + return min; + } + } + } + } + } + return min; + } + + /** + * Determine the minMax iVals of the xVals surrounding a range of one or more null values. + * @param series + * @param index index of the null value in question + * @return The iVals of the non-null values surrounding the null range. If the null range is unbounded on + * either side then either or both min and max values will also be null. + */ + protected static Region getNullRegion(XYSeries series, int index) { + Region region = new Region(); + if(series.getX(index) != null) { + throw new IllegalArgumentException("Attempt to find null region for non null index: " + index); + } + for(int i = index - 1; i >= 0; i--) { + Number val = series.getX(i); + if(val != null) { + region.setMin(i); + break; + } + } + + for(int i = index + 1; i < series.size(); i++) { + Number val = series.getX(i); + if(val != null) { + region.setMax(i); + break; + } + } + return region; + } + + /** + * @param lists + * @return + * @since 0.9.7 + */ + public static Region minMax(List... lists) { + return minMax(new Region(), lists); + } + + /** + * Determine the XVal order of an XYSeries. If series does not implement {@link OrderedXYSeries} + * then {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE} is assumed. + * @param series + * @return The {@link com.androidplot.xy.OrderedXYSeries.XOrder} of the series. + */ + public static OrderedXYSeries.XOrder getXYOrder(XYSeries series) { + return series instanceof OrderedXYSeries ? + ((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java b/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java deleted file mode 100644 index 22ac88df..00000000 --- a/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java +++ /dev/null @@ -1,96 +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.util; - -import android.graphics.PointF; -import android.graphics.RectF; - -/** - * Utility methods for converting pixel coordinates into real values and vice versa. - */ -public class ValPixConverter { - private static final int ZERO = 0; - - - public static float valToPix(double val, double min, double max, float lengthPix, boolean flip) { - if(lengthPix <= ZERO) { - throw new IllegalArgumentException("Length in pixels must be greater than 0."); - } - double range = range(min, max); - double scale = lengthPix / range; - double raw = val - min; - float pix = (float)(raw * scale); - - if(flip) { - pix = (lengthPix - pix); - } - return pix; - } - - public static double range(double min, double max) { - return (max-min); - } - - - public static double valPerPix(double min, double max, float lengthPix) { - double valRange = range(min, max); - return valRange/lengthPix; - } - - /** - * Convert a value in pixels to the type passed into min/max - * @param pix - * @param min - * @param max - * @param lengthPix - * @param flip True if the axis should be reversed before calculated. This is the case - * with the y axis for screen coords. - * @return - */ - public static double pixToVal(float pix, double min, double max, float lengthPix, boolean flip) { - if(pix < ZERO) { - throw new IllegalArgumentException("pixel values cannot be negative."); - } - - if(lengthPix <= ZERO) { - throw new IllegalArgumentException("Length in pixels must be greater than 0."); - } - float pMult = pix; - if(flip) { - pMult = lengthPix - pix; - } - double range = range(min, max); - return ((range / lengthPix) * pMult) + min; - } - - /** - * Converts a real value into a pixel value. - * @param x Real d (domain) component of the point to convert. - * @param y Real y (range) component of the point to convert. - * @param plotArea - * @param minX Minimum visible real value on the d (domain) axis. - * @param maxX Maximum visible real value on the y (domain) axis. - * @param minY Minimum visible real value on the y (range) axis. - * @param maxY Maximum visible real value on the y (range axis. - * @return - */ - public static PointF valToPix(Number x, Number y, RectF plotArea, Number minX, Number maxX, Number minY, Number maxY) { - float pixX = ValPixConverter.valToPix(x.doubleValue(), minX.doubleValue(), maxX.doubleValue(), plotArea.width(), false) + (plotArea.left); - float pixY = ValPixConverter.valToPix(y.doubleValue(), minY.doubleValue(), maxY.doubleValue(), plotArea.height(), true) + plotArea.top; - return new PointF(pixX, pixY); - } -} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java new file mode 100644 index 00000000..11b9e8e8 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java @@ -0,0 +1,130 @@ +/* + * Copyright 2016 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.content.*; +import android.graphics.*; +import com.androidplot.ui.RenderStack; +import com.androidplot.ui.SeriesRenderer; + +/** + * This is an experimental (but stable) implementation of an {@link XYSeriesRenderer} that provides instrumentation + * allowing advanced behaviors like dynamically coloring / styling individual segments of a series, etc. This class + * may be removed or renamed in future releases. + * Currently has the following constraints: + * - Interpolation is not supported + * - Only draws lines; no points or fill + * - Draws series lines using simple Canvas.drawLine(...) invocations. + * @since 0.9.9 + */ +public class AdvancedLineAndPointRenderer extends XYSeriesRenderer { + + private int latestIndex; + + public AdvancedLineAndPointRenderer(XYPlot plot) { + super(plot); + } + + @Override + 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++) { + Number y = series.getY(i); + Number x = series.getX(i); + + if (y != null && x != null) { + thisPoint = getPlot().getBounds() + .transformScreen(x, y, plotArea); + } else { + thisPoint = null; + } + + // don't need to do any of this if the line isnt going to be drawn: + if(formatter.getLinePaint() != null) { + if (thisPoint != null && lastPoint != null) { + canvas.drawLine(lastPoint.x, lastPoint.y, thisPoint.x, thisPoint.y, formatter.getLinePaint(i, latestIndex, series.size())); + } + } + lastPoint = thisPoint; + } + } + + @Override + protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) { + if(formatter.getLinePaint() != null) { + canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint()); + } + } + + public void setLatestIndex(int latestIndex) { + this.latestIndex = latestIndex; + } + + + /** + * Formatter designed to work in tandem with {@link AdvancedLineAndPointRenderer}. + * @since 0.9.9 + */ + public static class Formatter extends XYSeriesFormatter { + + private static final int DEFAULT_STROKE_WIDTH = 3; + + private Paint linePaint; + + public Formatter() { + linePaint = new Paint(); + linePaint.setStrokeWidth(DEFAULT_STROKE_WIDTH); + linePaint.setColor(Color.RED); + } + + public Formatter(Context context, int xmlConfigId) { + this(); + configure(context, xmlConfigId); + } + + @Override + public Class getRendererClass() { + return AdvancedLineAndPointRenderer.class; + } + + @Override + public AdvancedLineAndPointRenderer doGetRendererInstance(XYPlot plot) { + return new AdvancedLineAndPointRenderer(plot); + } + + public Paint getLinePaint() { + return linePaint; + } + + /** + * By default, simply returns the line paint as-is. May be overridden to provide custom behavior based + * on input params. + * @param thisIndex + * @param latestIndex + * @param seriesSize + * @return + */ + public Paint getLinePaint(int thisIndex, int latestIndex, int seriesSize) { + return getLinePaint(); + } + + public void setLinePaint(Paint linePaint) { + this.linePaint = linePaint; + } + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYAxisType.java b/androidplot-core/src/main/java/com/androidplot/xy/Axis.java similarity index 93% rename from androidplot-core/src/main/java/com/androidplot/xy/XYAxisType.java rename to androidplot-core/src/main/java/com/androidplot/xy/Axis.java index f01c669d..18f2cdf9 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYAxisType.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/Axis.java @@ -1,22 +1,22 @@ -/* - * 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; - -public enum XYAxisType { - DOMAIN, - RANGE -} +/* + * 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; + +public enum Axis { + DOMAIN, + RANGE +} 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 fa072c04..3272748d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java @@ -15,6 +15,7 @@ */ package com.androidplot.xy; +import android.content.*; import android.graphics.Paint; import com.androidplot.ui.SeriesRenderer; @@ -39,9 +40,16 @@ 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()... + */ + public BarFormatter() { fillPaint = new Paint(); - //fillPaint.setColor(Color.RED); fillPaint.setStyle(Paint.Style.FILL); fillPaint.setAlpha(100); borderPaint = new Paint(); @@ -49,24 +57,56 @@ public void setBorderPaint(Paint borderPaint) { borderPaint.setAlpha(100); } - /** - * Should only be used in conjunction with calls to configure()... - */ - public BarFormatter() { - } - public BarFormatter(int fillColor, int borderColor) { + this(); fillPaint.setColor(fillColor); borderPaint.setColor(borderColor); } + public BarFormatter(Context context, int xmlCfgId) { + this(); + configure(context, xmlCfgId); + } + @Override public Class getRendererClass() { return BarRenderer.class; } @Override - public SeriesRenderer getRendererInstance(XYPlot plot) { + 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 5279a1a6..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,91 +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.exception.PlotRenderException; import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesAndFormatter; -import com.androidplot.util.ValPixConverter; +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 XYSeriesRenderer { +public class BarRenderer extends GroupRenderer { + + 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); - private BarRenderStyle renderStyle = BarRenderStyle.OVERLAID; // default Render Style - private BarWidthStyle widthStyle = BarWidthStyle.FIXED_WIDTH; // default Width Style - private float barWidth = 5; - private float barGap = 1; - private Comparator barComparator = new BarComparator(); + /** + * 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, - public enum BarRenderStyle { + /** + * 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 } - public enum BarWidthStyle { - 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 setBarRenderStyle(BarRenderStyle renderStyle) { - this.renderStyle = renderStyle; + public BarGroupWidthMode getBarGroupWidthMode() { + return this.barGroupWidthMode; } - - public void setBarWidthStyle(BarWidthStyle widthStyle) { - this.widthStyle = widthStyle; + + public float getBarGroupWidth() { + return this.width; } - - public void setBarWidthStyle(BarWidthStyle style, float value) { - setBarWidthStyle(style); - switch (style) { - 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) { - canvas.drawRect(rect, formatter.getFillPaint()); + if (formatter.hasFillPaint()) { + canvas.drawRect(rect, formatter.getFillPaint()); + } canvas.drawRect(rect, formatter.getBorderPaint()); } @@ -121,281 +132,254 @@ public FormatterType getFormatter(int index, XYSeries series) { } @Override - public void onRender(Canvas canvas, RectF plotArea, XYSeries series, - FormatterType barFormatter, RenderStack stack) throws PlotRenderException { - - // get all the series associated with this renderer: - List> sfPairList = getSeriesList(); - - // this renderer uses special element-by-element z-indexing that results in - // all series associated with the renderer being rendered in a single pass, so - // we need to exclude the rest of the series on the render stack from being redrawn later: - stack.disable(getClass()); - - TreeMap axisMap = new TreeMap(); - - // dont try to render anything if there's nothing to render. - if(sfPairList == null) return; - - /* + public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { + + 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(SeriesAndFormatter thisPair : sfPairList) { - BarGroup barGroup; - - // For each value in the series - for(int i = 0; i < thisPair.getSeries().size(); 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 - * the Render Style and Width Style. + /* + * 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 (widthStyle) { - 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 = ValPixConverter.valToPix(rangeOrigin, - getPlot().getCalculatedMinY().doubleValue(), - getPlot().getCalculatedMaxY().doubleValue(), - plotArea.height(), true) + plotArea.top; - - switch (renderStyle) { - case OVERLAID: - Collections.sort(barGroup.bars, barComparator); - for (Bar bar : barGroup.bars) { - BarFormatter formatter = bar.getFormatter(); - PointLabelFormatter plf = formatter.getPointLabelFormatter(); - 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 (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 && 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.getPointLabelFormatter(); - 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 (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 && 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.getPointLabelFormatter(); - PointLabeler pointLabeler = null; - if (formatter != null) { - pointLabeler = formatter.getPointLabeler(); - } - int height = (int) b.barGroup.plotArea.bottom - b.intY; - int top = bottom - height; - if (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 && 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 float 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 = ValPixConverter.valToPix(xVal, getPlot().getCalculatedMinX().doubleValue(), getPlot().getCalculatedMaxX().doubleValue(), plotArea.width(), false) + (plotArea.left); - this.intX = (int) pixX; - - if (series.getY(seriesIndex) != null) { - this.yVal = series.getY(seriesIndex).doubleValue(); - this.pixY = ValPixConverter.valToPix(yVal, getPlot().getCalculatedMinY().doubleValue(), getPlot().getCalculatedMaxY().doubleValue(), plotArea.height(), true) + plotArea.top; - this.intY = (int) 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 (renderStyle) { - 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/BoundaryMode.java b/androidplot-core/src/main/java/com/androidplot/xy/BoundaryMode.java index 47c4fe51..c0b791a6 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BoundaryMode.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BoundaryMode.java @@ -20,7 +20,7 @@ public enum BoundaryMode { FIXED, AUTO, GROW, - SHRINNK + SHRINK } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java new file mode 100644 index 00000000..3a794b5c --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java @@ -0,0 +1,97 @@ +/* + * Copyright 2016 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.content.Context; +import android.graphics.Color; +import android.graphics.Paint; + +import com.androidplot.ui.SeriesRenderer; +import com.androidplot.util.PixelUtils; + +/** + * Format for drawing a value using {@link BubbleRenderer}. + * @since 1.2.2 + */ +public class BubbleFormatter extends XYSeriesFormatter { + + private static final float DEFAULT_STROKE_PIX = 1; + private static final int DEFAULT_STROKE_COLOR = Color.BLACK; + private static final int DEFAULT_FILL_COLOR = Color.YELLOW; + + + private Paint strokePaint; + private Paint fillPaint; + + { + strokePaint = new Paint(); + strokePaint.setAntiAlias(true); + strokePaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_STROKE_PIX)); + strokePaint.setStyle(Paint.Style.STROKE); + strokePaint.setColor(DEFAULT_STROKE_COLOR); + + fillPaint = new Paint(); + fillPaint.setAntiAlias(true); + fillPaint.setColor(DEFAULT_FILL_COLOR); + + // default point labeler should draw z for bubbles: + setPointLabeler(new PointLabeler() { + @Override + public String getLabel(BubbleSeries series, int index) { + return String.valueOf(series.getZ(index)); + } + }); + } + + public BubbleFormatter() {} + + public BubbleFormatter(Context context, int xmlCfgId) { + this(); + configure(context, xmlCfgId); + } + + public BubbleFormatter(int fillColor, int strokeColor) { + fillPaint.setColor(fillColor); + strokePaint.setColor(strokeColor); + } + + @Override + public Class getRendererClass() { + return BubbleRenderer.class; + } + + @Override + public BubbleRenderer doGetRendererInstance(XYPlot plot) { + return new BubbleRenderer(plot); + } + + public Paint getStrokePaint() { + return strokePaint; + } + + public void setStrokePaint(Paint strokePaint) { + this.strokePaint = strokePaint; + } + + public Paint getFillPaint() { + return fillPaint; + } + + public void setFillPaint(Paint fillPaint) { + this.fillPaint = fillPaint; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java new file mode 100644 index 00000000..dcebf6cb --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java @@ -0,0 +1,151 @@ +package com.androidplot.xy; + +import android.graphics.*; + +import com.androidplot.Region; +import com.androidplot.ui.*; +import com.androidplot.util.*; + +/** + * Renders three dimensional data onto an {@link XYPlot} as bubbles; the x/y values define the position + * of the bubble and z is uses as a scaling value for the bubble's radius. + * @since 1.2.2 + */ +public class BubbleRenderer extends XYSeriesRenderer { + + protected static final float MIN_BUBBLE_RADIUS_DEFAULT_DP = 9; + protected static final float MAX_BUBBLE_RADIUS_DEFAULT_DP = 25; + + private Region bubbleBounds; + + private BubbleScaleMode bubbleScaleMode = BubbleScaleMode.SQUARE_ROOT; + + public enum BubbleScaleMode { + + /** + * Bubble radius is scaled directly by {@link BubbleSeries} z-vals + */ + LINEAR, + + /** + * Bubble radius is scaled by the square root of {@link BubbleSeries} z-vals. + * This is the default scaling used. + */ + SQUARE_ROOT + } + + public BubbleRenderer(XYPlot plot) { + super(plot); + + bubbleBounds = new Region( + PixelUtils.dpToPix(MIN_BUBBLE_RADIUS_DEFAULT_DP), + PixelUtils.dpToPix(MAX_BUBBLE_RADIUS_DEFAULT_DP)); + } + + @Override + protected void onRender(Canvas canvas, RectF plotArea, BubbleSeries series, + FormatterType formatter, RenderStack stack) { + + Region magnitudeBounds = calculateBounds(); + for(int i = 0; i < series.size(); i++) { + + // only render non-null values greater than zero: + if(series.getY(i) != null && series.getZ(i).doubleValue() > 0) { + + final PointF centerPoint = getPlot().getBounds(). + transform(series.getX(i), series.getY(i), plotArea, false, true); + + // calculate bubble radius: + float bubbleRadius = magnitudeBounds. + transform(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT ? + Math.sqrt(series.getZ(i).doubleValue()) : + series.getZ(i).doubleValue(), bubbleBounds).floatValue(); + drawBubble(canvas, formatter, series, i, centerPoint, bubbleRadius); + } + } + } + + /** + * Render a bubble onto the canvas + * @param canvas + * @param formatter + * @param series + * @param index + * @param centerPoint the x/y coords of the center of the bubble + * @param radius size of the bubble + */ + protected void drawBubble(Canvas canvas, FormatterType formatter, BubbleSeries series, + int index, PointF centerPoint, float radius) { + canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getFillPaint()); + canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getStrokePaint()); + if(series != null && formatter.hasPointLabelFormatter() && formatter.getPointLabeler() != null) { + FontUtils.drawTextVerticallyCentered( + canvas, + formatter.getPointLabeler().getLabel(series, index), + centerPoint.x, + centerPoint.y, + formatter.getPointLabelFormatter().getTextPaint()); + } + } + + @Override + protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) { + drawBubble(canvas, formatter, null, 0, + new PointF(rect.centerX(), rect.centerY()), (rect.width()/2.5f)); + } + + public float getMinBubbleRadius() { + return bubbleBounds.getMin().floatValue(); + } + + public void setMinBubbleRadius(float minBubbleRadius) { + bubbleBounds.setMin(minBubbleRadius); + } + + public float getMaxBubbleRadius() { + return bubbleBounds.getMax().floatValue(); + } + + public void setMaxBubbleRadius(float maxBubbleRadius) { + bubbleBounds.setMax(maxBubbleRadius); + } + + public BubbleScaleMode getBubbleScaleMode() { + return bubbleScaleMode; + } + + public void setBubbleScaleMode(BubbleScaleMode bubbleScaleMode) { + this.bubbleScaleMode = bubbleScaleMode; + } + + protected Region calculateBounds() { + Region bounds = new Region(); + for(SeriesBundle f : getSeriesAndFormatterList()) { + SeriesUtils.minMax(bounds, f.getSeries().getZVals()); + } + + if(bounds.getMax() != null && bounds.getMax().doubleValue() > 0) { + if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) { + // scale for easier visual interpretation. see: + // https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly + bounds.setMax(Math.sqrt(bounds.getMax().doubleValue())); + } + } else { + // no non-null, greater than zero vals so bounds are undefined + return null; + } + + if(bounds.getMin().doubleValue() > 0) { + + if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) { + // scale for easier visual interpretation. see: + // https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly + bounds.setMin(Math.sqrt(bounds.getMin().doubleValue())); + } + } else { + // if the smallest value is negative, use zero instead since those vals arent visible: + bounds.setMax(0); + } + return bounds; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java new file mode 100644 index 00000000..ac36dc46 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java @@ -0,0 +1,81 @@ +package com.androidplot.xy; + +import java.util.*; + +/** + * Created by halfhp on 9/17/16. + */ +public class BubbleSeries implements XYSeries { + + private List xVals; + private List yVals; + private List zVals; + private String title; + + /** + * + * @param interleavedValues Interleaved values ordered as x, y, z; total size must be a multiple of 3. + */ + public BubbleSeries(Number... interleavedValues) { + if(interleavedValues == null || interleavedValues.length % 3 > 0) { + throw new RuntimeException("BubbleSeries interleave array length must be a non-zero multiple of 3."); + } + + xVals = new ArrayList<>(); + yVals = new ArrayList<>(); + zVals = new ArrayList<>(); + for(int i = 0; i < interleavedValues.length; i+=3) { + xVals.add(interleavedValues[i]); + yVals.add(interleavedValues[i+1]); + zVals.add(interleavedValues[i+2]); + } + } + + public BubbleSeries(List yVals, List zVals, String title) { + this.yVals = yVals; + this.zVals = zVals; + this.title = title; + // populate x with iVals: + this.xVals = new ArrayList<>(zVals.size()); + for(int i = 0; i < zVals.size(); i++) { + this.xVals.add(i); + } + } + + public BubbleSeries(List xVals, List yVals, List zVals, String title) { + this.xVals = xVals; + this.yVals = yVals; + this.zVals = zVals; + this.title = title; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public int size() { + return xVals.size(); + } + + @Override + public Number getX(int index) { + return xVals.get(index); + } + + @Override + public Number getY(int index) { + return yVals.get(index); + } + + public Number getZ(int index) { + return zVals.get(index); + } + + public List getZVals() { + return zVals; + } + + +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java new file mode 100644 index 00000000..84fce88f --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java @@ -0,0 +1,205 @@ +/* + * Copyright 2016 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.content.*; +import android.graphics.Color; +import android.graphics.Paint; +import com.androidplot.ui.SeriesRenderer; +import com.androidplot.util.PixelUtils; + +/** + * Format for drawing a value using {@link CandlestickRenderer}. + * @since 0.9.7 + */ +public class CandlestickFormatter extends XYSeriesFormatter { + + private static final float DEFAULT_WIDTH_PIX = PixelUtils.dpToPix(10); + private static final float DEFAULT_STROKE_PIX = PixelUtils.dpToPix(4); + + private Paint wickPaint; + private Paint risingBodyFillPaint; + private Paint fallingBodyFillPaint; + private Paint risingBodyStrokePaint; + private Paint fallingBodyStrokePaint; + private Paint upperCapPaint; + private Paint lowerCapPaint; + + private float bodyWidth = DEFAULT_WIDTH_PIX; + private float upperCapWidth = DEFAULT_WIDTH_PIX; + private float lowerCapWidth = DEFAULT_WIDTH_PIX; + + private BodyStyle bodyStyle; + + public enum BodyStyle { + SQUARE, + TRIANGULAR + } + + protected static Paint getDefaultFillPaint(int color) { + Paint p = new Paint(); + p.setStyle(Paint.Style.FILL); + p.setColor(color); + return p; + } + + protected static Paint getDefaultStrokePaint(int color) { + Paint p = new Paint(); + p.setStyle(Paint.Style.STROKE); + p.setStrokeWidth(DEFAULT_STROKE_PIX); + p.setColor(color); + p.setAntiAlias(true); + return p; + } + + public CandlestickFormatter(Context context, int xmlCfgId) { + this(); + configure(context, xmlCfgId); + } + + public CandlestickFormatter() { + this(getDefaultStrokePaint(Color.YELLOW), + getDefaultFillPaint(Color.GREEN), + getDefaultFillPaint(Color.RED), + getDefaultStrokePaint(Color.GREEN), + getDefaultStrokePaint(Color.RED), + getDefaultStrokePaint(Color.YELLOW), + getDefaultStrokePaint(Color.YELLOW), + BodyStyle.SQUARE); + } + + public CandlestickFormatter(Paint wickPaint, Paint risingBodyFillPaint, Paint fallingBodyFillPaint, + Paint risingBodyStrokePaint, Paint fallingBodyStrokePaint, + Paint upperCapPaint, Paint lowerCapPaint, BodyStyle bodyStyle) { + setWickPaint(wickPaint); + setRisingBodyFillPaint(risingBodyFillPaint); + setFallingBodyFillPaint(fallingBodyFillPaint); + setRisingBodyStrokePaint(risingBodyStrokePaint); + setFallingBodyStrokePaint(fallingBodyStrokePaint); + setUpperCapPaint(upperCapPaint); + setLowerCapPaint(lowerCapPaint); + setBodyStyle(bodyStyle); + } + + @Override + public Class getRendererClass() { + return CandlestickRenderer.class; + } + + @Override + public SeriesRenderer doGetRendererInstance(XYPlot plot) { + return new CandlestickRenderer(plot); + } + + public Paint getWickPaint() { + return wickPaint; + } + + public void setWickPaint(Paint wickPaint) { + this.wickPaint = wickPaint; + } + + public Paint getRisingBodyFillPaint() { + return risingBodyFillPaint; + } + + public void setRisingBodyFillPaint(Paint risingBodyFillPaint) { + this.risingBodyFillPaint = risingBodyFillPaint; + } + + public Paint getRisingBodyStrokePaint() { + return risingBodyStrokePaint; + } + + public void setRisingBodyStrokePaint(Paint risingBodyStrokePaint) { + this.risingBodyStrokePaint = risingBodyStrokePaint; + } + + public Paint getUpperCapPaint() { + return upperCapPaint; + } + + public void setUpperCapPaint(Paint upperCapPaint) { + this.upperCapPaint = upperCapPaint; + } + + public Paint getLowerCapPaint() { + return lowerCapPaint; + } + + public void setLowerCapPaint(Paint lowerCapPaint) { + this.lowerCapPaint = lowerCapPaint; + } + + public float getBodyWidth() { + return bodyWidth; + } + + public void setBodyWidth(float bodyWidth) { + this.bodyWidth = bodyWidth; + } + + public float getLowerCapWidth() { + return lowerCapWidth; + } + + public void setLowerCapWidth(float lowerCapWidth) { + this.lowerCapWidth = lowerCapWidth; + } + + public float getUpperCapWidth() { + return upperCapWidth; + } + + public void setUpperCapWidth(float upperCapWidth) { + this.upperCapWidth = upperCapWidth; + } + + public Paint getFallingBodyFillPaint() { + return fallingBodyFillPaint; + } + + public void setFallingBodyFillPaint(Paint fallingBodyFillPaint) { + this.fallingBodyFillPaint = fallingBodyFillPaint; + } + + public Paint getFallingBodyStrokePaint() { + return fallingBodyStrokePaint; + } + + public void setFallingBodyStrokePaint(Paint fallingBodyStrokePaint) { + this.fallingBodyStrokePaint = fallingBodyStrokePaint; + } + + public BodyStyle getBodyStyle() { + return bodyStyle; + } + + public void setBodyStyle(BodyStyle bodyStyle) { + this.bodyStyle = bodyStyle; + } + + /** + * Convenience method to set caps and wick to a single color in one call. + * @param paint + */ + public void setCapAndWickPaint(Paint paint) { + setUpperCapPaint(paint); + setLowerCapPaint(paint); + setWickPaint(paint); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickMaker.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickMaker.java new file mode 100644 index 00000000..8834cfbb --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickMaker.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016 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; + +/** + * Helper utility to simplify the creation of of candlestick charts + * @since 0.9.7 + */ +public abstract class CandlestickMaker { + + /** + * Adds a candlestick chart to the specified plot using the specified + * high, low, open and close values. + * @param plot + * @param formatter + * @param openVals + * @param closeVals + * @param highVals + * @param lowVals + */ + public static void make(XYPlot plot, CandlestickFormatter formatter, + XYSeries openVals, XYSeries closeVals, XYSeries highVals, XYSeries lowVals) { + plot.addSeries(formatter, highVals, lowVals, openVals, closeVals); + } + + /** + * Add a candlestick chart to the specified plot using the specified {@link CandlestickSeries}. + * @param plot + * @param formatter + * @param series + * @since 0.9.8 + */ + public static void make(XYPlot plot, CandlestickFormatter formatter, CandlestickSeries series) { + make(plot, formatter, series.getOpenSeries(), series.getCloseSeries(), + series.getHighSeries(), series.getLowSeries()); + } + + /** + * Check the validity of series data comprising a {@link CandlestickSeries}. + * This is a development aid; be sure to remove any usage of this method in production code. + * @param series + * @since 0.9.8 + */ + public static void check(CandlestickSeries series) { + check(series.getOpenSeries(), series.getCloseSeries(), series.getHighSeries(), series.getLowSeries()); + } + + /** + * Check the validity of series data comprising a candlestick chart. + * This is a development aid; be sure to remove any usage of this method in production code. + * @param openVals + * @param closeVals + * @param highVals + * @param lowVals + * @since 0.9.8 + */ + public static void check(XYSeries openVals, XYSeries closeVals, XYSeries highVals, XYSeries lowVals) { + final int size = openVals.size(); + assert closeVals.size() == size : "closeVals has irregular size."; + assert highVals.size() == size : "highVals has irregular size."; + assert lowVals.size() == size : "lowVals has irregular size."; + + for(int i = 0; i < size; i++) { + + final double highVal = highVals.getY(i).doubleValue(); + final double lowVal = lowVals.getY(i).doubleValue(); + final double openVal = openVals.getY(i).doubleValue(); + final double closeVal = closeVals.getY(i).doubleValue(); + + assert openVal <= highVal : "Detected openVal > highVal at index " + i; + assert openVal >= lowVal : "Detected openVal < lowVal at index " + i; + assert closeVal <= highVal : "Detected closeVal > highVal at index " + i; + assert closeVal >= lowVal : "Detected closeVal < lowVal at index " + i; + assert lowVal <= highVal : "Detected lowVal > highVal at index " + i; + } + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java new file mode 100644 index 00000000..81c4363c --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java @@ -0,0 +1,150 @@ +/* + * Copyright 2016 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.*; +import com.androidplot.ui.RenderStack; +import com.androidplot.ui.SeriesBundle; + +import java.util.List; + +/** + * Renders a group of {@link com.androidplot.xy.XYSeries} as a candlestick chart + * into an {@link com.androidplot.xy.XYPlot}. + * + * Constraints: + * - Exactly four series must be added using the same {@link CandlestickFormatter}. + * - Each of the four series has the same x(i) value. + * - Expects that series are added in the order of: + * high, low, open, close + * + * {@link CandlestickSeries} and {@link CandlestickMaker} provide simplified classes and methods + * for setting up a candlestick chart. + * @since 0.9.7 + */ +public class CandlestickRenderer extends GroupRenderer { + + protected static final int HIGH_INDEX = 0; + protected static final int LOW_INDEX = 1; + protected static final int OPEN_INDEX = 2; + protected static final int CLOSE_INDEX = 3; + + public CandlestickRenderer(XYPlot plot) { + super(plot); + } + + + @Override + public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { + + final FormatterType formatter = sfList.get(0).getFormatter(); + for(int i = 0; i < seriesSize; i++) { + + final XYSeries highSeries = sfList.get(HIGH_INDEX).getSeries(); + final XYSeries lowSeries = sfList.get(LOW_INDEX).getSeries(); + final XYSeries openSeries = sfList.get(OPEN_INDEX).getSeries(); + final XYSeries closeSeries = sfList.get(CLOSE_INDEX).getSeries(); + + // x-val for all series should be identical so just grab x from the first series: + Number x = highSeries.getX(i); + + Number high = highSeries.getY(i); + Number low = lowSeries.getY(i); + Number open = openSeries.getY(i); + Number close = closeSeries.getY(i); + + // draw the candlestick: + final PointF highPix = getPlot().getBounds().transformScreen(x, high, plotArea); + final PointF lowPix = getPlot().getBounds().transformScreen(x, low, plotArea); + final PointF openPix = getPlot().getBounds().transformScreen(x, open, plotArea); + final PointF closePix = getPlot().getBounds().transformScreen(x, close, plotArea); + + drawWick(canvas, highPix, lowPix, formatter); + drawBody(canvas, openPix, closePix, formatter); + drawUpperCap(canvas, highPix, formatter); + drawLowerCap(canvas, lowPix, formatter); + + // draw labels, if any: + final PointLabelFormatter plf = formatter.hasPointLabelFormatter() + ? formatter.getPointLabelFormatter() : null; + final PointLabeler pointLabeler = formatter.getPointLabeler(); + if(plf != null && pointLabeler != null) { + drawTextLabel(canvas, highPix, pointLabeler.getLabel(highSeries, i), plf); + drawTextLabel(canvas, lowPix, pointLabeler.getLabel(lowSeries, i), plf); + drawTextLabel(canvas, openPix, pointLabeler.getLabel(openSeries, i), plf); + drawTextLabel(canvas, closePix, pointLabeler.getLabel(closeSeries, i), plf); + } + } + } + + protected void drawTextLabel(Canvas canvas, PointF coords, String text, PointLabelFormatter plf) { + if(text != null) { + canvas.drawText(text, coords.x + plf.hOffset, coords.y + plf.vOffset, plf.getTextPaint()); + } + } + + protected void drawWick(Canvas canvas, PointF min, PointF max, FormatterType formatter) { + canvas.drawLine(min.x, min.y, max.x, max.y, formatter.getWickPaint()); + } + + protected void drawBody(Canvas canvas, PointF open, PointF close, FormatterType formatter) { + final float halfWidth = formatter.getBodyWidth() / 2; + final RectF rect = new RectF(open.x - halfWidth, open.y, close.x + halfWidth, close.y); + + Paint bodyFillPaint = open.y >= close.y ? + formatter.getRisingBodyFillPaint() : formatter.getFallingBodyFillPaint(); + + Paint bodyStrokePaint = open.y >= close.y ? + formatter.getRisingBodyStrokePaint() : formatter.getFallingBodyStrokePaint(); + + switch(formatter.getBodyStyle()) { + case SQUARE: + canvas.drawRect(rect, bodyFillPaint); + canvas.drawRect(rect, bodyStrokePaint); + break; + case TRIANGULAR: + drawTriangle(canvas, rect, bodyFillPaint, bodyStrokePaint); + } + } + + protected void drawUpperCap(Canvas canvas, PointF val, FormatterType formatter) { + final float halfWidth = formatter.getUpperCapWidth(); + canvas.drawLine(val.x - halfWidth, val.y, val.x + halfWidth, val.y, formatter.getUpperCapPaint()); + } + + protected void drawLowerCap(Canvas canvas, PointF val, FormatterType formatter) { + final float halfWidth = formatter.getLowerCapWidth(); + canvas.drawLine(val.x - halfWidth, val.y, val.x + halfWidth, val.y, formatter.getLowerCapPaint()); + } + + @Override + protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) { + // TODO + } + + protected void drawTriangle(Canvas canvas, RectF rect, + Paint fillPaint, Paint strokePaint) { + Path path = new Path(); + path.moveTo(rect.centerX(), rect.bottom); + path.lineTo(rect.left,rect.top); + path.lineTo(rect.right, rect.top); + path.close(); + canvas.drawPath(path, fillPaint); + canvas.drawPath(path, strokePaint); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickSeries.java new file mode 100644 index 00000000..40ec09ae --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickSeries.java @@ -0,0 +1,160 @@ +/* + * Copyright 2016 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 com.androidplot.xy.SimpleXYSeries; + +import java.util.*; + +/** + * Convenience class for representing a series of candlestick values; + * is NOT a descendant of {@link com.androidplot.xy.XYSeries} and therefore + * cannot be directly added to an {@link com.androidplot.xy.XYPlot}. + * + * This class is NOT threadsafe. + * + * @since 0.9.8 + */ +public class CandlestickSeries { + + private SimpleXYSeries highSeries = new SimpleXYSeries(null); + private SimpleXYSeries lowSeries = new SimpleXYSeries(null); + private SimpleXYSeries openSeries = new SimpleXYSeries(null); + private SimpleXYSeries closeSeries = new SimpleXYSeries(null); + + protected static List generateRange(int start, int end) { + List range = new ArrayList<>(end - start); + for(int i = start; i < end; i++) { + range.add(i); + } + return range; + } + + public CandlestickSeries(Item... items) { + this(Arrays.asList(items)); + } + + /** + * Creates a new CandlestickSeries. + * Calls {@link #CandlestickSeries(List, List)} with a list of xVals + * generated using the formula x=i. + * @param items + */ + public CandlestickSeries(List items) { + this(generateRange(0, items.size()), items); + } + + public CandlestickSeries(List xVals, List items) { + if(xVals.size() != items.size()) { + throw new IllegalArgumentException("xVals and yVals length must be identical."); + } + for(int i = 0; i < xVals.size(); i++) { + Number x = xVals.get(i); + highSeries.addLast(x, items.get(i).getHigh()); + lowSeries.addLast(x, items.get(i).getLow()); + openSeries.addLast(x, items.get(i).getOpen()); + closeSeries.addLast(x, items.get(i).getClose()); + } + } + + public SimpleXYSeries getHighSeries() { + return highSeries; + } + + public void setHighSeries(SimpleXYSeries highSeries) { + this.highSeries = highSeries; + } + + public SimpleXYSeries getLowSeries() { + return lowSeries; + } + + public void setLowSeries(SimpleXYSeries lowSeries) { + this.lowSeries = lowSeries; + } + + public SimpleXYSeries getOpenSeries() { + return openSeries; + } + + public void setOpenSeries(SimpleXYSeries openSeries) { + this.openSeries = openSeries; + } + + public SimpleXYSeries getCloseSeries() { + return closeSeries; + } + + public void setCloseSeries(SimpleXYSeries closeSeries) { + this.closeSeries = closeSeries; + } + + public static class Item { + private double low; + private double high; + private double open; + private double close; + + /** + * An individual candlestick value. Since it is illegal to include + * null values for any member of a candlestick, this class is modeled with + * double values instead of {@link Number} instances. + * @param low + * @param high + * @param open + * @param close + */ + public Item(double low, double high, double open, double close) { + this.low = low; + this.high = high; + this.open = open; + this.close = close; + } + + public double getLow() { + return low; + } + + public void setLow(double low) { + this.low = low; + } + + public double getHigh() { + return high; + } + + public void setHigh(double high) { + this.high = high; + } + + public double getOpen() { + return open; + } + + public void setOpen(double open) { + this.open = open; + } + + public double getClose() { + return close; + } + + public void setClose(double close) { + this.close = close; + } + } +} 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 f836e89c..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 { @@ -66,11 +66,11 @@ public void setType(Type type) { * Wraps a normal XYSeries, inserting a new point at the beginning and end of the series. */ static class ExtrapolatedXYSeries implements XYSeries { - private final XY first; - private final XY last; + private final XYCoords first; + private final XYCoords last; private final XYSeries series; - public ExtrapolatedXYSeries(XYSeries series, XY first, XY last) { + public ExtrapolatedXYSeries(XYSeries series, XYCoords first, XYCoords last) { this.series = series; this.first = first; this.last = last; @@ -119,7 +119,7 @@ public String getTitle() { * @throws java.lang.IllegalArgumentException if pointsPerSegment is less than 2. */ @Override - public List interpolate(XYSeries series, Params params) { + public List interpolate(XYSeries series, Params params) { if (params.getPointPerSegment() < 2) { throw new IllegalArgumentException( "pointsPerSegment must be greater than 2, since 2 points is just the linear segment."); @@ -140,7 +140,7 @@ public List interpolate(XYSeries series, Params params) { double y1 = series.getY(0).doubleValue() - dy; // Actually create the start point from the extrapolated values. - XY start = new XY(x1, y1); + XYCoords start = new XYCoords(x1, y1); // Repeat for the end control point. int n = series.size() -1; @@ -148,7 +148,7 @@ public List interpolate(XYSeries series, Params params) { dy = series.getY(n).doubleValue() - series.getY(n - 1).doubleValue(); double xn = series.getX(n).doubleValue() + dx; double yn = series.getY(n).doubleValue() + dy; - XY end = new XY(xn, yn); + XYCoords end = new XYCoords(xn, yn); // TODO: figure out whether this extra control-point synthesis is // TODO: really necessary and either remove the above or fix the below. @@ -162,14 +162,14 @@ public List interpolate(XYSeries series, Params params) { ExtrapolatedXYSeries extrapolatedXYSeries = new ExtrapolatedXYSeries(series, start, end); // Dimension a result list of coordinates. - List result = new ArrayList<>(); + List result = new ArrayList<>(); // When looping, remember that each cycle requires 4 points, starting // with i and ending with i+3. So we don't loop through all the points. for (int i = 0; i < extrapolatedXYSeries.size() - 3; i++) { // Actually calculate the Catmull-Rom curve for one segment. - List points = interpolate(extrapolatedXYSeries, i, params); + List points = interpolate(extrapolatedXYSeries, i, params); // Since the middle points are added twice, once for each bordering // segment, we only add the 0 index result point for the first @@ -196,8 +196,8 @@ 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) { - List result = new ArrayList<>(); + protected List interpolate(XYSeries series, int index, Params params) { + List result = new ArrayList<>(); double[] x = new double[4]; double[] y = new double[4]; double[] time = new double[4]; @@ -226,13 +226,13 @@ public List interpolate(XYSeries series, int index, Params params) { } int segments = params.getPointPerSegment() - 1; - result.add(new XY(series.getX(index + 1), series.getY(index + 1))); + result.add(new XYCoords(series.getX(index + 1), series.getY(index + 1))); for (int i = 1; i < segments; i++) { double xi = interpolate(x, time, tstart + (i * (tend - tstart)) / segments); double yi = interpolate(y, time, tstart + (i * (tend - tstart)) / segments); - result.add(new XY(xi, yi)); + result.add(new XYCoords(xi, yi)); } - result.add(new XY(series.getX(index + 2), series.getY(index + 2))); + result.add(new XYCoords(series.getX(index + 2), series.getY(index + 2))); return result; } @@ -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/AxisValueLabelFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/EditableXYSeries.java similarity index 61% rename from androidplot-core/src/main/java/com/androidplot/xy/AxisValueLabelFormatter.java rename to androidplot-core/src/main/java/com/androidplot/xy/EditableXYSeries.java index 9e3b9ba1..3fbff1be 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/AxisValueLabelFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/EditableXYSeries.java @@ -1,34 +1,33 @@ -/* - * 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; - -public class AxisValueLabelFormatter { - //private Paint textPaint; - private int color; - - public AxisValueLabelFormatter(int color) { - this.color = color; - } - - public int getColor() { - return color; - } - - public void setColor(int color) { - this.color = color; - } -} +/* + * 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; + +/** + * An {@link XYSeries} that exposes methods to set values and resize + */ +public interface EditableXYSeries extends XYSeries { + + void setX(Number x, int index); + void setY(Number y, int index); + + /** + * Resize to accommodate the specified number of x/y pairs. If elements must be droped, those + * at the highest iVal should be removed first. + * @param size + */ + void resize(int size); +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/Estimator.java b/androidplot-core/src/main/java/com/androidplot/xy/Estimator.java new file mode 100644 index 00000000..abf3de8a --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/Estimator.java @@ -0,0 +1,10 @@ +package com.androidplot.xy; + +/** + * Base for all estimation management schemes. + */ +public abstract class Estimator { + + public abstract void run(XYPlot plot, XYSeriesBundle sf); + +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java new file mode 100644 index 00000000..df070325 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java @@ -0,0 +1,169 @@ +/* + * Copyright 2016 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.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.ArrayList; +import java.util.List; + +/** + * A faster implementation of of {@link LineAndPointRenderer}. For performance reasons, has these constraints: + * - Interpolation is not supported + * - Does not draw fill + * - Does not support null values + * @since 1.2.0 + */ +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<>(); + public FastLineAndPointRenderer(XYPlot plot) { + super(plot); + } + + @Override + protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) { + + segmentOffsets.clear(); + segmentLengths.clear(); + + final int numPoints = series.size() * 2; + if(points == null || points.length != numPoints) { + // only allocate when necessary: + points = new float[series.size()*2]; + } + + 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); + + if (y != null && x != null) { + if(isLastPointNull) { + segmentOffsets.add(j); + segmentLen = 0; + isLastPointNull = false; + } + + 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: + if(i == series.size()-1) { + segmentLengths.add(segmentLen); + } + } else if(!isLastPointNull) { + segmentLengths.add(segmentLen); + isLastPointNull = true; + } + } + + // draw segments + if(formatter.linePaint != null || formatter.vertexPaint != null) { + for (int i = 0; i < segmentOffsets.size(); i++) { + final int len = segmentLengths.get(i); + final int offset = segmentOffsets.get(i); + drawSegment(canvas, points, offset, len, formatter); + } + } + } + + protected void drawSegment(@NonNull Canvas canvas, + @NonNull float[] points, + int offset, + int len, + Formatter formatter) { + if(formatter.linePaint != null) { + // draw lines: + 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); + canvas.drawLines(points, offset + 2, len - 2, formatter.linePaint); + } else { + canvas.drawLines(points, offset, len, formatter.linePaint); + canvas.drawLines(points, offset + 2, len - 4, formatter.linePaint); + } + } + } + + if(formatter.vertexPaint != null) { + // draw vertices: + canvas.drawPoints(points, offset, len, formatter.vertexPaint); + } + } + + @Override + 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()); + } + } + + /** + * Formatter designed to work in tandem with {@link AdvancedLineAndPointRenderer}. + * @since 0.9.9 + */ + public static class Formatter extends LineAndPointFormatter { + + public Formatter(Integer lineColor, Integer vertexColor, PointLabelFormatter plf) { + super(lineColor, vertexColor, null, plf); + } + + @Override + protected void initLinePaint(Integer lineColor) { + super.initLinePaint(lineColor); + + // disable anti-aliasing by default: + getLinePaint().setAntiAlias(false); + } + + @Override + public Class getRendererClass() { + return FastLineAndPointRenderer.class; + } + + @Override + public SeriesRenderer doGetRendererInstance(XYPlot plot) { + return new FastLineAndPointRenderer(plot); + } + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java new file mode 100644 index 00000000..de5d534b --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java @@ -0,0 +1,19 @@ +package com.androidplot.xy; + +/** + * 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 { + + /** + * TIP: You can use {@link RectRegion#union(Number, Number)} during + * to keep a running tally of min/max values when iterating. + * @return A {@link RectRegion} representing the min/max values that currently exist this series. + */ + RectRegion minMax(); +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java new file mode 100644 index 00000000..36ff371e --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java @@ -0,0 +1,89 @@ +package com.androidplot.xy; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.androidplot.util.FastNumber; + +import java.util.ArrayList; +import java.util.List; + +/** + * An efficient implementation of {@link EditableXYSeries} intended for use cases where + * the total number of points visible is known ahead of time and is fairly static. + * + * {@link #resize(int)} may be used to resize the series when necessary, however it is a slow + * operation and should be avoided as much as possible. + * + */ +public class FixedSizeEditableXYSeries implements EditableXYSeries { + + @NonNull + private List xVals = new ArrayList<>(); + + @NonNull + private List yVals = new ArrayList<>(); + private String title; + + public FixedSizeEditableXYSeries(String title, int size) { + setTitle(title); + resize(size); + } + + @Override + public void setX(@Nullable Number x, int index) { + xVals.set(index, FastNumber.orNull(x)); + } + + @Override + public void setY(@Nullable Number y, int index) { + yVals.set(index, FastNumber.orNull(y)); + } + + /** + * May be used to dynamically resize the series. This is a relatively slow operation, especially + * as size increases so care should be taken to avoid unnecessary usage. + * @param size + */ + @Override + public void resize(int size) { + resize(xVals, size); + resize(yVals, size); + } + + protected void resize(@NonNull List list, int size) { + if (size > list.size()) { + while (list.size() < size) { + list.add(null); + } + } else if (size < list.size()) { + while (list.size() > size) { + list.remove(list.size() - 1); + } + } + } + + @Override + public String getTitle() { + return this.title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public int size() { + return xVals.size(); + } + + @Override + public Number getX(int index) { + return xVals.get(index); + } + + @Override + public Number getY(int index) { + return yVals.get(index); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java new file mode 100644 index 00000000..503d624d --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 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.RectF; +import android.util.Log; +import com.androidplot.ui.RenderStack; +import com.androidplot.ui.SeriesBundle; + +import java.util.List; + +/** + * Renders data to an XYPlot that potentially contains more than a single yVal per index, or in other + * words data with potentially more than two dimensions. + * Examples of such data are bar plot groups and candlestick charts. + * @since 0.9.7 + */ +public abstract class GroupRenderer> + extends XYSeriesRenderer { + + private static final String TAG = GroupRenderer.class.getName(); + + public GroupRenderer(XYPlot plot) { + super(plot); + } + + @Override + protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, + FormatterType formatter, RenderStack stack) { + + + // get all the data associated with this renderer: + List> sfList = getSeriesAndFormatterList(); + + // no data to render so exit: + if(sfList == null) { + return; + } + + int seriesLength = sfList.get(0).getSeries().size(); + + // make sure all associated series are the same length: + for(int i = 1; i < sfList.size(); i++) { + if(sfList.get(i).getSeries().size() != seriesLength) { + // series sizes are irregular so don't try to render: + Log.w(TAG, getClass() + ": " + "not all associated series are of same size."); + return; + } + } + + + // this renderer uses special element-by-element z-indexing that results in + // all series associated with the renderer being rendered in a single pass, so + // we need to exclude the rest of the series on the render stack from being redrawn later: + stack.disable(getClass()); + + onRender(canvas, plotArea, sfList, seriesLength, stack); + } + + /** + * + * @param canvas + * @param plotArea + * @param sfList + * @param stack + */ + public abstract void onRender(Canvas canvas, RectF plotArea, List> sfList, int size, RenderStack stack); +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/InterpolationParams.java b/androidplot-core/src/main/java/com/androidplot/xy/InterpolationParams.java index d080efbd..4345947b 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/InterpolationParams.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/InterpolationParams.java @@ -21,5 +21,5 @@ */ public interface InterpolationParams { - public Class getInterpolatorClass(); + Class getInterpolatorClass(); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/Interpolator.java b/androidplot-core/src/main/java/com/androidplot/xy/Interpolator.java index bf4ff11b..7ead10e7 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/Interpolator.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/Interpolator.java @@ -24,5 +24,5 @@ public interface Interpolator { - List interpolate(XYSeries series, ParamsType params); + List interpolate(XYSeries series, ParamsType params); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java b/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java new file mode 100644 index 00000000..820f2250 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java @@ -0,0 +1,106 @@ +package com.androidplot.xy; + +import android.util.*; + +/** + * Adapted from: + * https://github.com/drcrane/downsample + * + * Note that this implementation does not yet support null values. + * + * Basic usage example: + *

+ * {@code
+ * // An instance of any implementation of XYSeries; SimpleXYSeries, etc:
+ * XYSeries origalSeries = ...;
+ *
+ * // Sampled series with half the resolution of the
+ * EditableXYSeries sampledSeries = new FixedSizeEditableXYSeries(
+ * origalSeries.getTitle(), origalSeries.size() / 2);
+ *
+ * // does the actual sampling:
+ * new LTTBSampler().run(origalSeries, sampledSeries);
+ * }
+ * 
+ */ +public class LTTBSampler implements Sampler { + + public RectRegion run(XYSeries rawData, EditableXYSeries sampled) { + RectRegion bounds = new RectRegion(); + final int threshold = sampled.size(); + final int dataLength = rawData.size(); + final int startIndex = 0; + + if (threshold >= dataLength || threshold == 0) { + //return data; // Nothing to do + // TODO: set flag to return raw data + throw new RuntimeException("Shouldnt be here!"); + } + + int sampledIndex = 0; + // Bucket size. Leave room for start and end data points + final double bucketSize = (double) (dataLength - 2) / (threshold - 2); + int a = 0; // Initially a is the first point in the triangle + int nextA = 0; + setSample(rawData, sampled, a + startIndex, sampledIndex, bounds); + sampledIndex++; + for (int i = 0; i < threshold - 2; i++) { + // Calculate point average for next bucket (containing c) + double pointCX = 0; + double pointCY = 0; + int pointCStart = (int) Math.floor((i + 1) * bucketSize) + 1; + int pointCEnd = (int) Math.floor((i + 2) * bucketSize) + 1; + pointCEnd = pointCEnd < dataLength ? pointCEnd : dataLength; + final int pointCSize = pointCEnd - pointCStart; + for (; pointCStart < pointCEnd; pointCStart++) { + if(rawData.getX(pointCStart + startIndex) != null) { + pointCX += rawData.getX(pointCStart + startIndex).doubleValue(); + } + + if(rawData.getY(pointCStart + startIndex) != null) { + pointCY += rawData.getY(pointCStart + startIndex).doubleValue(); + } + } + pointCX /= pointCSize; + pointCY /= pointCSize; + double pointAX = rawData.getX(a + startIndex).doubleValue(); + double pointAY = rawData.getY(a + startIndex).doubleValue(); + // Get the range for bucket b + int pointBStart = (int) Math.floor((i + 0) * bucketSize) + 1; + final int pointBEnd = (int) Math.floor((i + 1) * bucketSize) + 1; + double maxArea = -1; + XYCoords maxAreaPoint = null; + for (; pointBStart < pointBEnd; pointBStart++) { + final double area = Math.abs((pointAX - pointCX) * (rawData.getY(pointBStart + startIndex) + .doubleValue() - pointAY) - (pointAX - rawData.getX(pointBStart + startIndex) + .doubleValue()) + * (pointCY - pointAY)) * 0.5; + if (area > maxArea) { + if(rawData.getY(pointBStart + startIndex) == null) { + Log.i("LTTB", "Null value encountered in raw data at index: " + pointBStart); + } + maxArea = area; + maxAreaPoint = new XYCoords(rawData.getX(pointBStart + startIndex), + rawData.getY(pointBStart + startIndex)); + nextA = pointBStart; // Next a is this b + } + } + setSample(sampled, maxAreaPoint.x, maxAreaPoint.y, sampledIndex, bounds); + sampledIndex++; + a = nextA; // This a is the next a (chosen b) + } + setSample(rawData, sampled, (dataLength + startIndex) - 1, sampledIndex, bounds); + sampledIndex++; + return bounds; + } + + protected void setSample(XYSeries raw, EditableXYSeries sampled, int rawIndex, int sampleIndex, RectRegion bounds) { + setSample(sampled, raw.getX(rawIndex), raw.getY(rawIndex), sampleIndex, bounds); + } + + protected void setSample(EditableXYSeries sampled, Number x, Number y, int sampleIndex, RectRegion bounds) { + bounds.union(x, y); + sampled.setX(x, sampleIndex); + sampled.setY(y, sampleIndex); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java index f03e3e26..f33501d2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java @@ -16,6 +16,7 @@ package com.androidplot.xy; +import android.content.*; import android.graphics.Color; import android.graphics.Paint; import com.androidplot.ui.SeriesRenderer; @@ -30,14 +31,6 @@ public class LineAndPointFormatter extends XYSeriesFormatter private static final float DEFAULT_LINE_STROKE_WIDTH_DP = 1.5f; private static final float DEFAULT_VERTEX_STROKE_WIDTH_DP = 4.5f; - // default implementation prints point's yVal: - private PointLabeler pointLabeler = new PointLabeler() { - @Override - public String getLabel(XYSeries series, int index) { - return series.getY(index) + ""; - } - }; - public FillDirection getFillDirection() { return fillDirection; } @@ -56,10 +49,9 @@ public void setFillDirection(FillDirection fillDirection) { protected Paint vertexPaint; protected Paint fillPaint; protected InterpolationParams interpolationParams; - private PointLabelFormatter pointLabelFormatter; - { - initLinePaint(Color.BLACK); + public LineAndPointFormatter(Context context, int xmlCfgId) { + super(context, xmlCfgId); } /** @@ -69,12 +61,13 @@ public LineAndPointFormatter() { this(Color.RED, Color.GREEN, Color.BLUE, null); } - public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, PointLabelFormatter plf) { + public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, + PointLabelFormatter plf) { this(lineColor, vertexColor, fillColor, plf, FillDirection.BOTTOM); } - public LineAndPointFormatter(Integer lineColor, Integer vertexColor, - Integer fillColor, PointLabelFormatter plf, FillDirection fillDir) { + public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, + PointLabelFormatter plf, FillDirection fillDir) { initLinePaint(lineColor); initVertexPaint(vertexColor); initFillPaint(fillColor); @@ -88,7 +81,7 @@ public Class getRendererClass() { } @Override - public SeriesRenderer getRendererInstance(XYPlot plot) { + public SeriesRenderer doGetRendererInstance(XYPlot plot) { return new LineAndPointRenderer(plot); } @@ -127,20 +120,23 @@ protected void initFillPaint(Integer fillColor) { } /** - * Enables the shadow layer on linePaint and shadowPaint by calling - * setShadowLayer() with preset values. + * + * @return True if linePaint has been set, false otherwise. */ - public void enableShadows() { - linePaint.setShadowLayer(1, 3, 3, Color.BLACK); - vertexPaint.setShadowLayer(1, 3, 3, Color.BLACK); - } - - public void disableShadows() { - linePaint.setShadowLayer(0, 0, 0, Color.BLACK); - vertexPaint.setShadowLayer(0, 0, 0, Color.BLACK); + public boolean hasLinePaint() { + return linePaint != null; } + /** + * Get the {@link Paint} used to draw lines. Will instantiate and a new default instance + * if it is currently null. To run whether or not line paint has been set, use + * {@link #hasLinePaint()}. + * @return + */ public Paint getLinePaint() { + if(linePaint == null) { + initLinePaint(Color.TRANSPARENT); + } return linePaint; } @@ -148,7 +144,24 @@ public void setLinePaint(Paint linePaint) { this.linePaint = linePaint; } + /** + * + * @return True if vertexPaint has been set, false otherwise. + */ + public boolean hasVertexPaint() { + return vertexPaint != null; + } + + /** + * Get the {@link Paint} used to draw vertices (points). Will instantiate and a new default instance + * if it is currently null. To run whether or not vertex paint has been set, use + * {@link #hasVertexPaint()}. + * @return + */ public Paint getVertexPaint() { + if(vertexPaint == null) { + initVertexPaint(Color.TRANSPARENT); + } return vertexPaint; } @@ -156,7 +169,23 @@ public void setVertexPaint(Paint vertexPaint) { this.vertexPaint = vertexPaint; } + /** + * + * @return True if fillPaint has been set, false otherwise. + */ + public boolean hasFillPaint() { + return fillPaint != null; + } + /** + * Get the {@link Paint} used to fill series areas. Will instantiate and a new default instance + * if it is currently null. To run whether or not fill paint has been set, use + * {@link #hasFillPaint()}. + * @return + */ public Paint getFillPaint() { + if(fillPaint == null) { + initFillPaint(Color.TRANSPARENT); + } return fillPaint; } @@ -164,22 +193,6 @@ public void setFillPaint(Paint fillPaint) { this.fillPaint = fillPaint; } - public PointLabelFormatter getPointLabelFormatter() { - return pointLabelFormatter; - } - - public void setPointLabelFormatter(PointLabelFormatter pointLabelFormatter) { - this.pointLabelFormatter = pointLabelFormatter; - } - - public PointLabeler getPointLabeler() { - return pointLabeler; - } - - public void setPointLabeler(PointLabeler pointLabeler) { - this.pointLabeler = pointLabeler; - } - public InterpolationParams getInterpolationParams() { return interpolationParams; } 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 29a37b85..f50d6186 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java @@ -16,28 +16,53 @@ package com.androidplot.xy; -import android.graphics.*; -import com.androidplot.exception.PlotRenderException; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.RectF; + +import com.androidplot.Plot; +import com.androidplot.PlotListener; +import com.androidplot.Region; import com.androidplot.ui.RenderStack; -import com.androidplot.util.ValPixConverter; +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 * be rendered. */ -public class LineAndPointRenderer extends XYSeriesRenderer { +public class LineAndPointRenderer extends XYSeriesRenderer { protected static final int ZERO = 0; protected static final int ONE = 1; + 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); } @@ -50,11 +75,11 @@ public void doDrawLegendIcon(Canvas canvas, RectF rect, LineAndPointFormatter fo if(formatter.getFillPaint() != null) { canvas.drawRect(rect, formatter.getFillPaint()); } - if(formatter.getLinePaint() != null) { + if(formatter.hasLinePaint()) { canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint()); } - if(formatter.getVertexPaint() != null) { + if(formatter.hasVertexPaint()) { canvas.drawPoint(centerX, centerY, formatter.getVertexPaint()); } } @@ -68,38 +93,86 @@ protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) { path.lineTo(thisPoint.x, thisPoint.y); } + /** + * 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); + } + + if(pointsCache.size() < seriesSize) { + while(pointsCache.size() < seriesSize) { + pointsCache.add(null); + } + } else if(pointsCache.size() > seriesSize) { + while(pointsCache.size() > seriesSize) { + pointsCache.remove(0); + } + } + return pointsCache; + } + + protected void cullPointsCache() { + for(XYSeries series : pointsCaches.keySet()) { + if(!getPlot().getRegistry().contains(series, LineAndPointFormatter.class)) { + pointsCaches.remove(series); + } + } + } protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) { PointF thisPoint; PointF lastPoint = null; PointF firstPoint = null; - Paint linePaint = formatter.getLinePaint(); - Path path = null; - ArrayList points = new ArrayList<>(series.size()); - for (int i = 0; i < series.size(); i++) { - Number y = series.getY(i); - Number x = series.getX(i); + path.reset(); + final List points = getPointsCache(series); + + int iStart = 0; + 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() + 1; + if(iEnd < series.size() - 1) { + iEnd++; + } + } + for (int i = iStart; i < iEnd; i++) { + final Number y = series.getY(i); + final Number x = series.getX(i); + PointF iPoint = points.get(i); if (y != null && x != null) { - thisPoint = ValPixConverter.valToPix( - x, y, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - points.add(thisPoint); + if(iPoint == null) { + iPoint = new PointF(); + points.set(i, iPoint); + } + thisPoint = iPoint; + getPlot().getBounds().transformScreen(thisPoint, x, y, plotArea); } else { thisPoint = null; + iPoint = null; + points.set(i, iPoint); } // don't need to do any of this if the line isnt going to be drawn: - if(linePaint != null && formatter.getInterpolationParams() == null) { + if(formatter.hasLinePaint() && formatter.getInterpolationParams() == null) { if (thisPoint != null) { // record the first point of the new Path if (firstPoint == null) { - path = new Path(); + path.reset(); firstPoint = thisPoint; // create our first point at the bottom/x position so filling will look good: @@ -120,14 +193,15 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn } } } - if(linePaint != null) { + + if(formatter.hasLinePaint()) { if(formatter.getInterpolationParams() != null) { - List interpolatedPoints = getInterpolator( + List interpolatedPoints = getInterpolator( formatter.getInterpolationParams()).interpolate(series, formatter.getInterpolationParams()); firstPoint = convertPoint(interpolatedPoints.get(ZERO), plotArea); lastPoint = convertPoint(interpolatedPoints.get(interpolatedPoints.size()-ONE), plotArea); - path = new Path(); + path.reset(); path.moveTo(firstPoint.x, firstPoint.y); for(int i = 1; i < interpolatedPoints.size(); i++) { thisPoint = convertPoint(interpolatedPoints.get(i), plotArea); @@ -139,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); } /** @@ -157,37 +231,32 @@ protected Interpolator getInterpolator(InterpolationParams params) { } } - protected PointF convertPoint(XY coord, RectF plotArea) { - return ValPixConverter.valToPix( - coord.x.doubleValue(), - coord.y.doubleValue(), - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); + 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) { - Paint vertexPaint = formatter.getVertexPaint(); - PointLabelFormatter plf = formatter.getPointLabelFormatter(); - if (vertexPaint != null || plf != null) { - int i = 0; - for (PointF p : points) { - PointLabeler pointLabeler = formatter.getPointLabeler(); - - // if vertexPaint is available, draw vertex: - if (vertexPaint != null) { - canvas.drawPoint(p.x, p.y, formatter.getVertexPaint()); - } + if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) { + 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(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 textPaint and pointLabeler are available, draw point's text label: - if (plf != null && pointLabeler != null) { - 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++; } } } @@ -210,49 +279,47 @@ protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF first path.close(); break; case RANGE_ORIGIN: - float originPix = ValPixConverter.valToPix( - getPlot().getRangeOrigin().doubleValue(), - getPlot().getCalculatedMinY().doubleValue(), - getPlot().getCalculatedMaxY().doubleValue(), - plotArea.height(), - true); - originPix += plotArea.top; - + float originPix = (float) getPlot().getBounds().getxRegion() + .transform(getPlot().getRangeOrigin() + .doubleValue(), plotArea.top, plotArea.bottom, true); path.lineTo(lastPoint.x, originPix); path.lineTo(firstPoint.x, originPix); path.close(); break; default: - throw new UnsupportedOperationException("Fill direction not yet implemented: " + formatter.getFillDirection()); + throw new UnsupportedOperationException( + "Fill direction not yet implemented: " + formatter.getFillDirection()); } if (formatter.getFillPaint() != null) { canvas.drawPath(path, formatter.getFillPaint()); } - // draw any visible regions on top of the base region: - double minX = getPlot().getCalculatedMinX().doubleValue(); - double maxX = getPlot().getCalculatedMaxX().doubleValue(); - double minY = getPlot().getCalculatedMinY().doubleValue(); - double maxY = getPlot().getCalculatedMaxY().doubleValue(); + final RectRegion bounds = getPlot().getBounds(); + final RectRegion plotRegion = new RectRegion(plotArea); // draw each region: - for (RectRegion r : RectRegion.regionsWithin(formatter.getRegions().elements(), minX, maxX, minY, maxY)) { - XYRegionFormatter f = formatter.getRegionFormatter(r); - RectF regionRect = r.getRectF(plotArea, minX, maxX, minY, maxY); - if (regionRect != null) { - try { - canvas.save(Canvas.ALL_SAVE_FLAG); - canvas.clipPath(path); - canvas.drawRect(regionRect, f.getPaint()); - } finally { - canvas.restore(); + for (RectRegion thisRegion : bounds.intersects(formatter.getRegions().elements())) { + XYRegionFormatter regionFormatter = formatter.getRegionFormatter(thisRegion); + RectRegion thisRegionTransformed = bounds + .transform(thisRegion, plotRegion, false, true); + thisRegionTransformed.intersect(plotRegion); + if(thisRegion.isFullyDefined()) { + RectF thisRegionRectF = thisRegionTransformed.asRectF(); + if (thisRegionRectF != null) { + try { + canvas.save(); + canvas.clipPath(path); + canvas.drawRect(thisRegionRectF, regionFormatter.getPaint()); + } finally { + canvas.restore(); + } } } } // finally we draw the outline path on top of everything else: - if(formatter.getLinePaint() != null) { + if(formatter.hasLinePaint()) { canvas.drawPath(outlinePath, formatter.getLinePaint()); } 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 new file mode 100644 index 00000000..820e4330 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java @@ -0,0 +1,33 @@ +package com.androidplot.xy; + +/** + * An implementation of {@link XYSeries} that gives hints to it's renderer about the order + * of the data being rendered. + */ +public interface OrderedXYSeries extends XYSeries { + + enum XOrder { + /** + * XVals are in strict ascending order such that: + * x(i) < x(i+1) == true + */ + ASCENDING, + + /** + * XVals are in strict descending order such that: + * x(i) > x(i+1) == true + */ + DESCENDING, + + /** + * XVals appear in no particular order. + */ + NONE + } + + /** + * The order of XVals as they appear in this series. + * @return + */ + XOrder getXOrder(); +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java new file mode 100644 index 00000000..9cd80847 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java @@ -0,0 +1,535 @@ +package com.androidplot.xy; + +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.*; + +/** + * Enables basic pan/zoom touch behavior for an {@link XYPlot}. + * By default boundaries there are no boundaries imposed on scrolling and zooming. You can provide these boundaries + * on your {@link XYPlot} using {@link XYPlot#getOuterLimits()}. + * TODO: zoom using dynamic center point + * TODO: stretch both mode + */ +public class PanZoom implements View.OnTouchListener { + + protected static final float MIN_DIST_2_FING = 5f; + protected static final int FIRST_FINGER = 0; + protected static final int SECOND_FINGER = 1; + + private XYPlot plot; + private Pan pan; + private Zoom zoom; + + private ZoomLimit zoomLimit; + private boolean isEnabled = true; + + private DragState dragState = DragState.NONE; + private PointF firstFingerPos; + + // 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 { + NONE, + ONE_FINGER, + TWO_FINGERS + } + + public enum Pan { + NONE, + HORIZONTAL, + VERTICAL, + BOTH + } + + public enum Zoom { + + /** + * Comletely disable panning + */ + NONE, + + /** + * Zoom on the horizontal axis only + */ + STRETCH_HORIZONTAL, + + /** + * Zoom on the vertical axis only + */ + STRETCH_VERTICAL, + + /** + * Zoom on the vertical axis by the vertical distance between each finger, while zooming + * on the horizontal axis by the horizantal distance between each finger. + */ + STRETCH_BOTH, + + /** + * Zoom each axis by the same amount, specifically the total distance between each finger. + */ + SCALE + } + + /** + * 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, ZoomLimit)} for finer grain control of this behavior. + * @param plot + * @return + */ + public static PanZoom attach(@NonNull XYPlot plot) { + return attach(plot, Pan.BOTH, Zoom.SCALE); + } + + /** + * 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; + } + + public boolean isEnabled() { + return isEnabled; + } + + public void setEnabled(boolean enabled) { + isEnabled = enabled; + } + + @Override + public boolean onTouch(final View view, final MotionEvent event) { + boolean isConsumed = false; + if (delegate != null) { + isConsumed = delegate.onTouch(view, event); + } + if (isEnabled() && !isConsumed) { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: // start gesture + firstFingerPos = new PointF(event.getX(), event.getY()); + dragState = DragState.ONE_FINGER; + break; + case MotionEvent.ACTION_POINTER_DOWN: // second finger + { + setFingersRect(fingerDistance(event)); + // the distance run is done to avoid false alarms + if (getFingersRect().width() > MIN_DIST_2_FING || getFingersRect().width() < -MIN_DIST_2_FING) { + dragState = DragState.TWO_FINGERS; + } + break; + } + case MotionEvent.ACTION_POINTER_UP: // end zoom + dragState = DragState.NONE; + break; + + case MotionEvent.ACTION_MOVE: + if (dragState == DragState.ONE_FINGER) { + pan(event); + } else if (dragState == DragState.TWO_FINGERS) { + zoom(event); + } + break; + + case MotionEvent.ACTION_UP: + reset(); + break; + } + } + // we're forced to consume the event here as not consuming it will prevent future calls: + return true; + } + + /** + * Calculates the distance between two finger motion events. + * @param firstFingerX + * @param firstFingerY + * @param secondFingerX + * @param secondFingerY + * @return + */ + protected RectF fingerDistance(float firstFingerX, float firstFingerY, float secondFingerX, float secondFingerY) { + final float left = firstFingerX > secondFingerX ? secondFingerX : firstFingerX; + final float right = firstFingerX > secondFingerX ? firstFingerX : secondFingerX; + final float top = firstFingerY > secondFingerY ? secondFingerY : firstFingerY; + final float bottom = firstFingerY > secondFingerY ? firstFingerY : secondFingerY; + return new RectF(left, top, right, bottom); + } + + /** + * Calculates the distance between two finger motion events. + * @param evt + * @return + */ + protected RectF fingerDistance(final MotionEvent evt) { + return fingerDistance( + evt.getX(FIRST_FINGER), + evt.getY(FIRST_FINGER), + evt.getX(SECOND_FINGER), + evt.getY(SECOND_FINGER)); + } + + protected void pan(final MotionEvent motionEvent) { + if (pan == Pan.NONE) { + return; + } + + final PointF oldFirstFinger = firstFingerPos; //save old position of finger + firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position + if (EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) { + Region newBounds = new Region(); + calculatePan(oldFirstFinger, newBounds, true); + adjustDomainBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); + } + if (EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) { + Region newBounds = new Region(); + calculatePan(oldFirstFinger, newBounds, false); + adjustRangeBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); + } + + plot.redraw(); + } + + protected void calculatePan(final PointF oldFirstFinger, Region bounds, final boolean horizontal) { + final float offset; + // multiply the absolute finger movement for a factor. + // the factor is dependent on the calculated min and max + if (horizontal) { + bounds.setMinMax(plot.getBounds().getxRegion()); + offset = (oldFirstFinger.x - firstFingerPos.x) * + ((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getWidth()); + } else { + bounds.setMinMax(plot.getBounds().getyRegion()); + offset = -(oldFirstFinger.y - firstFingerPos.y) * + ((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getHeight()); + } + // move the calculated offset + bounds.setMin(bounds.getMin().floatValue() + offset); + bounds.setMax(bounds.getMax().floatValue() + offset); + + //get the distance between max and min + final float diff = bounds.length().floatValue(); + + //run if we reached the limit of panning + if (horizontal && plot.getOuterLimits().getxRegion().isDefined()) { + if (bounds.getMin().floatValue() < plot.getOuterLimits().getMinX().floatValue()) { + bounds.setMin(plot.getOuterLimits().getMinX()); + bounds.setMax(bounds.getMin().floatValue() + diff); + } + if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxX().floatValue()) { + bounds.setMax(plot.getOuterLimits().getMaxX()); + bounds.setMin(bounds.getMax().floatValue() - diff); + } + } else if(plot.getOuterLimits().getyRegion().isDefined()) { + if (bounds.getMin().floatValue() < plot.getOuterLimits().getMinY().floatValue()) { + bounds.setMin(plot.getOuterLimits().getMinY()); + bounds.setMax(bounds.getMin().floatValue() + diff); + } + if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxY().floatValue()) { + bounds.setMax(plot.getOuterLimits().getMaxY()); + bounds.setMin(bounds.getMax().floatValue() - diff); + } + } + } + + protected boolean isValidScale(float scale) { + return !Float.isInfinite(scale) + && !Float.isNaN(scale) + && (!(scale > -0.001) || !(scale < 0.001)); + } + + protected void zoom(final MotionEvent motionEvent) { + if (zoom == Zoom.NONE) { + return; + } + final RectF oldFingersRect = getFingersRect(); + final RectF newFingersRect = fingerDistance(motionEvent); + setFingersRect(newFingersRect); + if(oldFingersRect == null || RectFUtils.areIdentical(oldFingersRect, newFingersRect)) { + // zooming gesture has not happened yet so skip: + return; + } + RectF newRect = new RectF(); + + float scaleX = 1; + float scaleY = 1; + switch (zoom) { + case STRETCH_HORIZONTAL: + scaleX = oldFingersRect.width() / getFingersRect().width(); + if (!isValidScale(scaleX)) { + return; + } + break; + case STRETCH_VERTICAL: + scaleY = oldFingersRect.height() / getFingersRect().height(); + if (!isValidScale(scaleY)) { + return; + } + break; + case STRETCH_BOTH: + scaleX = oldFingersRect.width() / getFingersRect().width(); + scaleY = oldFingersRect.height() / getFingersRect().height(); + if (!isValidScale(scaleX) || !isValidScale(scaleY)) { + return; + } + break; + case SCALE: + float sc1 = (float) Math.hypot(oldFingersRect.height(), oldFingersRect.width()); + float sc2 = (float) Math.hypot(getFingersRect().height(), getFingersRect().width()); + float sc = sc1 / sc2; + scaleX = sc; + scaleY = sc; + if (!isValidScale(scaleX) || !isValidScale(scaleY)) { + return; + } + break; + } + + if (EnumSet.of( + Zoom.STRETCH_HORIZONTAL, + Zoom.STRETCH_BOTH, + Zoom.SCALE).contains(zoom)) { + calculateZoom(newRect, scaleX, true); + adjustDomainBoundary(newRect.left, newRect.right, BoundaryMode.FIXED); + } + if (EnumSet.of( + Zoom.STRETCH_VERTICAL, + Zoom.STRETCH_BOTH, + Zoom.SCALE).contains(zoom)) { + calculateZoom(newRect, scaleY, false); + adjustRangeBoundary(newRect.top, newRect.bottom, BoundaryMode.FIXED); + } + plot.redraw(); + } + + /** + * + * @param newRect RectF into which zoom calculation results should be placed. + * @param scale + * @param isHorizontal + */ + protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) { + final float calcMax; + final float span; + final RectRegion bounds = plot.getBounds(); + if (isHorizontal) { + calcMax = bounds.getMaxX().floatValue(); + span = calcMax - bounds.getMinX().floatValue(); + } else { + calcMax = bounds.getMaxY().floatValue(); + span = calcMax - bounds.getMinY().floatValue(); + } + + final float midPoint = calcMax - (span / 2.0f); + float offset = span * scale / 2.0f; + final RectRegion limits = plot.getOuterLimits(); + + if (isHorizontal ) { + // 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()) { + if (newRect.left < limits.getMinX().floatValue()) { + newRect.left = limits.getMinX().floatValue(); + } + if (newRect.right > limits.getMaxX().floatValue()) { + newRect.right = limits.getMaxX().floatValue(); + } + } + } else { + // 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()) { + if (newRect.top < limits.getMinY().floatValue()) { + newRect.top = limits.getMinY().floatValue(); + } + if (newRect.bottom > limits.getMaxY().floatValue()) { + newRect.bottom = limits.getMaxY().floatValue(); + } + } + } + } + + public Pan getPan() { + return pan; + } + + public void setPan(Pan pan) { + this.pan = pan; + } + + public Zoom getZoom() { + return zoom; + } + + 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; + } + + /** + * Set a delegate to receive onTouch calls before this class does. If the delegate wishes + * to consume the event, it should return true, otherwise it should return false. Returning + * false will not prevent future onTouch events from filtering through the delegate as it normally + * would when attaching directly to an instance of {@link View}. + * @param delegate + */ + public void setDelegate(View.OnTouchListener delegate) { + this.delegate = delegate; + } + + public void reset() { + this.firstFingerPos = null; + setFingersRect(null); + this.setFingersRect(null); + } + + protected RectF getFingersRect() { + return fingersRect; + } + + protected void setFingersRect(RectF fingersRect) { + this.fingersRect = fingersRect; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java index 1d9fe520..4f5f762d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java @@ -16,10 +16,8 @@ package com.androidplot.xy; -import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.PointF; import com.androidplot.util.PixelUtils; public class PointLabelFormatter { @@ -52,7 +50,14 @@ public PointLabelFormatter(int textColor, float hOffset, float vOffset) { this.vOffset = vOffset; } + public boolean hasTextPaint() { + return textPaint != null; + } + public Paint getTextPaint() { + if(textPaint == null) { + initTextPaint(Color.TRANSPARENT); + } return textPaint; } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java b/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java index 2aeeceda..12504dff 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java @@ -16,7 +16,7 @@ package com.androidplot.xy; -public interface PointLabeler { +public interface PointLabeler { - public String getLabel(XYSeries series, int index); + String getLabel(SeriesType series, int index); } 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 48228448..0bb8988d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java @@ -18,8 +18,8 @@ import android.graphics.PointF; import android.graphics.RectF; -import com.androidplot.LineRegion; -import com.androidplot.util.ValPixConverter; + +import com.androidplot.Region; import java.util.ArrayList; import java.util.List; @@ -31,51 +31,148 @@ */ public class RectRegion { - LineRegion xLineRegion; - LineRegion yLineRegion; + Region xRegion; + Region yRegion; private String label; + public RectRegion() { + xRegion = new Region(); + yRegion = new Region(); + } + + public static RectRegion withDefaults(RectRegion defaults) { + if(defaults == null || !defaults.isFullyDefined()) { + throw new IllegalArgumentException("When specifying defaults, RectRegion param must contain no null values."); + } + + RectRegion r = new RectRegion(); + r.xRegion = Region.withDefaults(defaults.getxRegion()); + r.yRegion = Region.withDefaults(defaults.getyRegion()); + return r; + } + + public RectRegion(XYCoords min, XYCoords max) { + this(min.x, max.x, min.y, max.y); + } + /** - * * @param minX * @param maxX * @param minY * @param maxY */ public RectRegion(Number minX, Number maxX, Number minY, Number maxY, String label) { - xLineRegion = new LineRegion(minX, maxX); - yLineRegion = new LineRegion(minY, maxY); + xRegion = new Region(minX, maxX); + yRegion = new Region(minY, maxY); this.setLabel(label); } + public RectRegion(RectF rect) { + this(rect.left < rect.right ? rect.left : rect.right, + rect.right > rect.left ? rect.right : rect.left, + rect.bottom < rect.top ? rect.bottom : rect.top, + rect.top > rect.bottom ? rect.top : rect.bottom); + } + @SuppressWarnings("SameParameterValue") public RectRegion(Number minX, Number maxX, Number minY, Number maxY) { this(minX, maxX, minY, maxY, null); } - public boolean containsPoint(PointF point) { - throw new UnsupportedOperationException("Not yet implemented."); + public XYCoords transform(Number x, Number y, RectRegion region2, boolean flipX, boolean flipY) { + Number xx = xRegion.transform(x.doubleValue(), region2.xRegion, flipX); + Number yy = yRegion.transform(y.doubleValue(), region2.yRegion, flipY); + return new XYCoords(xx, yy); + } + + public XYCoords transform(Number x, Number y, RectRegion region2) { + return transform(x, y, region2, false, false); } - public boolean containsValue(Number x, Number y) { - throw new UnsupportedOperationException("Not yet implemented."); + public XYCoords transform(XYCoords value, RectRegion region2) { + return transform(value.x, value.y, region2); } - public boolean containsDomainValue(Number value) { - //return RectRegion.isBetween(value, minX, maxX); - return xLineRegion.contains(value); + /** + * Transform a region (r) from the current region space (this) into the specified one (r2) + * @param r The region to which the transformation applies + * @param r2 The region into which r is being transformed + * @return + */ + public RectRegion transform(RectRegion r, RectRegion r2, boolean flipX, boolean flipY) { + return new RectRegion( + transform(r.getMinX(), r.getMinY(), r2, flipX, flipY), + transform(r.getMaxX(), r.getMaxY(), r2, flipX, flipY) + ); } - public boolean containsRangeValue(Number value) { - //return RectRegion.isBetween(value, minY, maxY); - return yLineRegion.contains(value); + /** + * Convenience method to transform into screen coordinate space. Equivalent to invoking + * {@link #transform(Number, Number, RectF, boolean, boolean)} with + * flipX = false and flipY = true. + * @param x + * @param y + * @param region2 + * @return + */ + public PointF transformScreen(Number x, Number y, RectF region2) { + return transform(x, y, region2, false, true); + } + + public void transformScreen(PointF result, Number x, Number y, RectF region2) { + transform(result, x, y, region2, false, true); + } + + public void transform(PointF result, Number x, Number y, RectF region2, boolean flipX, boolean flipY) { + result.x = (float) xRegion.transform(x.doubleValue(), region2.left, region2.right, flipX); + result.y = (float) yRegion.transform(y.doubleValue(), region2.top, region2.bottom, flipY); + } + + public PointF transform(Number x, Number y, RectF region2, boolean flipX, boolean flipY) { + PointF result = new PointF(); + transform(result, x, y, region2, flipX, flipY); + return result; + } + + public PointF transformScreen(XYCoords value, RectF region2) { + return transform(value, region2, false, true); + } + + /** + * Convenience method to transform into screen coordinate space. Equivalent to invoking + * {@link #transform(XYCoords, RectF, boolean, boolean)} with flipX = false and flipY = true. + * @param value + * @param region2 + * @param flipX + * @param flipY + * @return + */ + public PointF transform(XYCoords value, RectF region2, boolean flipX, boolean flipY) { + return transform(value.x, value.y, region2, flipX, flipY); + } + + public void union(Number x, Number y) { + xRegion.union(x); + yRegion.union(y); + } + + /** + * Compares the input bounds xy min/max vals against this instance's current xy min/max vals. + * If the input.min is less than this.min then this.min will be set to input.min. + * If the input.max is greater than this.max then this.max will be set to input.max + * + * The result will always have equal or greater area than the inputs. + * @param input + */ + public void union(RectRegion input) { + xRegion.union(input.xRegion); + yRegion.union(input.yRegion); } public boolean intersects(RectRegion region) { return intersects(region.getMinX(), region.getMaxX(), region.getMinY(), region.getMaxY()); } - /** * Tests whether this region intersects the region defined by params. Use * null to represent infinity. Negative and positive infinity is implied by @@ -88,51 +185,40 @@ public boolean intersects(RectRegion region) { * @return */ public boolean intersects(Number minX, Number maxX, Number minY, Number maxY) { - return xLineRegion.intersects(minX, maxX) && yLineRegion.intersects(minY, maxY); + return xRegion.intersects(minX, maxX) && yRegion.intersects(minY, maxY); } - public boolean intersects(RectF region, Number visMinX, Number visMaxX, Number visMinY, Number visMaxY) { - - RectF thisRegion = getRectF(region, visMinX.doubleValue(), visMaxX.doubleValue(), - visMinY.doubleValue(), visMaxY.doubleValue()); - return RectF.intersects(thisRegion, region); + public RectF asRectF() { + return new RectF(getMinX().floatValue(), getMinY().floatValue(), + getMaxX().floatValue(), getMaxY().floatValue()); } - public RectF getRectF(RectF plotRect, Number visMinX, Number visMaxX, Number visMinY, Number visMaxY) { - PointF topLeftPoint = ValPixConverter.valToPix( - xLineRegion.getMinVal().doubleValue() != Double.NEGATIVE_INFINITY ? xLineRegion.getMinVal() : visMinX, - yLineRegion.getMaxVal().doubleValue() != Double.POSITIVE_INFINITY ? yLineRegion.getMaxVal() : visMaxY, - plotRect, - visMinX, - visMaxX, - visMinY, - visMaxY); - PointF bottomRightPoint = ValPixConverter.valToPix( - xLineRegion.getMaxVal().doubleValue() != Double.POSITIVE_INFINITY ? xLineRegion.getMaxVal() : visMaxX, - yLineRegion.getMinVal().doubleValue() != Double.NEGATIVE_INFINITY ? yLineRegion.getMinVal() : visMinY, - plotRect, - visMinX, - visMaxX, - visMinY, - visMaxY); - // TODO: figure out why the y-values are inverted - return new RectF(topLeftPoint.x, topLeftPoint.y, bottomRightPoint.x, bottomRightPoint.y); + /** + * The result of an intersect is always a RectRegion with an equal or smaller area. + * @param clippingBounds + */ + public void intersect(RectRegion clippingBounds) { + if(intersects(clippingBounds)) { + xRegion.intersect(clippingBounds.xRegion); + yRegion.intersect(clippingBounds.yRegion); + } else { + setMinY(null); + setMaxY(null); + setMinX(null); + setMaxX(null); + } } /** * Returns a list of XYRegions that either completely or partially intersect the area * defined by params. A null value for any parameter represents infinity / no boundary. * @param regions The list of regions to search through - * @param minX - * @param maxX - * @param minY - * @param maxY * @return */ - public static List regionsWithin(List regions, Number minX, Number maxX, Number minY, Number maxY) { - ArrayList intersectingRegions = new ArrayList(); - for(RectRegion r : regions) { - if(r.intersects(minX, maxX, minY, maxY)) { + public List intersects(List regions) { + ArrayList intersectingRegions = new ArrayList<>(); + for (RectRegion r : regions) { + if (r.intersects(getMinX(), getMaxX(), getMinY(), getMaxY())) { intersectingRegions.add(r); } } @@ -140,7 +226,6 @@ public static List regionsWithin(List regions, Number mi } /** - * * @return Width of this region, in native units */ public Number getWidth() { @@ -148,7 +233,6 @@ public Number getWidth() { } /** - * * @return Height of this region, in native units */ public Number getHeight() { @@ -161,40 +245,63 @@ 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()); } + public void set(Number minX, Number maxX, Number minY, Number maxY) { + setMinX(minX); + setMaxX(maxX); + setMinY(minY); + setMaxY(maxY); + } + + public boolean isMinXSet() { + return xRegion.isMinSet(); + } + public Number getMinX() { - return xLineRegion.getMinVal(); + return xRegion.getMin(); + } + + public void setMinX(Number minX) { + xRegion.setMin(minX); } - public void setMinX(double minX) { - xLineRegion.setMinVal(minX); + public boolean isMaxXSet() { + return xRegion.isMaxSet(); } public Number getMaxX() { - return xLineRegion.getMaxVal(); + return xRegion.getMax(); } public void setMaxX(Number maxX) { - xLineRegion.setMaxVal(maxX); + xRegion.setMax(maxX); + } + + public boolean isMinYSet() { + return yRegion.isMinSet(); } public Number getMinY() { - return yLineRegion.getMinVal(); + return yRegion.getMin(); } public void setMinY(Number minY) { - yLineRegion.setMinVal(minY); + yRegion.setMin(minY); + } + + public boolean isMaxYSet() { + return yRegion.isMaxSet(); } public Number getMaxY() { - return yLineRegion.getMaxVal(); + return yRegion.getMax(); } public void setMaxY(Number maxY) { - yLineRegion.setMaxVal(maxY); + yRegion.setMax(maxY); } public String getLabel() { @@ -204,4 +311,47 @@ public String getLabel() { public void setLabel(String label) { this.label = label; } + + public Region getxRegion() { + return xRegion; + } + + public void setxRegion(Region xRegion) { + this.xRegion = xRegion; + } + + public Region getyRegion() { + return yRegion; + } + + public void setyRegion(Region yRegion) { + this.yRegion = yRegion; + } + + /** + * + * @return True if both xRegion and yRegion are defined, false otherwise + */ + public boolean isFullyDefined() { + return xRegion.isDefined() && yRegion.isDefined(); + } + + /** + * True if this region contains the specified coordinates. + * @param x + * @param y + * @return + */ + public boolean contains(Number x, Number y) { + return getxRegion().contains(x) && getyRegion().contains(y); + } + + @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 new file mode 100644 index 00000000..f795536b --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java @@ -0,0 +1,216 @@ +package com.androidplot.xy; + +import com.androidplot.*; +import com.androidplot.util.SeriesUtils; + +import java.util.*; + +/** + * An implementation of {@link FastXYSeries} that samples its self into multiple levels to + * achieve faster rendering / zoom behavior. By default, uses {@link LTTBSampler} as + * it's sampling algorithm. Note that this algorithm does not yet support null values. + * + * Sampling behavior is controlled by two values: + * Ratio: A value greater than 1; controls the sampling ratio of each successive series. For example, + * a step of 2 would mean that each successive series contains 2x fewer points than the previous. + * + * Threshold: A value < the original series size; controls the lower limit at which point sampling should stop. + * For example, sampling a series with size 1000 given a ratio of 2 and a threshold of 100, + * three sampled resolutions will be generated: + * + * 500 - 2x sampling + * 250 - 4x sampling + * 125 - 8x sampling + * + */ +public class SampledXYSeries implements FastXYSeries, OrderedXYSeries { + private int threshold; + private Sampler algorithm = new LTTBSampler(); + + private XYSeries rawData; + + private List zoomLevels; + + private XYSeries activeSeries; + + private RectRegion bounds; + private Exception lastResamplingException; + + private final XOrder xOrder; + private float ratio; + + /** + * + * @param rawData + * @param xOrder If your data is in ascending or descending order, specifying it here speed up + * optimize render times. + * @param ratio The ratio used to determine the size of each new sampled series. Must be > 1. + * downsampled series until threshold is reached. + * @param threshold The desired size of the smallest sample series. Must be < rawData.size. + */ + public SampledXYSeries(XYSeries rawData, XOrder xOrder, float ratio, int threshold) { + this.rawData = rawData; + this.xOrder = xOrder; + this.setRatio(ratio); + this.setThreshold(threshold); + resample(); + } + + /** + * Generate a SampledXYSeries from the input series. + * @param rawData The original series to be downsampled + * @param ratio The ratio used to determine the size of each new sampled series. Must be > 1. + * downsampled series until threshold is reached. + * @param threshold The desired size of the smallest sample series. Must be < rawData.size. + */ + public SampledXYSeries(XYSeries rawData, float ratio, int threshold) { + this(rawData, SeriesUtils.getXYOrder(rawData), ratio, threshold); + } + + public void resample() { + bounds = null; + zoomLevels = new ArrayList<>(); + int t = (int) Math.ceil(rawData.size() / getRatio()); + List threads = new ArrayList<>(zoomLevels.size()); + while (t > threshold) { + final int thisThreshold = t; + final EditableXYSeries thisSeries = new FixedSizeEditableXYSeries(getTitle(), thisThreshold); + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + try { + // TODO: make bounds a param to prevent calculating on each setZoomFactor level: + RectRegion b = getAlgorithm() + .run(rawData, thisSeries); + if (bounds == null) { + bounds = b; + } + } catch(Exception ex) { + lastResamplingException = ex; + } + } + }, "Androidplot XY Series Sampler"); + getZoomLevels().add(thisSeries); + threads.add(thread); + thread.start(); + + t = (int) Math.ceil(t / getRatio()); + } + for(Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + if(lastResamplingException != null) { + throw new RuntimeException("Exception encountered during resampling", lastResamplingException); + } + + } + + protected List getZoomLevels() { + return this.zoomLevels; + } + + /** + * Set zoom factor; 2.5 = 2.5x zoom, 10.0 = 10x zoom etc. This method will set the zoom level + * to the closest available factor to the specified factor; a specified factor of 4.5x may result + * in an actual factor of 4x. + * @param factor + */ + public void setZoomFactor(double factor) { + if(factor <= 1) { + activeSeries = rawData; + } else { + //int i = (int) Math.round(Math.sqrt(factor) - 1); + int i = getZoomIndex(factor, getRatio()); + if (i < zoomLevels.size()) { + activeSeries = zoomLevels.get(i); + } else { + activeSeries = zoomLevels.get(zoomLevels.size() - 1); + } + } + } + + protected static int getZoomIndex(double zoomFactor, double ratio) { + final double lhs = Math.log(zoomFactor); + final double rhs = Math.log(ratio); + final double log = lhs / rhs; + final int index = (int) Math.round(log); + return index > 0 ? index - 1 : 0; + } + + public double getMaxZoomFactor() { + return Math.pow(getRatio(), zoomLevels.size()); + } + + public Sampler getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(Sampler algorithm) { + this.algorithm = algorithm; + resample(); + } + + @Override + public String getTitle() { + return rawData.getTitle(); + } + + @Override + public int size() { + return activeSeries.size(); + } + + @Override + public Number getX(int index) { + return activeSeries.getX(index); + } + + @Override + public Number getY(int index) { + return activeSeries.getY(index); + } + + public int getThreshold() { + return threshold; + } + + public void setThreshold(int threshold) { + if(threshold >= rawData.size()) { + throw new IllegalArgumentException("Threshold must be < original series size."); + } + this.threshold = threshold; + } + + public RectRegion getBounds() { + return bounds; + } + + public void setBounds(RectRegion bounds) { + this.bounds = bounds; + } + + @Override + public RectRegion minMax() { + return bounds; + } + + @Override + public XOrder getXOrder() { + return xOrder; + } + + public float getRatio() { + return ratio; + } + + public void setRatio(float ratio) { + if(ratio <= 1) { + throw new IllegalArgumentException("Ratio must be greater than 1"); + } + this.ratio = ratio; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java b/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java new file mode 100644 index 00000000..ebbc8238 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java @@ -0,0 +1,16 @@ +package com.androidplot.xy; + +/** + * An algorithm used to to resample a larger set of data into a smaller set. + */ +public interface Sampler { + + /** + * + * @param input The original unsampled series + * @param output The destination series to contain sampled result. + * This series size should be set to the desired sampled size. + * @return min/max values encountered while processing input. + */ + RectRegion run(XYSeries input, EditableXYSeries output); +} 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 7cfadff4..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,9 +17,12 @@ package com.androidplot.xy; import android.graphics.Canvas; + import com.androidplot.Plot; import com.androidplot.PlotListener; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; @@ -29,9 +32,46 @@ /** * A convenience class used to create instances of XYPlot generated from Lists of Numbers. */ -public class SimpleXYSeries implements XYSeries, PlotListener { +public class SimpleXYSeries implements EditableXYSeries, OrderedXYSeries, PlotListener { + private volatile LinkedList xVals = new LinkedList<>(); + private volatile LinkedList yVals = new LinkedList<>(); + private volatile String title = null; + + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + private XOrder xOrder = XOrder.NONE; - private static final String TAG = SimpleXYSeries.class.getName(); + public enum ArrayFormat { + Y_VALS_ONLY, + XY_VALS_INTERLEAVED + } + + public SimpleXYSeries(String title) { + this.title = title; + } + + 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) { @@ -43,20 +83,12 @@ public void onAfterDraw(Plot source, Canvas canvas) { lock.readLock().unlock(); } - public enum ArrayFormat { - Y_VALS_ONLY, - XY_VALS_INTERLEAVED + protected static List asNumberList(Number... model) { + List numbers = new ArrayList<>(model.length); + Collections.addAll(numbers, model); + return numbers; } - 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; - } /** * Generates an XYSeries instance from the List of numbers passed in. This is a convenience class * and should only be used for static data models; it is not suitable for representing dynamically @@ -109,7 +141,7 @@ public void setModel(List model, ArrayFormat format) { lock.writeLock().lock(); try { // empty the current values: - xVals = null; + xVals.clear(); yVals.clear(); // make sure the new model has data: @@ -121,15 +153,16 @@ public void setModel(List 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."); @@ -177,6 +210,26 @@ public void setY(Number value, int index) { } } + @Override + 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); + } + } else if(xVals.size() > size) { + for(int i = xVals.size(); i > size; i--) { + xVals.removeLast(); + yVals.removeLast(); + } + } + } finally { + lock.writeLock().unlock(); + } + } + /** * Sets xy values based on index * @param xVal @@ -205,15 +258,15 @@ public void addFirst(Number x, Number y) { /** * - * @return {@link XY} with first equal to x-val and second equal to y-val. + * @return {@link XYCoords} with first equal to x-val and second equal to y-val. */ - public XY removeFirst() { + public XYCoords removeFirst() { lock.writeLock().lock(); try { if (size() <= 0) { throw new NoSuchElementException(); } - return new XY(xVals != null ? xVals.removeFirst() : 0, yVals.removeFirst()); + return new XYCoords(xVals != null ? xVals.removeFirst() : 0, yVals.removeFirst()); } finally { lock.writeLock().unlock(); } @@ -233,15 +286,15 @@ public void addLast(Number x, Number y) { /** * - * @return {@link XY} with first equal to x-val and second equal to y-val. + * @return {@link XYCoords} with first equal to x-val and second equal to y-val. */ - public XY removeLast() { + public XYCoords removeLast() { lock.writeLock().lock(); try { if (size() <= 0) { throw new NoSuchElementException(); } - return new XY(xVals != null ? xVals.removeLast() : yVals.size() - 1, yVals.removeLast()); + return new XYCoords(xVals != null ? xVals.removeLast() : yVals.size() - 1, yVals.removeLast()); } finally { lock.writeLock().unlock(); } @@ -273,4 +326,27 @@ public Number getX(int index) { public Number getY(int index) { return yVals.get(index); } + + public LinkedList getxVals() { + return xVals; + } + + public LinkedList getyVals() { + return yVals; + } + + /** + * Remove all values from the series + */ + public void clear() { + lock.writeLock().lock(); + try { + if (xVals != null) { + xVals.clear(); + } + yVals.clear(); + } finally { + lock.writeLock().unlock(); + } + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYStep.java b/androidplot-core/src/main/java/com/androidplot/xy/Step.java similarity index 84% rename from androidplot-core/src/main/java/com/androidplot/xy/XYStep.java rename to androidplot-core/src/main/java/com/androidplot/xy/Step.java index 01a502d7..ef8a2cfa 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYStep.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/Step.java @@ -1,60 +1,60 @@ -/* - * 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; - -/** - * An immutable object generated by XYStepCalculator representing - * a stepping model to be used by an XYPlot. - */ -public class XYStep { - - private final float stepCount; - private final float stepPix; - private final double stepVal; - - //public XYStep() {} - - public XYStep(float stepCount, float stepPix, double stepVal) { - this.stepCount = stepCount; - this.stepPix = stepPix; - this.stepVal = stepVal; - } - - public double getStepCount() { - return stepCount; - } - - /*public void setStepCount(double stepCount) { - this.stepCount = stepCount; - }*/ - - public float getStepPix() { - return stepPix; - } - - /*public void setStepPix(float stepPix) { - this.stepPix = stepPix; - }*/ - - public double getStepVal() { - return stepVal; - } - - /*public void setStepVal(double stepVal) { - this.stepVal = stepVal; - }*/ -} +/* + * 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; + +/** + * An immutable object generated by XYStepCalculator representing + * a stepping model to be used by an XYPlot. + */ +public class Step { + + private final double stepCount; + private final double stepPix; + private final double stepVal; + + //public XYStep() {} + + public Step(double stepCount, double stepPix, double stepVal) { + this.stepCount = stepCount; + this.stepPix = stepPix; + this.stepVal = stepVal; + } + + public double getStepCount() { + return stepCount; + } + + /*public void setStepCount(double stepCount) { + this.stepCount = stepCount; + }*/ + + public double getStepPix() { + return stepPix; + } + + /*public void setStepPix(float stepPix) { + this.stepPix = stepPix; + }*/ + + public double getStepVal() { + return stepVal; + } + + /*public void setStepVal(double stepVal) { + this.stepVal = stepVal; + }*/ +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java index 37e64701..daec3449 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java @@ -16,9 +16,7 @@ package com.androidplot.xy; -import android.content.Context; import com.androidplot.ui.SeriesRenderer; -import com.androidplot.util.Configurator; public class StepFormatter extends LineAndPointFormatter { @@ -38,7 +36,7 @@ public Class getRendererClass() { } @Override - public SeriesRenderer getRendererInstance(XYPlot plot) { + public SeriesRenderer doGetRendererInstance(XYPlot plot) { return new StepRenderer(plot); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYStepMode.java b/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java similarity index 81% rename from androidplot-core/src/main/java/com/androidplot/xy/XYStepMode.java rename to androidplot-core/src/main/java/com/androidplot/xy/StepMode.java index 9aee00e9..00f60747 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYStepMode.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java @@ -1,28 +1,30 @@ -/* - * 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; - -/** - * INCREMENTAL_VALUE - (default) draw a tick every n values. - * INCREMENTAL_PIXEL - draw a tick every n pixels. - * SUBDIVIDE - draw n number of evenly spaced ticks. - */ -public enum XYStepMode { - SUBDIVIDE, // default - INCREMENT_BY_VAL, - INCREMENT_BY_PIXELS -} +/* + * 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; + +/** + * 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_FIT +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYStepModel.java b/androidplot-core/src/main/java/com/androidplot/xy/StepModel.java similarity index 84% rename from androidplot-core/src/main/java/com/androidplot/xy/XYStepModel.java rename to androidplot-core/src/main/java/com/androidplot/xy/StepModel.java index 4abf7838..0334cb17 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYStepModel.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/StepModel.java @@ -19,21 +19,21 @@ /** * Encapsulates a set of stepping parameters for a single axis. */ -public class XYStepModel { +public class StepModel { - public XYStepModel(XYStepMode mode, double value) { + public StepModel(StepMode mode, double value) { setMode(mode); setValue(value); } - private XYStepMode mode; + private StepMode mode; private double value; - public XYStepMode getMode() { + public StepMode getMode() { return mode; } - public void setMode(XYStepMode mode) { + public void setMode(StepMode mode) { this.mode = mode; } 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 5413cf18..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) { @@ -32,133 +31,7 @@ public StepRenderer(XYPlot plot) { @Override protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) { - //path.lineTo(thisPoint.x, thisPoint.y); - path.lineTo(thisPoint.x, lastPoint.y); path.lineTo(thisPoint.x, thisPoint.y); - //canvas.drawPoint(point.x, lastPoint.y, format.getVertexPaint()); - // next the vertical: - //canvas.drawLine(point.x, lastPoint.y, point.x, point.y, format.getLinePaint()); - } -} -/* -public class StepRenderer extends XYSeriesRenderer { - - private PointF lastPoint; - - private boolean drawLinesEnabled = true; - private boolean drawPointsEnabled = true; - - private XYAxisType stepAxis = XYAxisType.DOMAIN; - - - - public StepRenderer(XYPlot plot) { - super(plot); - } - - @Override - public void onRender(Canvas canvas, RectF plotArea) throws PlotRenderException { - - - - for(XYSeries series : getPlot().getSeriesListForRenderer(this.getClass())) { - - drawSeries(canvas, plotArea, series, getFormatter(series)); - } - //foreach(this.) - //foreach() - } - - @Override - public void doDrawLegendIcon(Canvas canvas, RectF rect, String text, StepFormatter getFormatter) { - // horizontal icon: - float centerY = rect.centerY(); - float centerX = rect.centerX(); - canvas.drawLine(rect.left, rect.top, rect.right, rect.bottom, getFormatter.getLinePaint()); - canvas.drawPoint(centerX, centerY, getFormatter.getVertexPaint()); - //canvas.drawRect(rect, getFormatter.getLinePaint()); - - } - - - private void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, StepFormatter getFormatter) throws PlotRenderException { - beginSeries(canvas, plotArea, getFormatter); - //XYDataset series = bundle.getDataset(); - //int seriesIndex = bundle.getSeriesIndex(); - PointF thisPoint; - for (int i = 0; i < series.size(); i++) { - Number y = series.getY(i); - Number x = series.getD(i); - - if (y != null && x != null) { - - thisPoint = ValPixConverter.valToPix( - x, - y, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - //float pixX = ValPixConverter.valToPix(x.doubleValue(), getPlot().getCalculatedMinX().doubleValue(), getPlot().getCalculatedMaxX().doubleValue(), plotArea.width(), false) + (plotArea.left); - //float pixY = ValPixConverter.valToPix(y.doubleValue(), getPlot().getCalculatedMinY().doubleValue(), getPlot().getCalculatedMaxY().doubleValue(), plotArea.height(), true) + plotArea.top; - - //thisPoint = new PointF(pixX, pixY); - } else { - thisPoint = null; - } - drawPoint(canvas, thisPoint, plotArea, getFormatter); - } - endSeries(canvas, plotArea, getFormatter); - } - - private void beginSeries(Canvas canvas, RectF plotArea, StepFormatter format) throws PlotRenderException { - lastPoint = null; - } - - private void drawPoint(Canvas canvas, PointF point, RectF plotArea, StepFormatter format) throws PlotRenderException { - if (lastPoint != null) { - if (point != null) { - - switch(stepAxis) { - case DOMAIN: - // first draw the horizontal line: - canvas.drawLine(lastPoint.x, lastPoint.y, point.x, lastPoint.y, format.getLinePaint()); - canvas.drawPoint(point.x, lastPoint.y, format.getVertexPaint()); - // next the vertical: - canvas.drawLine(point.x, lastPoint.y, point.x, point.y, format.getLinePaint()); - break; - case RANGE: - break; - } - //doDrawLine(canvas, lastPoint, point, plotArea, format); - - - } - drawLastPoint(canvas, plotArea, format); - } - - lastPoint = point; - } - - private void endSeries(Canvas canvas, RectF plotArea, StepFormatter format) throws PlotRenderException { - if(lastPoint != null) { - drawLastPoint(canvas, plotArea, format); - } - } - - protected void drawLastPoint(Canvas canvas, RectF plotArea, StepFormatter format) throws PlotRenderException { - canvas.drawPoint(lastPoint.x, lastPoint.y, format.getVertexPaint()); - } - - - public XYAxisType getStepAxis() { - return stepAxis; - } - - public void setStepAxis(XYAxisType stepAxis) { - this.stepAxis = stepAxis; } } -*/ 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 dbdd8043..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,9 +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. @@ -26,6 +31,8 @@ */ public abstract class ValueMarker { + private static final int MARKER_LABEL_SPACING = 2; + public String getText() { return text; } @@ -34,14 +41,9 @@ public void setText(String text) { this.text = text; } - public enum TextOrientation { - HORIZONTAL, - VERTICAL - } private Number value; private Paint linePaint; private Paint textPaint; - //private Paint backgroundPaint; private TextOrientation textOrientation; private int textMargin = 2; private PositionMetricType textPosition; @@ -55,10 +57,6 @@ public enum TextOrientation { textPaint = new Paint(); textPaint.setAntiAlias(true); textPaint.setColor(Color.RED); - //backgroundPaint = new Paint(); - //backgroundPaint.setColor(Color.argb(100, 100, 100, 100)); - //backgroundPaint.setColor(Color.DKGRAY); - } public ValueMarker(Number value, String text, PositionMetricType textPosition) { @@ -77,10 +75,8 @@ public ValueMarker(Number value, String text, PositionMetricType textPosition) { */ public ValueMarker(Number value, String text, PositionMetricType textPosition, Paint linePaint, Paint textPaint) { this(value, text, textPosition); - this.linePaint = linePaint; this.textPaint = textPaint; - //this.backgroundPaint = backgroundPaint; } public ValueMarker(Number value, String text, PositionMetricType textPosition, int linePaint, int textPaint) { @@ -113,14 +109,6 @@ public void setTextPaint(Paint textPaint) { this.textPaint = textPaint; } - /*public Paint getBackgroundPaint() { - return backgroundPaint; - } - - public void setBackgroundPaint(Paint backgroundPaint) { - this.backgroundPaint = backgroundPaint; - }*/ - public TextOrientation getTextOrientation() { return textOrientation; } @@ -153,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 2bc77923..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,11 +16,14 @@ package com.androidplot.xy; +import android.graphics.Canvas; import android.graphics.Paint; -import com.androidplot.ui.YLayoutStyle; -import com.androidplot.ui.YPositionMetric; +import android.graphics.RectF; -public class XValueMarker extends ValueMarker { +import com.androidplot.ui.VerticalPositioning; +import com.androidplot.ui.VerticalPosition; + +public class XValueMarker extends ValueMarker { /** * @@ -28,7 +31,7 @@ public class XValueMarker extends ValueMarker { * @param text Set to null to use the plot's default getFormatter. */ public XValueMarker(Number value, String text) { - super(value, text, new YPositionMetric(3, YLayoutStyle.ABSOLUTE_FROM_TOP)); + super(value, text, new VerticalPosition(3, VerticalPositioning.ABSOLUTE_FROM_TOP)); } /** @@ -39,7 +42,7 @@ public XValueMarker(Number value, String text) { * @param linePaint * @param textPaint */ - public XValueMarker(Number value, String text, YPositionMetric textPosition, Paint linePaint, Paint textPaint) { + public XValueMarker(Number value, String text, VerticalPosition textPosition, Paint linePaint, Paint textPaint) { super(value, text, textPosition, linePaint, textPaint); } @@ -52,7 +55,22 @@ public XValueMarker(Number value, String text, YPositionMetric textPosition, Pai * @param linePaint * @param textPaint */ - public XValueMarker(Number value, String text, YPositionMetric textPosition, int linePaint, int textPaint) { + 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 new file mode 100644 index 00000000..c4452d33 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java @@ -0,0 +1,195 @@ +/* + * Copyright 2016 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Calculates the min/max constraints for an xy plane. + * + * @since 0.9.7 + */ +public class XYConstraints { + + // used for calculating the domain/range extents that will be displayed on the plot. + // using boundaries and origins are mutually exclusive and enabling one will disable + // the other. when only setting the FramingModel, the origin or boundary is set to + // the current value of the plot. + private XYFramingModel domainFramingModel = XYFramingModel.EDGE; + private XYFramingModel rangeFramingModel = XYFramingModel.EDGE; + + // determines how boundaries adjust as new min/max values are encountered: + private BoundaryMode domainUpperBoundaryMode = BoundaryMode.AUTO; + private BoundaryMode domainLowerBoundaryMode = BoundaryMode.AUTO; + private BoundaryMode rangeUpperBoundaryMode = BoundaryMode.AUTO; + private BoundaryMode rangeLowerBoundaryMode = BoundaryMode.AUTO; + + private Number minX; + private Number maxX; + private Number minY; + private Number maxY; + + public XYConstraints() { + this(null, null, null, null); + } + + 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) { + // this is essentially an invisible point: + 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(@NonNull XYFramingModel domainFramingModel) { + this.domainFramingModel = domainFramingModel; + } + + @NonNull + public XYFramingModel getRangeFramingModel() { + return rangeFramingModel; + } + + public void setRangeFramingModel(@NonNull XYFramingModel rangeFramingModel) { + this.rangeFramingModel = rangeFramingModel; + } + + @NonNull + public BoundaryMode getDomainUpperBoundaryMode() { + return domainUpperBoundaryMode; + } + + public void setDomainUpperBoundaryMode(@NonNull BoundaryMode domainUpperBoundaryMode) { + this.domainUpperBoundaryMode = domainUpperBoundaryMode; + } + + @NonNull + public BoundaryMode getDomainLowerBoundaryMode() { + return domainLowerBoundaryMode; + } + + public void setDomainLowerBoundaryMode(@NonNull BoundaryMode domainLowerBoundaryMode) { + this.domainLowerBoundaryMode = domainLowerBoundaryMode; + } + + @NonNull + public BoundaryMode getRangeUpperBoundaryMode() { + return rangeUpperBoundaryMode; + } + + public void setRangeUpperBoundaryMode(@NonNull BoundaryMode rangeUpperBoundaryMode) { + this.rangeUpperBoundaryMode = rangeUpperBoundaryMode; + } + + @NonNull + public BoundaryMode getRangeLowerBoundaryMode() { + return rangeLowerBoundaryMode; + } + + public void setRangeLowerBoundaryMode(@NonNull BoundaryMode rangeLowerBoundaryMode) { + this.rangeLowerBoundaryMode = rangeLowerBoundaryMode; + } + + @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/XY.java b/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java similarity index 89% rename from androidplot-core/src/main/java/com/androidplot/xy/XY.java rename to androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java index ff3259b7..1bd6c0c8 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XY.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java @@ -19,11 +19,13 @@ /** * A pair of x/y coordinates */ -public class XY { +public class XYCoords { public Number x; public Number y; - public XY(Number x, Number y) { + public XYCoords() {} + + public XYCoords(Number x, Number y) { this.x = x; this.y = y; } 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 729446cc..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,178 +16,230 @@ package com.androidplot.xy; -import android.graphics.*; - -import com.androidplot.exception.PlotRenderException; -import com.androidplot.ui.BoxModel; +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.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.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. + * 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 { private static final int ZERO = 0; private static final int ONE = 1; private static final int TWO = 2; - private static final float FLOAT_ONE = 1; - private static final int MARKER_LABEL_SPACING = TWO; + /** + * Fudge factor used to compensate for double<->float precision loss. + */ + private static final float FUDGE = 0.00001f; + + private static final float DEFAULT_LINE_LABEL_TEXT_SIZE_PX = PixelUtils.spToPix(15); - // max size of domain tick labels in pixels - private float domainTickLabelWidth = 15; + /** + * Line interval per range label + */ + private int linesPerRangeLabel = ONE; - // ... - private float rangeTickLabelWidth = 41; - private float domainTickLabelVerticalOffset = -5; - private float domainTickLabelHorizontalOffset = ZERO; + /** + * Line interval per domain label + */ + private int linesPerDomainLabel = ONE; - // allows tweaking of text position - private float rangeTickLabelHorizontalOffset = FLOAT_ONE; + private Insets gridInsets = new Insets(); - // allows tweaking of text position - private float rangeTickLabelVerticalOffset = ZERO; + /** + * Insets defining the positioning of line labels relative to the edges of the graph + */ + private Insets lineLabelInsets = new Insets(); - private int ticksPerRangeLabel = ONE; - private int ticksPerDomainLabel = ONE; + private RectF gridRect; - private BoxModel gridBox = new BoxModel(); - private DisplayDimensions gridDimensions; + private RectF labelRect; - private int domainTickExtension = 5; - private int rangeTickExtension = 5; - private int domainLabelSubTickExtension = ZERO; - private int rangeLabelSubTickExtension = ZERO; + /** + * how far lines extend on labeled "tick" lines + */ + private float lineExtensionTop; + private float lineExtensionBottom; + private float lineExtensionLeft; + private float lineExtensionRight; + + /** + * Background color of the grid area + */ private Paint gridBackgroundPaint; + + /** + * Color of range grid lines + */ private Paint rangeGridLinePaint; + + /** + * Color of range sub-grid lines + */ private Paint rangeSubGridLinePaint; + + /** + * Color of domain grid lines + */ private Paint domainGridLinePaint; + + /** + * Color of domain sub-grid lines + */ private Paint domainSubGridLinePaint; - private Paint domainTickLabelPaint; - private Paint rangeTickLabelPaint; + + private boolean isGridClippingEnabled = true; + private Paint domainCursorPaint; private Paint rangeCursorPaint; - private Paint cursorLabelPaint; - private Paint cursorLabelBackgroundPaint; private XYPlot plot; - private Format rangeValueFormat; - private Format domainValueFormat; + private Paint domainOriginLinePaint; private Paint rangeOriginLinePaint; - private Paint domainOriginTickLabelPaint; - private Paint rangeOriginTickLabelPaint; - private float domainCursorPosition; - private float rangeCursorPosition; + private Float domainCursorPosition; + private Float rangeCursorPosition; - private boolean drawCursorLabelEnabled = true; private boolean drawMarkersEnabled = true; + private boolean drawGridOnTop; - private boolean rangeAxisLeft = true; - private boolean domainAxisBottom = true; - - private boolean rangeTick = true; - private boolean rangeSubTick = true; - private boolean domainTick = true; - private boolean domainSubTick = true; + /** + * Set of edges for which line labels should be displayed + */ + private EnumSet lineLabelEdges = EnumSet.noneOf(Edge.class); - private float rangeLabelOrientation; - private float domainLabelOrientation; + private RenderStack renderStack; - private Mapping domainTickLabelPaintMap; - private Mapping rangeTickLabelPaintMap; + private CursorLabelFormatter cursorLabelFormatter; + + 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) { + final int canvasState = canvas.save(); + try { + final String txt = style.format.format(val); + canvas.rotate(style.getRotation(), x, y); + drawLabel(canvas, txt, style.getPaint(), x, y, isOrigin); + } finally { + canvas.restoreToCount(canvasState); + } + } - private ZHash axisValueLabelRegions; + protected void drawLabel(Canvas canvas, + String text, + Paint paint, + float x, + float y, + boolean isOrigin) { + canvas.drawText(text, x, y, paint); + } + } - private RenderStack renderStack; + public static class LineLabelStyle { + private Paint paint = new Paint(); + private float rotation = 0; + private Format format = new DecimalFormat("0.0"); - private static final float DEFAULT_TICK_LABEL_TEXT_SIZE_PX = PixelUtils.spToPix(15); // 15sp + { + paint.setColor(Color.LTGRAY); + paint.setAntiAlias(true); + paint.setTextAlign(Paint.Align.CENTER); + paint.setTextSize(DEFAULT_LINE_LABEL_TEXT_SIZE_PX); + } - public float getRangeLabelOrientation() { - return rangeLabelOrientation; - } + public Format getFormat() { + return format; + } - public void setRangeLabelOrientation(float rangeLabelOrientation) { - this.rangeLabelOrientation = rangeLabelOrientation; - } + public void setFormat(Format format) { + this.format = format; + } - public float getDomainLabelOrientation() { - return domainLabelOrientation; - } + public float getRotation() { + return rotation; + } - public void setDomainLabelOrientation(float domainLabelOrientation) { - this.domainLabelOrientation = domainLabelOrientation; - } + public void setRotation(float rotation) { + this.rotation = rotation; + } - /** - * - * @return - */ - public Mapping getDomainTickLabelPaintMap() { - return domainTickLabelPaintMap; - } + public Paint getPaint() { + return paint; + } - /** - * Set a mapping to override the Paint used to draw domain labels. The mapping should - * return null for values that should not be overridden. - * @param domainTickLabelPaintMap - */ - public void setDomainTickLabelPaintMap(Mapping domainTickLabelPaintMap) { - this.domainTickLabelPaintMap = domainTickLabelPaintMap; + public void setPaint(Paint paint) { + this.paint = paint; + } } - public Mapping getRangeTickLabelPaintMap() { - return rangeTickLabelPaintMap; - } + public interface CursorLabelFormatter { - /** - * Set a mapping to override the Paint used to draw range labels. The mapping should - * return null for values that should not be overridden. - * @param rangeLabelTickPaintMap - */ - public void setRangeLabelTickPaintMap(Mapping rangeLabelTickPaintMap) { - this.rangeTickLabelPaintMap = rangeLabelTickPaintMap; - } + /** + * @return The Paint to be used to draw the cursor text label. + */ + Paint getTextPaint(); - public BoxModel getGridBox() { - return gridBox; - } + /** + * @return Null if no background should be drawn, the Paint used to draw the background + * otherwise. + */ + Paint getBackgroundPaint(); - public void setGridBox(BoxModel gridBox) { - this.gridBox = gridBox; + String getLabelText(Number x, Number y); } - public Paint getDomainCursorPaint() { - return domainCursorPaint; - } + public enum Edge { + NONE(0), + LEFT(1), + RIGHT(2), + TOP(4), + BOTTOM(8); - /** - * - * @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; - } + private final int value; - public Paint getRangeCursorPaint() { - return rangeCursorPaint; - } + Edge(int value) { + this.value = value; + } - /** - * - * @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; + public int getValue() { + return value; + } } { @@ -195,46 +247,17 @@ public void setRangeCursorPaint(Paint rangeCursorPaint) { gridBackgroundPaint.setColor(Color.rgb(140, 140, 140)); gridBackgroundPaint.setStyle(Paint.Style.FILL); - rangeGridLinePaint = new Paint(); - rangeGridLinePaint.setColor(Color.rgb(180, 180, 180)); - rangeGridLinePaint.setAntiAlias(true); - rangeGridLinePaint.setStyle(Paint.Style.STROKE); - - domainGridLinePaint = new Paint(rangeGridLinePaint); - domainSubGridLinePaint = new Paint(domainGridLinePaint); - rangeSubGridLinePaint = new Paint(rangeGridLinePaint); - - domainOriginLinePaint = new Paint(); - domainOriginLinePaint.setColor(Color.WHITE); - domainOriginLinePaint.setAntiAlias(true); - - rangeOriginLinePaint = new Paint(); - rangeOriginLinePaint.setColor(Color.WHITE); - rangeOriginLinePaint.setAntiAlias(true); - - domainOriginTickLabelPaint = new Paint(); - domainOriginTickLabelPaint.setColor(Color.WHITE); - domainOriginTickLabelPaint.setAntiAlias(true); - domainOriginTickLabelPaint.setTextAlign(Paint.Align.CENTER); - domainOriginTickLabelPaint.setTextSize(DEFAULT_TICK_LABEL_TEXT_SIZE_PX); - - rangeOriginTickLabelPaint = new Paint(); - rangeOriginTickLabelPaint.setColor(Color.WHITE); - rangeOriginTickLabelPaint.setAntiAlias(true); - rangeOriginTickLabelPaint.setTextAlign(Paint.Align.RIGHT); - rangeOriginTickLabelPaint.setTextSize(DEFAULT_TICK_LABEL_TEXT_SIZE_PX); - - domainTickLabelPaint = new Paint(); - domainTickLabelPaint.setColor(Color.LTGRAY); - domainTickLabelPaint.setAntiAlias(true); - domainTickLabelPaint.setTextAlign(Paint.Align.CENTER); - domainTickLabelPaint.setTextSize(DEFAULT_TICK_LABEL_TEXT_SIZE_PX); - - rangeTickLabelPaint = new Paint(); - rangeTickLabelPaint.setColor(Color.LTGRAY); - rangeTickLabelPaint.setAntiAlias(true); - rangeTickLabelPaint.setTextAlign(Paint.Align.RIGHT); - rangeTickLabelPaint.setTextSize(DEFAULT_TICK_LABEL_TEXT_SIZE_PX); + final Paint defaultLinePaint = new Paint(); + defaultLinePaint.setColor(Color.rgb(180, 180, 180)); + defaultLinePaint.setAntiAlias(true); + defaultLinePaint.setStyle(Paint.Style.STROKE); + + rangeGridLinePaint = new Paint(defaultLinePaint); + domainGridLinePaint = new Paint(defaultLinePaint); + domainSubGridLinePaint = new Paint(defaultLinePaint); + rangeSubGridLinePaint = new Paint(defaultLinePaint); + domainOriginLinePaint = new Paint(defaultLinePaint); + rangeOriginLinePaint = new Paint(defaultLinePaint); domainCursorPaint = new Paint(); domainCursorPaint.setColor(Color.YELLOW); @@ -242,16 +265,10 @@ public void setRangeCursorPaint(Paint rangeCursorPaint) { rangeCursorPaint = new Paint(); rangeCursorPaint.setColor(Color.YELLOW); - cursorLabelPaint = new Paint(); - cursorLabelPaint.setColor(Color.YELLOW); - cursorLabelBackgroundPaint = new Paint(); - cursorLabelBackgroundPaint.setColor(Color.argb(100, 50, 50, 50)); setMarginTop(7); setMarginRight(4); setMarginBottom(4); - rangeValueFormat = new DecimalFormat("0.0"); - domainValueFormat = new DecimalFormat("0.0"); - axisValueLabelRegions = new ZHash(); + setClippingEnabled(true); } public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) { @@ -260,169 +277,284 @@ public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) { renderStack = new RenderStack(plot); } - public ZIndexable getAxisValueLabelRegions() { - return axisValueLabelRegions; - } - /** - * Add a new Region used for rendering axis valuelabels. Note that it is - * possible to add multiple Region instances which overlap, in which cast - * the last region to be added will be used. It is up to the developer to - * guard against this often undesireable situation. - * - * @param region - * @param formatter + * Apply xml attrs + * + * @param attrs */ - public void addAxisValueLabelRegion(RectRegion region, - AxisValueLabelFormatter formatter) { - axisValueLabelRegions.addToTop(region, formatter); - } + public void processAttrs(TypedArray attrs) { - /** - * Convenience method - wraps addAxisValueLabelRegion, using - * Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY to mask off range - * axis value labels. - * - * @param min - * @param max - * @param formatter - * - */ - public void addDomainAxisValueLabelRegion(double min, double max, - AxisValueLabelFormatter formatter) { - addAxisValueLabelRegion(new RectRegion(min, max, - Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, null), - formatter); - } + setDrawGridOnTop(attrs.getBoolean(R.styleable.xy_XYPlot_drawGridOnTop, isDrawGridOnTop())); + int tlp = attrs.getInt(R.styleable.xy_XYPlot_lineLabels, 0); + if (tlp != 0) { + setLineLabelEdges(tlp); + } - /** - * Convenience method - wraps addAxisValueLabelRegion, using - * Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY to mask off domain - * axis value labels. - * - * @param min - * @param max - * @param formatter - */ - public void addRangeAxisValueLabelRegion(double min, double max, - AxisValueLabelFormatter formatter) { - addAxisValueLabelRegion(new RectRegion(Double.POSITIVE_INFINITY, - Double.NEGATIVE_INFINITY, min, max, null), formatter); + setGridClippingEnabled(attrs.getBoolean( + R.styleable.xy_XYPlot_gridClippingEnabled, + isGridClippingEnabled() + )); + + final LineLabelStyle lineLabelStyleTop = getLineLabelStyle(Edge.TOP); + final LineLabelStyle lineLabelStyleBottom = getLineLabelStyle(Edge.BOTTOM); + final LineLabelStyle lineLabelStyleLeft = getLineLabelStyle(Edge.LEFT); + final LineLabelStyle lineLabelStyleRight = getLineLabelStyle(Edge.RIGHT); + + lineLabelStyleTop.setRotation(attrs.getFloat( + R.styleable.xy_XYPlot_lineLabelRotationTop, + lineLabelStyleTop.getRotation() + )); + + lineLabelStyleBottom.setRotation(attrs.getFloat( + R.styleable.xy_XYPlot_lineLabelRotationBottom, + lineLabelStyleBottom.getRotation() + )); + + lineLabelStyleLeft.setRotation(attrs.getFloat( + R.styleable.xy_XYPlot_lineLabelRotationLeft, + lineLabelStyleLeft.getRotation() + )); + + lineLabelStyleRight.setRotation(attrs.getFloat( + R.styleable.xy_XYPlot_lineLabelRotationRight, + lineLabelStyleRight.getRotation() + )); + + setLineExtensionTop(attrs.getDimension( + R.styleable.xy_XYPlot_lineExtensionTop, getLineExtensionTop())); + setLineExtensionBottom(attrs.getDimension( + R.styleable.xy_XYPlot_lineExtensionBottom, getLineExtensionBottom())); + setLineExtensionLeft(attrs.getDimension( + R.styleable.xy_XYPlot_lineExtensionLeft, getLineExtensionLeft())); + setLineExtensionRight(attrs.getDimension( + R.styleable.xy_XYPlot_lineExtensionRight, getLineExtensionRight())); + + AttrUtils.configureTextPaint(attrs, lineLabelStyleTop.getPaint(), + R.styleable.xy_XYPlot_lineLabelTextColorTop, + R.styleable.xy_XYPlot_lineLabelTextSizeTop, + 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 + ); + + AttrUtils.configureTextPaint(attrs, lineLabelStyleLeft.getPaint(), + R.styleable.xy_XYPlot_lineLabelTextColorLeft, + R.styleable.xy_XYPlot_lineLabelTextSizeLeft, + 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 + ); + + AttrUtils.configureInsets(attrs, getGridInsets(), + R.styleable.xy_XYPlot_gridInsetTop, + R.styleable.xy_XYPlot_gridInsetBottom, + R.styleable.xy_XYPlot_gridInsetLeft, + 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 + ); + + // graph size & position + AttrUtils.configureWidget(attrs, this, + R.styleable.xy_XYPlot_graphHeightMode, R.styleable.xy_XYPlot_graphHeight, + 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 + ); + + // domainLabel size & position + AttrUtils.configureWidget(attrs, this, + R.styleable.xy_XYPlot_domainTitleHeightMode, R.styleable.xy_XYPlot_domainTitleHeight, + 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 + ); + + // rangeLabel size & position + AttrUtils.configureWidget(attrs, this, + R.styleable.xy_XYPlot_rangeTitleHeightMode, R.styleable.xy_XYPlot_rangeTitleHeight, + 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 + ); + + // rotation + AttrUtils.configureWidgetRotation(attrs, this, R.styleable.xy_XYPlot_graphRotation); + + // 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 + ); + + // domainOriginLinePaint + AttrUtils.configureLinePaint(attrs, getDomainOriginLinePaint(), + R.styleable.xy_XYPlot_domainOriginLineColor, + R.styleable.xy_XYPlot_domainOriginLineThickness + ); + + // rangeOriginLinePaint + AttrUtils.configureLinePaint(attrs, getRangeOriginLinePaint(), + R.styleable.xy_XYPlot_rangeOriginLineColor, + R.styleable.xy_XYPlot_rangeOriginLineThickness + ); + + AttrUtils.configureLinePaint(attrs, getDomainGridLinePaint(), + R.styleable.xy_XYPlot_domainLineColor, + R.styleable.xy_XYPlot_domainLineThickness + ); + + AttrUtils.configureLinePaint(attrs, getRangeGridLinePaint(), + R.styleable.xy_XYPlot_rangeLineColor, + R.styleable.xy_XYPlot_rangeLineThickness + ); + + AttrUtils.setColor(attrs, getBackgroundPaint(), + R.styleable.xy_XYPlot_graphBackgroundColor + ); + + AttrUtils.setColor(attrs, getGridBackgroundPaint(), + R.styleable.xy_XYPlot_gridBackgroundColor + ); } /** - * Returns the formatter associated with the first (bottom) Region - * containing x and y. - * - * @param x - * @param y - * @return the formatter associated with the first (bottom) region - * containing x and y. null otherwise. + * 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 AxisValueLabelFormatter getAxisValueLabelFormatterForVal(double x, - double y) { - for (RectRegion r : axisValueLabelRegions.elements()) { - if (r.containsValue(x, y)) { - return axisValueLabelRegions.get(r); - } - } - return null; - } - - public AxisValueLabelFormatter getAxisValueLabelFormatterForDomainVal( - double val) { - for (RectRegion r : axisValueLabelRegions.elements()) { - if (r.containsDomainValue(val)) { - return axisValueLabelRegions.get(r); - } - } - return null; - } - - public AxisValueLabelFormatter getAxisValueLabelFormatterForRangeVal( - double val) { - for (RectRegion r : axisValueLabelRegions.elements()) { - if (r.containsRangeValue(val)) { - return axisValueLabelRegions.get(r); - } + protected XYCoords screenToSeries(PointF point) { + if (!plot.getBounds().isFullyDefined()) { + return null; } - return null; - } - - private String getFormattedRangeValue(Number value) { - return rangeValueFormat.format(value); + return new RectRegion(gridRect) + .transform(point.x, point.y, plot.getBounds(), false, true); } - private String getFormattedDomainValue(Number value) { - return domainValueFormat.format(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 point + * @return + */ + protected Number screenToSeriesX(PointF point) { + return screenToSeriesX(point.x); } /** - * 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 Double getYVal(PointF point) { - return getYVal(point.y); + protected Number screenToSeriesY(PointF point) { + return screenToSeriesY(point.y); } /** - * Converts a y pixel to a y value. - * - * @param yPix + * 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 Double getYVal(float yPix) { - if (plot.getCalculatedMinY() == null - || plot.getCalculatedMaxY() == null) { + protected Number screenToSeriesX(float xPix) { + if (!plot.getBounds().xRegion.isDefined()) { return null; } - return ValPixConverter.pixToVal(yPix - gridDimensions.paddedRect.top, plot - .getCalculatedMinY().doubleValue(), plot.getCalculatedMaxY() - .doubleValue(), gridDimensions.paddedRect.height(), true); + return new Region(gridRect.left, gridRect.right) + .transform(xPix, plot.getBounds().getxRegion()); } /** - * Convenience method. Wraps getXVal(float) - * - * @param point + * 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 */ - public Double getXVal(PointF point) { - return getXVal(point.x); + 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); } - /** - * Converts an x pixel into an x value. - * - * @param xPix - * @return - */ - public Double getXVal(float xPix) { - if (plot.getCalculatedMinX() == null - || plot.getCalculatedMaxX() == null) { + protected PointF seriesToScreen(XYCoords xy) { + if (!plot.getBounds().isFullyDefined()) { return null; } - return ValPixConverter.pixToVal(xPix - gridDimensions.paddedRect.left, plot - .getCalculatedMinX().doubleValue(), plot.getCalculatedMaxX() - .doubleValue(), gridDimensions.paddedRect.width(), false); + 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 doOnDraw(Canvas canvas, RectF widgetRect) - throws PlotRenderException { + protected void onResize(@Nullable RectF oldRect, @NonNull RectF newRect) { + recalculateSizes(newRect); + } - calculateGridDimensions(widgetRect); + 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 ((gridDimensions.paddedRect.height() > ZERO) && (gridDimensions.paddedRect.width() > ZERO)) { - if (plot.getCalculatedMinX() != null - && plot.getCalculatedMaxX() != null - && plot.getCalculatedMinY() != null - && plot.getCalculatedMaxY() != null) { - drawGrid(canvas); - drawData(canvas); + if (gridRect.height() > ZERO && gridRect.width() > ZERO) { + final RectRegion bounds = plot.getBounds(); + if (bounds.getMinX() != null + && bounds.getMaxX() != null + && bounds.getMinY() != null + && bounds.getMaxY() != null) { + if (drawGridOnTop) { + drawData(canvas); + drawGrid(canvas); + } else { + drawGrid(canvas); + drawData(canvas); + } drawCursors(canvas); if (isDrawMarkersEnabled()) { drawMarkers(canvas); @@ -431,476 +563,257 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect) } } - private void calculateGridDimensions(RectF widgetRect) { - RectF r = new RectF(widgetRect.left + ((rangeAxisLeft) ? - rangeTickLabelWidth : ONE), - widgetRect.top + ((domainAxisBottom) ? ONE : domainTickLabelWidth), - widgetRect.right - ((rangeAxisLeft) ? ONE : rangeTickLabelWidth), - widgetRect.bottom - ((domainAxisBottom) ? domainTickLabelWidth : ONE)); + protected void drawDomainLine(Canvas canvas, float xPix, Number xVal, + Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) { - // don't calculate if nothing has changed: - if(gridDimensions == null || !RectFUtils.areIdentical(r, gridDimensions.canvasRect)) { - RectF mRect = gridBox.getMarginatedRect(r); - RectF pRect = gridBox.getPaddedRect(mRect); - gridDimensions = new DisplayDimensions(r, mRect, pRect); + // lines + if (linePaint != null) { + canvas.drawLine(xPix, gridRect.top - lineExtensionTop, + xPix, gridRect.bottom + lineExtensionBottom, linePaint + ); } - } - private void drawTickText(Canvas canvas, XYAxisType axis, Number value, - float xPix, float yPix, Paint labelPaint) { - AxisValueLabelFormatter rf = null; - String txt = null; - double v = value.doubleValue(); - - int canvasState = canvas.save(); - try { - switch (axis) { - case DOMAIN: - rf = getAxisValueLabelFormatterForDomainVal(v); - txt = getFormattedDomainValue(value); - canvas.rotate(getDomainLabelOrientation(), xPix, yPix); - break; - case RANGE: - rf = getAxisValueLabelFormatterForRangeVal(v); - txt = getFormattedRangeValue(value); - canvas.rotate(getRangeLabelOrientation(), xPix, yPix); - break; - default: - throw new RuntimeException("Invalid axis type: " + axis); + // labels + if(shouldDrawLabel) { + if (isLineLabelEnabled(Edge.TOP)) { + drawLineLabel(canvas, Edge.TOP, xVal, xPix, labelRect.top, isOrigin); } - // if a matching region formatter was found, create a clone - // of labelPaint and use the formatter's color. Otherwise - // just use labelPaint: - Paint p; - if (rf != null) { - p = new Paint(labelPaint); - p.setColor(rf.getColor()); - } else { - p = labelPaint; + if (isLineLabelEnabled(Edge.BOTTOM)) { + drawLineLabel(canvas, Edge.BOTTOM, xVal, xPix, labelRect.bottom, isOrigin); } - canvas.drawText(txt, xPix, yPix, p); - } finally { - canvas.restoreToCount(canvasState); } } - private void drawDomainTick(Canvas canvas, float xPix, Number xVal, - Paint labelPaint, Paint linePaint, boolean drawLineOnly) { + protected void drawRangeLine(Canvas canvas, float yPix, Number yVal, + Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) { + // lines + if (linePaint != null) { + canvas.drawLine(gridRect.left - lineExtensionLeft, yPix, + gridRect.right + lineExtensionRight, yPix, linePaint + ); + } - final RectF gridRect = gridDimensions.paddedRect; - if (!drawLineOnly) { - if (linePaint != null && (domainTick || domainTickExtension > ZERO)) { - if (domainAxisBottom){ - canvas.drawLine(xPix, domainTick ? gridRect.top : gridRect.bottom, - xPix, gridRect.bottom + domainTickExtension, linePaint); - } else { - canvas.drawLine(xPix, gridRect.top - domainTickExtension, - xPix, domainTick ? gridRect.bottom : gridRect.top, linePaint); - } - } - if (labelPaint != null) { - float fontHeight = FontUtils.getFontHeight(labelPaint); - float yPix; - if (domainAxisBottom){ - yPix = gridRect.bottom + domainTickExtension - + domainTickLabelVerticalOffset + fontHeight; - } else { - yPix = gridRect.top - domainTickExtension - - domainTickLabelVerticalOffset; - } - drawTickText(canvas, XYAxisType.DOMAIN, xVal, - xPix + domainTickLabelHorizontalOffset, yPix, - labelPaint); + if(shouldDrawLabel) { + // labels + if (isLineLabelEnabled(Edge.LEFT)) { + drawLineLabel(canvas, Edge.LEFT, yVal, labelRect.left, yPix, isOrigin); } - } else if (linePaint != null && (domainSubTick || domainLabelSubTickExtension > ZERO)) { - if (domainAxisBottom){ - canvas.drawLine(xPix, domainSubTick ? gridRect.top : gridRect.bottom, - xPix, gridRect.bottom + domainLabelSubTickExtension, linePaint); - } else { - canvas.drawLine(xPix, gridRect.top - domainLabelSubTickExtension, - xPix, domainSubTick ? gridRect.bottom : gridRect.top, linePaint); + if (isLineLabelEnabled(Edge.RIGHT)) { + drawLineLabel(canvas, Edge.RIGHT, yVal, labelRect.right, yPix, isOrigin); } } } - public void drawRangeTick(Canvas canvas, float yPix, Number yVal, - Paint labelPaint, Paint linePaint, boolean drawLineOnly) { - final RectF gridRect = gridDimensions.paddedRect; - if (!drawLineOnly) { - if (linePaint != null && (rangeTick || rangeTickExtension > ZERO)) { - if (rangeAxisLeft){ - canvas.drawLine(gridRect.left - rangeTickExtension, yPix, - rangeTick ? gridRect.right : gridRect.left, yPix, linePaint); - } else { - canvas.drawLine(rangeTick ? gridRect.left : gridRect.right, yPix, - gridRect.right + rangeTickExtension, yPix, linePaint); - } - } - if (labelPaint != null) { - float xPix; - if (rangeAxisLeft){ - xPix = gridRect.left - - (rangeTickExtension + rangeTickLabelHorizontalOffset); - } else { - xPix = gridRect.right - + (rangeTickExtension + rangeTickLabelHorizontalOffset); - } - drawTickText(canvas, XYAxisType.RANGE, yVal, xPix, yPix - rangeTickLabelVerticalOffset, - labelPaint); - } - } else if (linePaint != null && (rangeSubTick || rangeLabelSubTickExtension > ZERO)) { - if (rangeAxisLeft){ - canvas.drawLine(gridRect.left - rangeLabelSubTickExtension, yPix, - rangeSubTick ? gridRect.right : gridRect.left, yPix, linePaint); - } else { - canvas.drawLine(rangeTick ? gridRect.left : gridRect.right, yPix, - gridRect.right + rangeLabelSubTickExtension, yPix, linePaint); - } - } + 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) { - final RectF paddedGridRect = gridDimensions.paddedRect; - if (gridBackgroundPaint != null) { - canvas.drawRect(gridDimensions.paddedRect, gridBackgroundPaint); + if (!drawGridOnTop) { + drawGridBackground(canvas); } - float domainOriginF; - if (plot.getDomainOrigin() != null) { - double domainOriginVal = plot.getDomainOrigin().doubleValue(); - domainOriginF = ValPixConverter.valToPix(domainOriginVal, plot - .getCalculatedMinX().doubleValue(), plot - .getCalculatedMaxX().doubleValue(), paddedGridRect.width(), - false); - domainOriginF += paddedGridRect.left; - // if no origin is set, use the leftmost value visible on the grid: + + Number domainOrigin = plot.getDomainOrigin(); + final double domainOriginPix; + if (domainOrigin != null) { + domainOriginPix = plot.getBounds().getxRegion().transform( + plot.getDomainOrigin().doubleValue(), gridRect.left, gridRect.right, false); } else { - domainOriginF = paddedGridRect.left; + // if no domain origin is set, use the leftmost value visible on the grid: + domainOriginPix = gridRect.left; + domainOrigin = plot.getBounds().getMinX(); } - XYStep domainStep = XYStepCalculator.getStep(plot, XYAxisType.DOMAIN, - paddedGridRect, plot.getCalculatedMinX().doubleValue(), plot - .getCalculatedMaxX().doubleValue()); - - - // draw domain origin: - if (domainOriginF >= paddedGridRect.left - && domainOriginF <= paddedGridRect.right) { - if (domainOriginLinePaint != null){ - domainOriginLinePaint.setTextAlign(Paint.Align.CENTER); - } - - Paint olp = domainTickLabelPaintMap != null ? - domainTickLabelPaintMap.get(plot.getDomainOrigin()) : domainOriginTickLabelPaint; - if(olp == null) { - olp = domainTickLabelPaint; - } - drawDomainTick(canvas, domainOriginF, plot.getDomainOrigin() - .doubleValue(), olp, domainOriginLinePaint, false); - } + Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect); - // draw ticks LEFT of origin: - float xPix = domainOriginF - domainStep.getStepPix(); - for (int i = ONE; xPix >= paddedGridRect.left; xPix = domainOriginF - - (i * domainStep.getStepPix())) { - double xVal = plot.getDomainOrigin().doubleValue() - i - * domainStep.getStepVal(); - Paint dlp = domainTickLabelPaintMap != null ? - domainTickLabelPaintMap.get(xVal) : domainTickLabelPaint; - if (dlp == null) { - dlp = domainTickLabelPaint; - } - if (xPix >= paddedGridRect.left && xPix <= paddedGridRect.right) { - if (i % getTicksPerDomainLabel() == ZERO) { - drawDomainTick(canvas, xPix, xVal, dlp, domainGridLinePaint, false); - } else { - drawDomainTick(canvas, xPix, xVal, dlp, domainSubGridLinePaint, true); - } - } - i++; - } + // Draw Domain Lines: - // draw ticks RIGHT of origin: - xPix = domainOriginF + domainStep.getStepPix(); - for (int i = ONE; xPix <= paddedGridRect.right; xPix = domainOriginF - + (i * domainStep.getStepPix())) { - double xVal = plot.getDomainOrigin().doubleValue() + i - * domainStep.getStepVal(); - - Paint dlp = domainTickLabelPaintMap != null ? - domainTickLabelPaintMap.get(xVal) : domainTickLabelPaint; - if (dlp == null) { - dlp = domainTickLabelPaint; - } - if (xPix >= paddedGridRect.left && xPix <= paddedGridRect.right) { + final double domainStepPix = domainStep.getStepPix(); + final double iMin = (gridRect.left - domainOriginPix - FUDGE) / domainStepPix; + final double iMax = (gridRect.right - domainOriginPix + FUDGE) / domainStepPix; - if (i % getTicksPerDomainLabel() == ZERO) { - drawDomainTick(canvas, xPix, xVal, dlp, domainGridLinePaint, false); - } else { - drawDomainTick(canvas, xPix, xVal, dlp, domainSubGridLinePaint, true); - } + 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); } - // draw range origin: - float rangeOriginF; - if (plot.getRangeOrigin() != null) { - double rangeOriginD = plot.getRangeOrigin().doubleValue(); - rangeOriginF = ValPixConverter.valToPix(rangeOriginD, plot - .getCalculatedMinY().doubleValue(), plot - .getCalculatedMaxY().doubleValue(), - paddedGridRect.height(), true); - rangeOriginF += paddedGridRect.top; - // if no origin is set, use the leftmost value visible on the grid + Number rangeOrigin = plot.getRangeOrigin(); + final double rangeOriginPix; + if (rangeOrigin != null) { + rangeOriginPix = plot.getBounds().getyRegion().transform( + rangeOrigin.doubleValue(), gridRect.top, gridRect.bottom, true); } else { - rangeOriginF = paddedGridRect.bottom; - } - - XYStep rangeStep = XYStepCalculator.getStep(plot, XYAxisType.RANGE, - paddedGridRect, plot.getCalculatedMinY().doubleValue(), plot - .getCalculatedMaxY().doubleValue()); - - // draw range origin: - if (rangeOriginF >= paddedGridRect.top - && rangeOriginF <= paddedGridRect.bottom) { - if (rangeOriginLinePaint != null){ - rangeOriginLinePaint.setTextAlign(Paint.Align.RIGHT); - } - - Paint olp = rangeTickLabelPaintMap != null ? - rangeTickLabelPaintMap.get(plot.getRangeOrigin()) : rangeOriginTickLabelPaint; - if(olp == null) { - olp = rangeTickLabelPaint; - } - drawRangeTick(canvas, rangeOriginF, plot.getRangeOrigin() - .doubleValue(), olp, - rangeOriginLinePaint, false); - } - - // draw ticks ABOVE origin: - float yPix = rangeOriginF - rangeStep.getStepPix(); - for (int i = ONE; yPix >= paddedGridRect.top; yPix = rangeOriginF - - (i * rangeStep.getStepPix())) { - double yVal = plot.getRangeOrigin().doubleValue() + i - * rangeStep.getStepVal(); - - Paint rlp = rangeTickLabelPaintMap != null ? - rangeTickLabelPaintMap.get(yVal) : rangeTickLabelPaint; - if (rlp == null) { - rlp = rangeTickLabelPaint; - } - if (yPix >= paddedGridRect.top && yPix <= paddedGridRect.bottom) { - if (i % getTicksPerRangeLabel() == ZERO) { - drawRangeTick(canvas, yPix, yVal, rlp, - rangeGridLinePaint, false); - } else { - drawRangeTick(canvas, yPix, yVal, rlp, - rangeSubGridLinePaint, true); - } - } - i++; + // if no range origin is set, use the bottom-most value visible on the grid: + rangeOriginPix = gridRect.bottom; + rangeOrigin = plot.getBounds().getMinY(); } - // draw ticks BENEATH origin: - yPix = rangeOriginF + rangeStep.getStepPix(); - for (int i = ONE; yPix <= paddedGridRect.bottom; yPix = rangeOriginF - + (i * rangeStep.getStepPix())) { - double yVal = plot.getRangeOrigin().doubleValue() - i - * rangeStep.getStepVal(); - - Paint rlp = rangeTickLabelPaintMap != null ? - rangeTickLabelPaintMap.get(yVal) : rangeTickLabelPaint; - if (rlp == null) { - rlp = rangeTickLabelPaint; - } - if (yPix >= paddedGridRect.top && yPix <= paddedGridRect.bottom) { - if (i % getTicksPerRangeLabel() == ZERO) { - drawRangeTick(canvas, yPix, yVal, rlp, - rangeGridLinePaint, false); - } else { - drawRangeTick(canvas, yPix, yVal, rlp, - rangeSubGridLinePaint, true); - } + Step rangeStep = XYStepCalculator.getStep(plot, Axis.RANGE, gridRect); + + // Draw Range Lines: + + final double rangeStepPix = rangeStep.getStepPix(); + 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++; - } - } - - /** - * 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) { - final RectF paddedRect = gridDimensions.paddedRect; - 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 > paddedRect.right) { - textRect.offset(-(textRect.right - paddedRect.right), ZERO); + drawRangeLine(canvas, (float) yPix, yVal, linePaint, isOrigin, isMajorTick); } - - if (textRect.top < paddedRect.top) { - textRect.offset(0, paddedRect.top - textRect.top); - } - - canvas.drawText(text, textRect.left, textRect.bottom, - marker.getTextPaint()); - } protected void drawMarkers(Canvas canvas) { - for (YValueMarker marker : plot.getYValueMarkers()) { - - final RectF paddedRect = gridDimensions.paddedRect; - - if (marker.getValue() != null) { - double yVal = marker.getValue().doubleValue(); - float yPix = ValPixConverter.valToPix(yVal, plot - .getCalculatedMinY().doubleValue(), plot - .getCalculatedMaxY().doubleValue(), paddedRect - .height(), true); - yPix += paddedRect.top; - canvas.drawLine(paddedRect.left, yPix, - paddedRect.right, yPix, marker.getLinePaint()); - - float xPix = marker.getTextPosition().getPixelValue( - paddedRect.width()); - xPix += paddedRect.left; - - if (marker.getText() != null) { - drawMarkerText(canvas, marker.getText(), marker, xPix, yPix); - } else { - drawMarkerText(canvas, - getFormattedRangeValue(marker.getValue()), marker, - xPix, yPix); - } + if (plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) { + for (YValueMarker marker : plot.getYValueMarkers()) { + marker.draw(canvas, plot, gridRect); } } - for (XValueMarker marker : plot.getXValueMarkers()) { - - final RectF paddedRect = gridDimensions.paddedRect; - - if (marker.getValue() != null) { - double xVal = marker.getValue().doubleValue(); - float xPix = ValPixConverter.valToPix(xVal, plot - .getCalculatedMinX().doubleValue(), plot - .getCalculatedMaxX().doubleValue(), paddedRect - .width(), false); - xPix += paddedRect.left; - canvas.drawLine(xPix, paddedRect.top, xPix, - paddedRect.bottom, marker.getLinePaint()); - - float yPix = marker.getTextPosition().getPixelValue( - paddedRect.height()); - yPix += paddedRect.top; - if (marker.getText() != null) { - drawMarkerText(canvas, marker.getText(), marker, xPix, yPix); - } else { - drawMarkerText(canvas, - getFormattedDomainValue(marker.getValue()), marker, - xPix, yPix); - } + if (plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) { + for (XValueMarker marker : plot.getXValueMarkers()) { + marker.draw(canvas, plot, gridRect); } } } protected void drawCursors(Canvas canvas) { - - final RectF paddedRect = gridDimensions.paddedRect; boolean hasDomainCursor = false; // draw the domain cursor: if (domainCursorPaint != null - && domainCursorPosition <= paddedRect.right - && domainCursorPosition >= paddedRect.left) { + && domainCursorPosition != null + && domainCursorPosition <= gridRect.right + && domainCursorPosition >= gridRect.left) { hasDomainCursor = true; - canvas.drawLine(domainCursorPosition, paddedRect.top, - domainCursorPosition, paddedRect.bottom, - domainCursorPaint); + canvas.drawLine(domainCursorPosition, gridRect.top, + domainCursorPosition, gridRect.bottom, + domainCursorPaint + ); } boolean hasRangeCursor = false; // draw the range cursor: if (rangeCursorPaint != null - && rangeCursorPosition >= paddedRect.top - && rangeCursorPosition <= paddedRect.bottom) { + && rangeCursorPosition != null + && rangeCursorPosition >= gridRect.top + && rangeCursorPosition <= gridRect.bottom) { hasRangeCursor = true; - canvas.drawLine(paddedRect.left, rangeCursorPosition, - paddedRect.right, rangeCursorPosition, rangeCursorPaint); + canvas.drawLine(gridRect.left, rangeCursorPosition, + gridRect.right, rangeCursorPosition, rangeCursorPaint + ); } - if (drawCursorLabelEnabled && cursorLabelPaint != null - && hasRangeCursor && hasDomainCursor) { + if (getCursorLabelFormatter() != null && hasRangeCursor && hasDomainCursor) { + drawCursorLabel(canvas); + } + } - String label = "X=" - + getDomainValueFormat().format(getDomainCursorVal()); - label += " Y=" + getRangeValueFormat().format(getRangeCursorVal()); + protected void drawCursorLabel(Canvas canvas) { + final String label = getCursorLabelFormatter(). + getLabelText(getDomainCursorVal(), getRangeCursorVal()); - // convert the label dimensions rect into floating-point: - RectF cursorRect = new RectF(FontUtils.getPackedStringDimensions( - label, cursorLabelPaint)); - cursorRect.offsetTo(domainCursorPosition, rangeCursorPosition - - cursorRect.height()); + // convert the label dimensions rect into floating-point: + RectF cursorRect = new RectF(FontUtils.getPackedStringDimensions( + label, getCursorLabelFormatter().getTextPaint())); + cursorRect.offsetTo(domainCursorPosition, rangeCursorPosition + - cursorRect.height()); - // 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 >= paddedRect.right) { - cursorRect.offsetTo(domainCursorPosition - cursorRect.width(), - cursorRect.top); - } + // 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 + ); + } - // same thing for the top edge of the plot: - // dunno why but these rects can have negative values for top and bottom. - if (cursorRect.top <= paddedRect.top) { - cursorRect.offsetTo(cursorRect.left, rangeCursorPosition); - } + // same thing for the top edge of the plot: + // dunno why but these rects can have negative values for top and bottom. + if (cursorRect.top <= gridRect.top) { + cursorRect.offsetTo(cursorRect.left, rangeCursorPosition); + } - if (cursorLabelBackgroundPaint != null) { - canvas.drawRect(cursorRect, cursorLabelBackgroundPaint); - } + if (getCursorLabelFormatter().getBackgroundPaint() != null) { + canvas.drawRect(cursorRect, getCursorLabelFormatter().getBackgroundPaint()); + } - canvas.drawText(label, cursorRect.left, cursorRect.bottom, - cursorLabelPaint); + canvas.drawText(label, cursorRect.left, cursorRect.bottom, + getCursorLabelFormatter().getTextPaint() + ); + } + + protected void drawGridBackground(Canvas canvas) { + if (gridBackgroundPaint != null) { + canvas.drawRect(gridRect, gridBackgroundPaint); } } /** * 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 { - canvas.save(Canvas.ALL_SAVE_FLAG); - canvas.clipRect(gridDimensions.marginatedRect, android.graphics.Region.Op.INTERSECT); + if (isGridClippingEnabled) { + canvas.save(); + canvas.clipRect(gridRect, android.graphics.Region.Op.INTERSECT); + } + renderStack.sync(); - for(RenderStack.StackElement thisElement : renderStack.getElements()) { - if(thisElement.isEnabled()) { - plot.getRenderer(thisElement.get().getFormatter().getRendererClass()). - render(canvas, gridDimensions.paddedRect, thisElement.get(), renderStack); + for (RenderStack.StackElement thisElement : renderStack.getElements()) { + if (thisElement.isEnabled()) { + Class rendererClass = + thisElement.get().getFormatter().getRendererClass(); + plot.getRenderer(rendererClass).render( + canvas, gridRect, thisElement.get(), renderStack); } } } finally { - canvas.restore(); + if (isGridClippingEnabled) { + canvas.restore(); + } } } @@ -908,54 +821,6 @@ protected void drawPoint(Canvas canvas, PointF point, Paint paint) { canvas.drawPoint(point.x, point.y, paint); } - public float getDomainTickLabelWidth() { - return domainTickLabelWidth; - } - - public void setDomainTickLabelWidth(float domainTickLabelWidth) { - this.domainTickLabelWidth = domainTickLabelWidth; - } - - public float getRangeTickLabelWidth() { - return rangeTickLabelWidth; - } - - public void setRangeTickLabelWidth(float rangeTickLabelWidth) { - this.rangeTickLabelWidth = rangeTickLabelWidth; - } - - public float getDomainTickLabelVerticalOffset() { - return domainTickLabelVerticalOffset; - } - - public void setDomainTickLabelVerticalOffset(float domainTickLabelVerticalOffset) { - this.domainTickLabelVerticalOffset = domainTickLabelVerticalOffset; - } - - public float getDomainTickLabelHorizontalOffset() { - return domainTickLabelHorizontalOffset; - } - - public void setDomainTickLabelHorizontalOffset(float domainTickLabelHorizontalOffset) { - this.domainTickLabelHorizontalOffset = domainTickLabelHorizontalOffset; - } - - public float getRangeTickLabelHorizontalOffset() { - return rangeTickLabelHorizontalOffset; - } - - public void setRangeTickLabelHorizontalOffset(float rangeTickLabelHorizontalOffset) { - this.rangeTickLabelHorizontalOffset = rangeTickLabelHorizontalOffset; - } - - public float getRangeTickLabelVerticalOffset() { - return rangeTickLabelVerticalOffset; - } - - public void setRangeTickLabelVerticalOffset(float rangeTickLabelVerticalOffset) { - this.rangeTickLabelVerticalOffset = rangeTickLabelVerticalOffset; - } - public Paint getGridBackgroundPaint() { return gridBackgroundPaint; } @@ -964,22 +829,6 @@ public void setGridBackgroundPaint(Paint gridBackgroundPaint) { this.gridBackgroundPaint = gridBackgroundPaint; } - public Paint getDomainTickLabelPaint() { - return domainTickLabelPaint; - } - - public void setDomainTickLabelPaint(Paint domainTickLabelPaint) { - this.domainTickLabelPaint = domainTickLabelPaint; - } - - public Paint getRangeTickLabelPaint() { - return rangeTickLabelPaint; - } - - public void setRangeTickLabelPaint(Paint rangeTickLabelPaint) { - this.rangeTickLabelPaint = rangeTickLabelPaint; - } - /** * Get the paint used to draw the domain grid line. */ @@ -989,6 +838,7 @@ public Paint getDomainGridLinePaint() { /** * Set the paint used to draw the domain grid line. + * * @param gridLinePaint */ public void setDomainGridLinePaint(Paint gridLinePaint) { @@ -1008,9 +858,10 @@ public Paint getRangeGridLinePaint() { public Paint getDomainSubGridLinePaint() { return domainSubGridLinePaint; } - + /** * Set the paint used to draw the domain grid line. + * * @param gridLinePaint */ public void setDomainSubGridLinePaint(Paint gridLinePaint) { @@ -1019,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) { @@ -1034,301 +886,298 @@ public Paint getRangeSubGridLinePaint() { /** * Set the Paint used to draw the range grid line. + * * @param gridLinePaint */ public void setRangeSubGridLinePaint(Paint gridLinePaint) { this.rangeSubGridLinePaint = gridLinePaint; } - - // TODO: make a generic renderer queue. - public Format getRangeValueFormat() { - return rangeValueFormat; + public int getLinesPerRangeLabel() { + return linesPerRangeLabel; } - public void setRangeValueFormat(Format rangeValueFormat) { - this.rangeValueFormat = rangeValueFormat; + public void setLinesPerRangeLabel(int linesPerRangeLabel) { + this.linesPerRangeLabel = linesPerRangeLabel; } - public Format getDomainValueFormat() { - return domainValueFormat; + public int getLinesPerDomainLabel() { + return linesPerDomainLabel; } - public void setDomainValueFormat(Format domainValueFormat) { - this.domainValueFormat = domainValueFormat; + public void setLinesPerDomainLabel(int linesPerDomainLabel) { + this.linesPerDomainLabel = linesPerDomainLabel; } - public int getDomainTickExtension() { - return domainTickExtension; + public Paint getDomainOriginLinePaint() { + return domainOriginLinePaint; + } + + public void setDomainOriginLinePaint(Paint domainOriginLinePaint) { + this.domainOriginLinePaint = domainOriginLinePaint; } - public void setDomainTickExtension(int domainTickExtension) { - this.domainTickExtension = domainTickExtension; + public Paint getRangeOriginLinePaint() { + return rangeOriginLinePaint; } - public int getRangeTickExtension() { - return rangeTickExtension; + public void setRangeOriginLinePaint(Paint rangeOriginLinePaint) { + this.rangeOriginLinePaint = rangeOriginLinePaint; } - public int getDomainLabelSubTickExtension() { - return domainLabelSubTickExtension; + /** + * Set domain and range cursor position using screen coordinates + * + * @param x + * @param y + */ + public void setCursorPosition(Float x, Float y) { + setDomainCursorPosition(x); + setRangeCursorPosition(y); } - public void setDomainLabelSubTickExtension(int domainLabelSubTickExtension) { - this.domainLabelSubTickExtension = domainLabelSubTickExtension; + /** + * Set domain and range cursor position using screen coordinates + * + * @param point + */ + public void setCursorPosition(PointF point) { + setCursorPosition(point.x, point.y); } - public void setRangeTickExtension(int rangeTickExtension) { - this.rangeTickExtension = rangeTickExtension; + public Float getDomainCursorPosition() { + return domainCursorPosition; } - public int getRangeLabelSubTickExtension() { - return rangeLabelSubTickExtension; + public Number getDomainCursorVal() { + return screenToSeriesX(getDomainCursorPosition()); } - public void setRangeLabelSubTickExtension(int rangeLabelSubTickExtension) { - this.rangeLabelSubTickExtension = rangeLabelSubTickExtension; + /** + * Set domain cursor position using screen coordinates + * + * @param domainCursorPosition + */ + public void setDomainCursorPosition(Float domainCursorPosition) { + this.domainCursorPosition = domainCursorPosition; } - public int getTicksPerRangeLabel() { - return ticksPerRangeLabel; + public Float getRangeCursorPosition() { + return rangeCursorPosition; } - public void setTicksPerRangeLabel(int ticksPerRangeLabel) { - this.ticksPerRangeLabel = ticksPerRangeLabel; + public Number getRangeCursorVal() { + return screenToSeriesY(getRangeCursorPosition()); } - public int getTicksPerDomainLabel() { - return ticksPerDomainLabel; + /** + * Set range cursor position using screen coordinates + * + * @param rangeCursorPosition + */ + public void setRangeCursorPosition(Float rangeCursorPosition) { + this.rangeCursorPosition = rangeCursorPosition; } - public void setTicksPerDomainLabel(int ticksPerDomainLabel) { - this.ticksPerDomainLabel = ticksPerDomainLabel; + public boolean isDrawGridOnTop() { + return drawGridOnTop; } - public Paint getDomainOriginLinePaint() { - return domainOriginLinePaint; + public void setDrawGridOnTop(boolean drawGridOnTop) { + this.drawGridOnTop = drawGridOnTop; } - public void setDomainOriginLinePaint(Paint domainOriginLinePaint) { - this.domainOriginLinePaint = domainOriginLinePaint; + public boolean isDrawMarkersEnabled() { + return drawMarkersEnabled; } - public Paint getRangeOriginLinePaint() { - return rangeOriginLinePaint; + public void setDrawMarkersEnabled(boolean drawMarkersEnabled) { + this.drawMarkersEnabled = drawMarkersEnabled; } - public void setRangeOriginLinePaint(Paint rangeOriginLinePaint) { - this.rangeOriginLinePaint = rangeOriginLinePaint; + public Paint getDomainCursorPaint() { + return domainCursorPaint; } - public Paint getDomainOriginTickLabelPaint() { - return domainOriginTickLabelPaint; + /** + * @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; } - public void setDomainOriginTickLabelPaint(Paint domainOriginTickLabelPaint) { - this.domainOriginTickLabelPaint = domainOriginTickLabelPaint; + public Paint getRangeCursorPaint() { + return rangeCursorPaint; } - public Paint getRangeOriginTickLabelPaint() { - return rangeOriginTickLabelPaint; + /** + * @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; } - public void setRangeOriginTickLabelPaint(Paint rangeOriginTickLabelPaint) { - this.rangeOriginTickLabelPaint = rangeOriginTickLabelPaint; + public float getLineExtensionTop() { + return lineExtensionTop; } - public void setCursorPosition(float x, float y) { - setDomainCursorPosition(x); - setRangeCursorPosition(y); + public void setLineExtensionTop(float lineExtensionTop) { + this.lineExtensionTop = lineExtensionTop; } - public void setCursorPosition(PointF point) { - setCursorPosition(point.x, point.y); + public float getLineExtensionBottom() { + return lineExtensionBottom; } - public float getDomainCursorPosition() { - return domainCursorPosition; + public void setLineExtensionBottom(float lineExtensionBottom) { + this.lineExtensionBottom = lineExtensionBottom; } - public Double getDomainCursorVal() { - return getXVal(getDomainCursorPosition()); + public float getLineExtensionLeft() { + return lineExtensionLeft; } - public void setDomainCursorPosition(float domainCursorPosition) { - this.domainCursorPosition = domainCursorPosition; + public void setLineExtensionLeft(float lineExtensionLeft) { + this.lineExtensionLeft = lineExtensionLeft; } - public float getRangeCursorPosition() { - return rangeCursorPosition; + public float getLineExtensionRight() { + return lineExtensionRight; } - public Double getRangeCursorVal() { - return getYVal(getRangeCursorPosition()); + public void setLineExtensionRight(float lineExtensionRight) { + this.lineExtensionRight = lineExtensionRight; } - public void setRangeCursorPosition(float rangeCursorPosition) { - this.rangeCursorPosition = rangeCursorPosition; + 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()); + defaults.put(Edge.RIGHT, new LineLabelStyle()); + return defaults; } - public Paint getCursorLabelPaint() { - return cursorLabelPaint; + 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()); + defaults.put(Edge.RIGHT, new LineLabelRenderer()); + return defaults; } - public void setCursorLabelPaint(Paint cursorLabelPaint) { - this.cursorLabelPaint = cursorLabelPaint; + public LineLabelRenderer getLineLabelRenderer(Edge edge) { + return lineLabelRenderers.get(edge); } - public Paint getCursorLabelBackgroundPaint() { - return cursorLabelBackgroundPaint; + public void setLineLabelRenderer(Edge edge, LineLabelRenderer renderer) { + lineLabelRenderers.put(edge, renderer); } - public void setCursorLabelBackgroundPaint(Paint cursorLabelBackgroundPaint) { - this.cursorLabelBackgroundPaint = cursorLabelBackgroundPaint; + public LineLabelStyle getLineLabelStyle(Edge edge) { + return lineLabelStyles.get(edge); } - public boolean isDrawMarkersEnabled() { - return drawMarkersEnabled; + public void setLineLabelStyle(Edge edge, LineLabelStyle style) { + lineLabelStyles.put(edge, style); } - public void setDrawMarkersEnabled(boolean drawMarkersEnabled) { - this.drawMarkersEnabled = drawMarkersEnabled; + public CursorLabelFormatter getCursorLabelFormatter() { + return cursorLabelFormatter; } - public boolean isRangeAxisLeft() { - return rangeAxisLeft; + public void setCursorLabelFormatter( + CursorLabelFormatter cursorLabelFormatter) { + this.cursorLabelFormatter = cursorLabelFormatter; } - public void setRangeAxisLeft(boolean rangeAxisLeft) { - this.rangeAxisLeft = rangeAxisLeft; + /** + * Grid insets + */ + public Insets getGridInsets() { + return gridInsets; } - public boolean isDomainAxisBottom() { - return domainAxisBottom; + public void setGridInsets(Insets gridInsets) { + this.gridInsets = gridInsets; + recalculateSizes(null); } - public void setDomainAxisBottom(boolean domainAxisBottom) { - this.domainAxisBottom = domainAxisBottom; + /** + * Domain / Range label insets + */ + public Insets getLineLabelInsets() { + return lineLabelInsets; } - - public boolean isRangeTick() { - return rangeTick; + + public void setLineLabelInsets(Insets lineLabelInsets) { + this.lineLabelInsets = lineLabelInsets; + recalculateSizes(null); } - public void setRangeTick(boolean rangeTick) { - this.rangeTick = rangeTick; + public RectF getGridRect() { + return gridRect; } - public boolean isRangeSubTick() { - return rangeSubTick; + public void setGridRect(RectF gridRect) { + this.gridRect = gridRect; } - public void setRangeSubTick(boolean rangeSubTick) { - this.rangeSubTick = rangeSubTick; + public RectF getLabelRect() { + return labelRect; } - public boolean isDomainTick() { - return domainTick; + public void setLabelRect(RectF labelRect) { + this.labelRect = labelRect; } - public void setDomainTick(boolean domainTick) { - this.domainTick = domainTick; + public boolean isGridClippingEnabled() { + return isGridClippingEnabled; } - public boolean isDomainSubTick() { - return domainSubTick; + public void setGridClippingEnabled(boolean gridClippingEnabled) { + isGridClippingEnabled = gridClippingEnabled; } - public void setDomainSubTick(boolean domainSubTick) { - this.domainSubTick = domainSubTick; + public boolean isLineLabelEnabled(Edge position) { + return lineLabelEdges.contains(position); } - /* - * set the position of the range axis labels. Set the labelPaint textSizes before setting this. - * This call sets the various vertical and horizontal offsets and widths to good defaults. - * - * @param rangeAxisLeft axis labels are on the left hand side not the right hand side. - * @param rangeAxisOverlay axis labels are overlaid on the plot, not external to it. - * @param tickSize the size of the tick extensions for none overlaid axis. - * @param maxLableString Sample label representing the biggest size space needs to be allocated for. - */ - public void setRangeAxisPosition(boolean rangeAxisLeft, boolean rangeAxisOverlay, int tickSize, String maxLableString){ - setRangeAxisLeft(rangeAxisLeft); - - if (rangeAxisOverlay) { - setRangeTickLabelWidth(1); // needs to be at least 1 to display grid line. - setRangeTickLabelHorizontalOffset(-TWO); - setRangeTickLabelVerticalOffset(TWO); // get above the line - Paint p = getRangeTickLabelPaint(); - if (p != null) { - p.setTextAlign(((rangeAxisLeft)?Paint.Align.LEFT:Paint.Align.RIGHT)); - } - Paint po = getRangeOriginTickLabelPaint(); - if (po != null) { - po.setTextAlign(((rangeAxisLeft)?Paint.Align.LEFT:Paint.Align.RIGHT)); - } - setRangeTickExtension(ZERO); - } else { - setRangeTickLabelWidth(ONE); // needs to be at least 1 to display grid line. - // if we have a paint this gets bigger. - setRangeTickLabelHorizontalOffset(FLOAT_ONE); - setRangeTickExtension(tickSize); - Paint p = getRangeTickLabelPaint(); - if (p != null) { - p.setTextAlign(((!rangeAxisLeft)?Paint.Align.LEFT:Paint.Align.RIGHT)); - Rect r = FontUtils.getPackedStringDimensions(maxLableString,p); - setRangeTickLabelVerticalOffset(r.top / TWO); - setRangeTickLabelWidth(r.right + getRangeTickExtension()); - } - Paint po = getRangeOriginTickLabelPaint(); - if (po != null) { - po.setTextAlign(((!rangeAxisLeft)?Paint.Align.LEFT:Paint.Align.RIGHT)); - } + public void setLineLabelEdges(Edge... positions) { + EnumSet positionSet = EnumSet.noneOf(Edge.class); + if (positions != null) { + Collections.addAll(positionSet, positions); } + this.lineLabelEdges = positionSet; } - - /* - * set the position of the domain axis labels. Set the labelPaint textSizes before setting this. - * This call sets the various vertical and horizontal offsets and widths to good defaults. - * - * @param domainAxisBottom axis labels are on the bottom not the top of the plot. - * @param domainAxisOverlay axis labels are overlaid on the plot, not external to it. - * @param tickSize the size of the tick extensions for non overlaid axis. - * @param maxLableString Sample label representing the biggest size space needs to be allocated for. - */ - public void setDomainAxisPosition(boolean domainAxisBottom, boolean domainAxisOverlay, int tickSize, String maxLabelString){ - setDomainAxisBottom(domainAxisBottom); - if (domainAxisOverlay) { - setDomainTickLabelWidth(ONE); // needs to be at least 1 to display grid line. - setDomainTickLabelVerticalOffset(TWO); // get above the line - setDomainTickExtension(ZERO); - Paint p = getDomainTickLabelPaint(); - if (p != null) { - Rect r = FontUtils.getPackedStringDimensions(maxLabelString,p); - if (domainAxisBottom){ - setDomainTickLabelVerticalOffset(TWO * r.top); - } else { - setDomainTickLabelVerticalOffset(r.top - FLOAT_ONE); - } - } - } else { - setDomainTickLabelWidth(1); // needs to be at least 1 to display grid line. - // if we have a paint this gets bigger. - setDomainTickExtension(tickSize); - Paint p = getDomainTickLabelPaint(); - if (p != null) { - float fontHeight = FontUtils.getFontHeight(p); - if (domainAxisBottom){ - setDomainTickLabelVerticalOffset(-4.0f); - } else { - setDomainTickLabelVerticalOffset(FLOAT_ONE); - } - setDomainTickLabelWidth(fontHeight + getDomainTickExtension()); + + 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) { + lineLabelEdges.add(tp); } } } - public DisplayDimensions getGridDimensions() { - return gridDimensions; + /** + * Checks whether the point exists within the visible grid space. + * + * @param x + * @param y + * @return + * @since 1.0 + */ + public boolean containsPoint(float x, float y) { + if (gridRect != null) { + return gridRect.contains(x, y); + } + return false; } -} +} \ No newline at end of file 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 c4fa1572..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,217 +17,80 @@ package com.androidplot.xy; import android.graphics.*; +import androidx.annotation.NonNull; + import com.androidplot.ui.LayoutManager; -import com.androidplot.ui.SeriesAndFormatter; +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 java.util.*; - -public class XYLegendWidget extends Widget { +import com.androidplot.ui.widget.LegendWidget; - /** - * 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()); - } - } +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Hashtable; +import java.util.List; +import java.util.Map.Entry; - private enum CellType { - SERIES, - REGION - } +/** + * Displays a legend for each series added to the owning {@link XYPlot}. + */ +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.getGraphWidget().getGridBackgroundPaint(); - if(drawIconBackgroundEnabled && bgPaint != null) { - canvas.drawRect(iconRect, bgPaint); - } - } - - private void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, String text) { - - Paint bgPaint = plot.getGraphWidget().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); + } } @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.getSeriesRegistry().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(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 19bae858..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,15 +22,23 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; +import androidx.annotation.NonNull; import android.util.AttributeSet; + import com.androidplot.Plot; import com.androidplot.R; -import com.androidplot.ui.*; -import com.androidplot.ui.TextOrientationType; +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; -import java.text.Format; +import com.androidplot.util.SeriesUtils; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -38,10 +46,7 @@ /** * A View to graphically display x/y coordinates. */ -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; +public class XYPlot extends Plot { private static final int DEFAULT_GRAPH_WIDGET_H_DP = 18; private static final int DEFAULT_GRAPH_WIDGET_W_DP = 10; @@ -52,8 +57,11 @@ public class XYPlot extends Plot private static final int DEFAULT_RANGE_LABEL_WIDGET_H_DP = 50; private static final int DEFAULT_RANGE_LABEL_WIDGET_W_DP = 10; + 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_LEGEND_WIDGET_Y_OFFSET_DP = 0; private static final int DEFAULT_LEGEND_WIDGET_X_OFFSET_DP = 40; + private static final int DEFAULT_GRAPH_WIDGET_Y_OFFSET_DP = 0; private static final int DEFAULT_GRAPH_WIDGET_X_OFFSET_DP = 0; @@ -63,43 +71,27 @@ public class XYPlot extends Plot private static final int DEFAULT_RANGE_LABEL_WIDGET_Y_OFFSET_DP = 0; private static final int DEFAULT_RANGE_LABEL_WIDGET_X_OFFSET_DP = 0; - private static final int DEFAULT_GRAPH_WIDGET_TOP_MARGIN_DP = 3; - private static final int DEFAULT_GRAPH_WIDGET_BOTTOM_MARGIN_DP = 3; - private static final int DEFAULT_GRAPH_WIDGET_LEFT_MARGIN_DP = 3; - private static final int DEFAULT_GRAPH_WIDGET_RIGHT_MARGIN_DP = 3; - - private static final int DEFAULT_PLOT_LEFT_MARGIN_DP = 2; - private static final int DEFAULT_PLOT_RIGHT_MARGIN_DP = 2; - private static final int DEFAULT_PLOT_BOTTOM_MARGIN_DP = 2; + private static final int DEFAULT_PLOT_LEFT_MARGIN_DP = 1; + private static final int DEFAULT_PLOT_RIGHT_MARGIN_DP = 1; + private static final int DEFAULT_PLOT_TOP_MARGIN_DP = 1; + private static final int DEFAULT_PLOT_BOTTOM_MARGIN_DP = 1; private BoundaryMode domainOriginBoundaryMode; private BoundaryMode rangeOriginBoundaryMode; // widgets - private XYLegendWidget legendWidget; - private XYGraphWidget graphWidget; - private TextLabelWidget domainLabelWidget; - private TextLabelWidget rangeLabelWidget; - - //private XYStepMode domainStepMode = XYStepMode.SUBDIVIDE; - //private double domainStepValue = 10; - - //private XYStepMode rangeStepMode = XYStepMode.SUBDIVIDE; - //private double rangeStepValue = 10; - private XYStepModel domainStepModel; - private XYStepModel rangeStepModel; - - // user settable min/max values - private Number userMinX; - private Number userMaxX; - private Number userMinY; - private Number userMaxY; - - // these are the final min/max used for dispplaying data - private Number calculatedMinX; - private Number calculatedMaxX; - private Number calculatedMinY; - private Number calculatedMaxY; + private XYLegendWidget legend; + private XYGraphWidget graph; + private TextLabelWidget domainTitle; + private TextLabelWidget rangeTitle; + + private StepModel domainStepModel; + private StepModel rangeStepModel; + + private XYConstraints constraints = new XYConstraints(); + + // min/max used for displaying data + private RectRegion bounds = RectRegion.withDefaults(new RectRegion(-1, 1, -1, 1)); // previous calculated min/max vals. // primarily used for GROW/SHRINK operations. @@ -108,47 +100,36 @@ public class XYPlot extends Plot private Number prevMinY; private Number prevMaxY; - // uses set boundary min and max values - // should be null if not used. - private Number rangeTopMin = null; - private Number rangeTopMax = null; - private Number rangeBottomMin = null; - private Number rangeBottomMax = null; - private Number domainLeftMin = null; - private Number domainLeftMax = null; - private Number domainRightMin = null; - private Number domainRightMax = null; - - // used for calculating the domain/range extents that will be displayed on the plot. - // using boundaries and origins are mutually exclusive. because of this, - // setting one will disable the other. when only setting the FramingModel, - // the origin or boundary is set to the current value of the plot. - private XYFramingModel domainFramingModel = XYFramingModel.EDGE; - private XYFramingModel rangeFramingModel = XYFramingModel.EDGE; + /** + * The inner and outer limits define a kind of picture-frame shape area that is used as the valid + * region for setting domain/range boundaries. If the set boundaries exceed one of these limits + * then the limit value is used instead of the boundary. This is most commonly used to constrain + * panning & zooming to a specific range on both axes. + */ + private final RectRegion innerLimits = new RectRegion(); + private final RectRegion outerLimits = new RectRegion(); private Number userDomainOrigin; private Number userRangeOrigin; - private Number calculatedDomainOrigin; - private Number calculatedRangeOrigin; + private XYCoords calculatedOrigin = new XYCoords(); @SuppressWarnings("FieldCanBeLocal") private Number domainOriginExtent = null; + @SuppressWarnings("FieldCanBeLocal") private Number rangeOriginExtent = null; - private BoundaryMode domainUpperBoundaryMode = BoundaryMode.AUTO; - private BoundaryMode domainLowerBoundaryMode = BoundaryMode.AUTO; - private BoundaryMode rangeUpperBoundaryMode = BoundaryMode.AUTO; - private BoundaryMode rangeLowerBoundaryMode = BoundaryMode.AUTO; - - private boolean drawDomainOriginEnabled = true; - private boolean drawRangeOriginEnabled = true; - private ArrayList yValueMarkers; private ArrayList xValueMarkers; - private RectRegion defaultBounds; + private PreviewMode previewMode; + + public enum PreviewMode { + LineAndPoint, + Candlestick, + Bar + } public XYPlot(Context context, String title) { super(context, title); @@ -169,121 +150,147 @@ public XYPlot(Context context, AttributeSet attrs, int defStyle) { @Override protected void onPreInit() { - legendWidget = new XYLegendWidget( + legend = new XYLegendWidget( getLayoutManager(), this, new Size( PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_H_DP), - SizeLayoutType.ABSOLUTE, 0.5f, SizeLayoutType.RELATIVE), + SizeMode.ABSOLUTE, 0.5f, SizeMode.RELATIVE), new DynamicTableModel(0, 1), new Size( PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP), - SizeLayoutType.ABSOLUTE, + SizeMode.ABSOLUTE, PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP), - SizeLayoutType.ABSOLUTE)); + SizeMode.ABSOLUTE)); - graphWidget = new XYGraphWidget( + graph = new XYGraphWidget( getLayoutManager(), this, new Size( PixelUtils.dpToPix(DEFAULT_GRAPH_WIDGET_H_DP), - SizeLayoutType.FILL, + SizeMode.FILL, PixelUtils.dpToPix(DEFAULT_GRAPH_WIDGET_W_DP), - SizeLayoutType.FILL)); + SizeMode.FILL)); Paint backgroundPaint = new Paint(); backgroundPaint.setColor(Color.DKGRAY); backgroundPaint.setStyle(Paint.Style.FILL); - graphWidget.setBackgroundPaint(backgroundPaint); + graph.setBackgroundPaint(backgroundPaint); - domainLabelWidget = new TextLabelWidget( + domainTitle = new TextLabelWidget( getLayoutManager(), new Size( PixelUtils.dpToPix(DEFAULT_DOMAIN_LABEL_WIDGET_H_DP), - SizeLayoutType.ABSOLUTE, + SizeMode.ABSOLUTE, PixelUtils.dpToPix(DEFAULT_DOMAIN_LABEL_WIDGET_W_DP), - SizeLayoutType.ABSOLUTE), - TextOrientationType.HORIZONTAL); - rangeLabelWidget = new TextLabelWidget( + SizeMode.ABSOLUTE), + TextOrientation.HORIZONTAL); + rangeTitle = new TextLabelWidget( getLayoutManager(), new Size( PixelUtils.dpToPix(DEFAULT_RANGE_LABEL_WIDGET_H_DP), - SizeLayoutType.ABSOLUTE, + SizeMode.ABSOLUTE, PixelUtils.dpToPix(DEFAULT_RANGE_LABEL_WIDGET_W_DP), - SizeLayoutType.ABSOLUTE), - TextOrientationType.VERTICAL_ASCENDING); + SizeMode.ABSOLUTE), + TextOrientation.VERTICAL_ASCENDING); - legendWidget.position( + legend.position( PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_X_OFFSET_DP), - XLayoutStyle.ABSOLUTE_FROM_RIGHT, + HorizontalPositioning.ABSOLUTE_FROM_RIGHT, PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP), - YLayoutStyle.ABSOLUTE_FROM_BOTTOM, - AnchorPosition.RIGHT_BOTTOM); + VerticalPositioning.ABSOLUTE_FROM_BOTTOM, + Anchor.RIGHT_BOTTOM); - graphWidget.position( + graph.position( PixelUtils.dpToPix(DEFAULT_GRAPH_WIDGET_X_OFFSET_DP), - XLayoutStyle.ABSOLUTE_FROM_RIGHT, + HorizontalPositioning.ABSOLUTE_FROM_RIGHT, PixelUtils.dpToPix(DEFAULT_GRAPH_WIDGET_Y_OFFSET_DP), - YLayoutStyle.ABSOLUTE_FROM_CENTER, - AnchorPosition.RIGHT_MIDDLE); + VerticalPositioning.ABSOLUTE_FROM_CENTER, + Anchor.RIGHT_MIDDLE); - domainLabelWidget.position( + domainTitle.position( PixelUtils.dpToPix(DEFAULT_DOMAIN_LABEL_WIDGET_X_OFFSET_DP), - XLayoutStyle.ABSOLUTE_FROM_LEFT, + HorizontalPositioning.ABSOLUTE_FROM_LEFT, PixelUtils.dpToPix(DEFAULT_DOMAIN_LABEL_WIDGET_Y_OFFSET_DP), - YLayoutStyle.ABSOLUTE_FROM_BOTTOM, - AnchorPosition.LEFT_BOTTOM); + VerticalPositioning.ABSOLUTE_FROM_BOTTOM, + Anchor.LEFT_BOTTOM); - rangeLabelWidget.position( + rangeTitle.position( PixelUtils.dpToPix(DEFAULT_RANGE_LABEL_WIDGET_X_OFFSET_DP), - XLayoutStyle.ABSOLUTE_FROM_LEFT, + HorizontalPositioning.ABSOLUTE_FROM_LEFT, PixelUtils.dpToPix(DEFAULT_RANGE_LABEL_WIDGET_Y_OFFSET_DP), - YLayoutStyle.ABSOLUTE_FROM_CENTER, - AnchorPosition.LEFT_MIDDLE); - - getLayoutManager().moveToTop(getTitleWidget()); - getLayoutManager().moveToTop(getLegendWidget()); - graphWidget.setMarginTop(PixelUtils.dpToPix(DEFAULT_GRAPH_WIDGET_TOP_MARGIN_DP)); - graphWidget.setMarginBottom(PixelUtils.dpToPix(DEFAULT_GRAPH_WIDGET_BOTTOM_MARGIN_DP)); - graphWidget.setMarginLeft(PixelUtils.dpToPix(DEFAULT_GRAPH_WIDGET_LEFT_MARGIN_DP)); - graphWidget.setMarginRight(PixelUtils.dpToPix(DEFAULT_GRAPH_WIDGET_RIGHT_MARGIN_DP)); - - getDomainLabelWidget().pack(); - getRangeLabelWidget().pack(); + VerticalPositioning.ABSOLUTE_FROM_CENTER, + Anchor.LEFT_MIDDLE); + + getLayoutManager().moveToTop(getTitle()); + getLayoutManager().moveToTop(getLegend()); + + getDomainTitle().pack(); + getRangeTitle().pack(); setPlotMarginLeft(PixelUtils.dpToPix(DEFAULT_PLOT_LEFT_MARGIN_DP)); setPlotMarginRight(PixelUtils.dpToPix(DEFAULT_PLOT_RIGHT_MARGIN_DP)); + setPlotMarginTop(PixelUtils.dpToPix(DEFAULT_PLOT_TOP_MARGIN_DP)); setPlotMarginBottom(PixelUtils.dpToPix(DEFAULT_PLOT_BOTTOM_MARGIN_DP)); xValueMarkers = new ArrayList<>(); yValueMarkers = new ArrayList<>(); - setDefaultBounds(new RectRegion(-1, 1, -1, 1)); + domainStepModel = new StepModel(StepMode.SUBDIVIDE, 10); + rangeStepModel = new StepModel(StepMode.SUBDIVIDE, 10); + } + @Override + protected void onAfterConfig() { // display some generic series data in editors that support it: if(isInEditMode()) { - addSeries(new SimpleXYSeries(Arrays.asList(1, 2, 3, 3, 4), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Red"), - new LineAndPointFormatter(Color.RED, null, null, null)); - addSeries(new SimpleXYSeries(Arrays.asList(2, 1, 4, 2, 5), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Green"), - new LineAndPointFormatter(Color.GREEN, null, null, null)); - addSeries(new SimpleXYSeries(Arrays.asList(3, 3, 2, 3, 3), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Blue"), - new LineAndPointFormatter(Color.BLUE, null, null, null)); - } - domainStepModel = new XYStepModel(XYStepMode.SUBDIVIDE, 10); - rangeStepModel = new XYStepModel(XYStepMode.SUBDIVIDE, 10); + switch (previewMode) { + case LineAndPoint: { + addSeries(new SimpleXYSeries(Arrays.asList(1, 2, 3, 3, 4), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Red"), + new LineAndPointFormatter(Color.RED, null, null, null)); + addSeries(new SimpleXYSeries(Arrays.asList(2, 1, 4, 2, 5), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Green"), + new LineAndPointFormatter(Color.GREEN, null, null, null)); + addSeries(new SimpleXYSeries(Arrays.asList(3, 3, 2, 3, 3), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Blue"), + new LineAndPointFormatter(Color.BLUE, null, null, null)); + } + break; + case Candlestick: { + 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)); + CandlestickMaker.make(this, new CandlestickFormatter(), candlestickSeries); + } + break; + case Bar: { + throw new UnsupportedOperationException("Not yet implemented."); + } + default: + throw new UnsupportedOperationException("Unexpected preview mode: " + previewMode); + } + } } @Override protected void processAttrs(TypedArray attrs) { - String domainLabelAttr = attrs.getString(R.styleable.xy_XYPlot_domainLabel); + this. previewMode = PreviewMode.values()[attrs.getInt( + R.styleable.xy_XYPlot_previewMode, PreviewMode.LineAndPoint.ordinal())]; + + String domainLabelAttr = attrs.getString(R.styleable.xy_XYPlot_domainTitle); if(domainLabelAttr != null) { - setDomainLabel(domainLabelAttr); + getDomainTitle().setText(domainLabelAttr); } - String rangeLabelAttr = attrs.getString(R.styleable.xy_XYPlot_rangeLabel); + String rangeLabelAttr = attrs.getString(R.styleable.xy_XYPlot_rangeTitle); if(rangeLabelAttr != null) { - setRangeLabel(rangeLabelAttr); + getRangeTitle().setText(rangeLabelAttr); } AttrUtils.configureStep(attrs, getDomainStepModel(), @@ -292,108 +299,45 @@ protected void processAttrs(TypedArray attrs) { AttrUtils.configureStep(attrs, getRangeStepModel(), R.styleable.xy_XYPlot_rangeStepMode, R.styleable.xy_XYPlot_rangeStep); - - // domainLabel size & position - AttrUtils.configureWidget(attrs, getDomainLabelWidget(), - R.styleable.xy_XYPlot_domainLabelHeightSizeLayoutType, R.styleable.xy_XYPlot_domainLabelHeight, - R.styleable.xy_XYPlot_domainLabelWidthSizeLayoutType, R.styleable.xy_XYPlot_domainLabelWidth, - R.styleable.xy_XYPlot_domainLabelLayoutStyleX, R.styleable.xy_XYPlot_domainLabelPositionX, - R.styleable.xy_XYPlot_domainLabelLayoutStyleY, R.styleable.xy_XYPlot_domainLabelPositionY, - R.styleable.xy_XYPlot_domainLabelAnchorPosition, R.styleable.xy_XYPlot_domainLabelVisible); - - // rangeLabel size & position - AttrUtils.configureWidget(attrs, getRangeLabelWidget(), - R.styleable.xy_XYPlot_rangeLabelHeightSizeLayoutType, R.styleable.xy_XYPlot_rangeLabelHeight, - R.styleable.xy_XYPlot_rangeLabelWidthSizeLayoutType, R.styleable.xy_XYPlot_rangeLabelWidth, - R.styleable.xy_XYPlot_rangeLabelLayoutStyleX, R.styleable.xy_XYPlot_rangeLabelPositionX, - R.styleable.xy_XYPlot_rangeLabelLayoutStyleY, R.styleable.xy_XYPlot_rangeLabelPositionY, - R.styleable.xy_XYPlot_rangeLabelAnchorPosition, R.styleable.xy_XYPlot_rangeLabelVisible); - // domainLabelPaint - AttrUtils.configureTextPaint(attrs, getDomainLabelWidget().getLabelPaint(), - R.styleable.xy_XYPlot_domainLabelTextColor, R.styleable.xy_XYPlot_domainLabelTextSize); + AttrUtils.configureTextPaint(attrs, getDomainTitle().getLabelPaint(), + R.styleable.xy_XYPlot_domainTitleTextColor, R.styleable.xy_XYPlot_domainTitleTextSize); // rangeLabelPaint - AttrUtils.configureTextPaint(attrs, getRangeLabelWidget().getLabelPaint(), - R.styleable.xy_XYPlot_rangeLabelTextColor, R.styleable.xy_XYPlot_rangeLabelTextSize); - - // graphWidget - AttrUtils.configureBoxModelable(attrs, getGraphWidget(), - 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); - - // gridRect - AttrUtils.configureBoxModelable(attrs, getGraphWidget().getGridBox(), - R.styleable.xy_XYPlot_gridMarginTop, R.styleable.xy_XYPlot_gridMarginBottom, - R.styleable.xy_XYPlot_gridMarginLeft, R.styleable.xy_XYPlot_gridMarginRight, - R.styleable.xy_XYPlot_gridPaddingTop, R.styleable.xy_XYPlot_gridPaddingBottom, - R.styleable.xy_XYPlot_gridPaddingLeft, R.styleable.xy_XYPlot_gridPaddingRight); - - // domainTickLabelPaint - AttrUtils.configureTextPaint(attrs, getGraphWidget().getDomainTickLabelPaint(), - R.styleable.xy_XYPlot_domainTickLabelTextColor, - R.styleable.xy_XYPlot_domainTickLabelTextSize); - - // rangeTickLabelPaint - AttrUtils.configureTextPaint(attrs, getGraphWidget().getRangeTickLabelPaint(), - R.styleable.xy_XYPlot_rangeTickLabelTextColor, - R.styleable.xy_XYPlot_rangeTickLabelTextSize); - - // domainOriginTickLabelPaint - AttrUtils.configureTextPaint(attrs, getGraphWidget().getDomainOriginTickLabelPaint(), - R.styleable.xy_XYPlot_domainOriginTickLabelTextColor, - R.styleable.xy_XYPlot_domainOriginTickLabelTextSize); - - // rangeOriginTickLabelPaint - AttrUtils.configureTextPaint(attrs, getGraphWidget().getRangeOriginTickLabelPaint(), - R.styleable.xy_XYPlot_rangeOriginTickLabelTextColor, - R.styleable.xy_XYPlot_rangeOriginTickLabelTextSize); + AttrUtils.configureTextPaint(attrs, getRangeTitle().getLabelPaint(), + R.styleable.xy_XYPlot_rangeTitleTextColor, R.styleable.xy_XYPlot_rangeTitleTextSize); // legendWTextPaint - AttrUtils.configureTextPaint(attrs, getLegendWidget().getTextPaint(), + AttrUtils.configureTextPaint(attrs, getLegend().getTextPaint(), R.styleable.xy_XYPlot_legendTextColor, R.styleable.xy_XYPlot_legendTextSize); // legendIconSize - AttrUtils.configureSize(attrs, getLegendWidget().getIconSize(), - R.styleable.xy_XYPlot_legendIconHeightSizeLayoutType, R.styleable.xy_XYPlot_legendIconHeight, - R.styleable.xy_XYPlot_legendIconWidthSizeLayoutType, R.styleable.xy_XYPlot_legendIconWidth); + AttrUtils.configureSize(attrs, getLegend().getIconSize(), + R.styleable.xy_XYPlot_legendIconHeightMode, R.styleable.xy_XYPlot_legendIconHeight, + R.styleable.xy_XYPlot_legendIconWidthMode, R.styleable.xy_XYPlot_legendIconWidth); // legend size & position - AttrUtils.configureWidget(attrs, getLegendWidget(), - R.styleable.xy_XYPlot_legendHeightSizeLayoutType, R.styleable.xy_XYPlot_legendHeight, - R.styleable.xy_XYPlot_legendWidthSizeLayoutType, R.styleable.xy_XYPlot_legendWidth, - R.styleable.xy_XYPlot_legendLayoutStyleX, R.styleable.xy_XYPlot_legendPositionX, - R.styleable.xy_XYPlot_legendLayoutStyleY, R.styleable.xy_XYPlot_legendPositionY, - R.styleable.xy_XYPlot_legendAnchorPosition, R.styleable.xy_XYPlot_legendVisible); - - AttrUtils.configureLinePaint(attrs, getGraphWidget().getDomainGridLinePaint(), - R.styleable.xy_XYPlot_graphDomainLineColor, R.styleable.xy_XYPlot_graphDomainLineThickness); - - AttrUtils.configureLinePaint(attrs, getGraphWidget().getRangeGridLinePaint(), - R.styleable.xy_XYPlot_graphRangeLineColor, R.styleable.xy_XYPlot_graphRangeLineThickness); - - getGraphWidget().getBackgroundPaint().setColor(attrs.getColor( - R.styleable.xy_XYPlot_graphBackgroundColor, getGraphWidget().getBackgroundPaint().getColor())); + AttrUtils.configureWidget(attrs, getLegend(), + R.styleable.xy_XYPlot_legendHeightMode, R.styleable.xy_XYPlot_legendHeight, + R.styleable.xy_XYPlot_legendWidthMode, R.styleable.xy_XYPlot_legendWidth, + R.styleable.xy_XYPlot_legendHorizontalPositioning, R.styleable.xy_XYPlot_legendHorizontalPosition, + R.styleable.xy_XYPlot_legendVerticalPositioning, R.styleable.xy_XYPlot_legendVerticalPosition, + R.styleable.xy_XYPlot_legendAnchor, R.styleable.xy_XYPlot_legendVisible); - getGraphWidget().getGridBackgroundPaint().setColor(attrs.getColor( - R.styleable.xy_XYPlot_gridBackgroundColor, getGraphWidget().getGridBackgroundPaint().getColor())); - } - - public void setGridPadding(float left, float top, float right, float bottom) { - getGraphWidget().getGridBox().setPadding(left, top, right, bottom); + getGraph().processAttrs(attrs); } @Override protected void notifyListenersBeforeDraw(Canvas canvas) { super.notifyListenersBeforeDraw(canvas); + calculateMinMaxVals(); + // this call must be AFTER the notify so that if the listener // is a synchronized series, it has the opportunity to // place a read lock on it's data. - calculateMinMaxVals(); + getRegistry().estimate(this); // TODO: clean this mechanism up!!! } /** @@ -404,10 +348,7 @@ protected void notifyListenersBeforeDraw(Canvas canvas) { * @return */ public boolean containsPoint(float x, float y) { - if (getGraphWidget().getGridDimensions().marginatedRect != null) { - return getGraphWidget().getGridDimensions().marginatedRect.contains(x, y); - } - return false; + return getGraph().containsPoint(x, y); } /** ` @@ -421,133 +362,177 @@ public boolean containsPoint(PointF point) { } public void setCursorPosition(PointF point) { - getGraphWidget().setCursorPosition(point); + getGraph().setCursorPosition(point); } public void setCursorPosition(float x, float y) { - getGraphWidget().setCursorPosition(x, y); + getGraph().setCursorPosition(x, y); + } + + /** + * Convert a screen xVal into a series xVal. + * @param xPix + * @return + * @deprecated Use {@link #screenToSeriesY(float)}. + */ + @Deprecated + public Number getXVal(float xPix) { + return getGraph().screenToSeriesX(xPix); } + /** + * Convert a screen yVal into a series yVal. + * @param yPix + * @return + * @deprecated Use {@link #screenToSeriesY(float)}. + */ + public Number getYVal(float yPix) { + return getGraph().screenToSeriesY(yPix); + } + + /** + * Convert the y coord of a PointF into a series yVal. + * @param point + * @return + * @deprecated Use {@link #screenToSeriesY(float)}. + */ + @Deprecated public Number getYVal(PointF point) { - return getGraphWidget().getYVal(point); + return getGraph().screenToSeriesY(point); } + /** + * Convert the x coord of a PointF into a series xVal. + * @param point + * @return + * @deprecated Use {@link #screenToSeriesY(float)}. + */ + @Deprecated public Number getXVal(PointF point) { - return getGraphWidget().getXVal(point); + return getGraph().screenToSeriesX(point); } - private boolean isXValWithinView(double xVal) { - return (userMinY == null || xVal >= userMinY.doubleValue()) && - userMaxY == null || xVal <= userMaxY.doubleValue(); + public Number screenToSeriesX(float x) { + return getGraph().screenToSeriesX(x); } - private boolean isPointVisible(Number x, Number y) { - // values without both an x and y val arent visible - if (x == null || y == null) { - return false; - } - return isValWithinRange(y.doubleValue(), userMinY, userMaxY) && - isValWithinRange(x.doubleValue(), userMinX, userMaxX); + public Number screenToSeriesY(float y) { + return getGraph().screenToSeriesY(y); + } + + /** + * Convert a series xVal into a screen x coord + * @param x + * @return + */ + public float seriesToScreenX(Number x) { + return getGraph().seriesToScreenX(x); + } + + /** + * Convert a series yVal into a screen y coord + * @param y + * @return + */ + public float seriesToScreenY(Number y) { + return getGraph().seriesToScreenY(y); } - private boolean isValWithinRange(double val, Number min, Number max) { - boolean isAboveMinThreshold = min == null || val >= min.doubleValue(); - boolean isBelowMaxThreshold = max == null || val <= max.doubleValue(); - return isAboveMinThreshold && - isBelowMaxThreshold; + /** + * Convert a series xy value into a screen point. + * @param xy + * @return + */ + public PointF seriesToScreen(XYCoords xy) { + return getGraph().seriesToScreen(xy); + } + + /** + * Convert a screen point into a series xy value. + * @param point + * @return + */ + public XYCoords screentoSeries(PointF point) { + return getGraph().screenToSeries(point); } public void calculateMinMaxVals() { - prevMinX = calculatedMinX; - prevMaxX = calculatedMaxX; - prevMinY = calculatedMinY; - prevMaxY = calculatedMaxY; - - calculatedMinX = userMinX; - calculatedMaxX = userMaxX; - calculatedMinY = userMinY; - calculatedMaxY = userMaxY; - - // next we go through each series to update our min/max values: - for (SeriesAndFormatter thisPair : getSeriesRegistry()) { - XYSeries series = thisPair.getSeries(); - // step through each point in each series: - for (int i = 0; i < series.size(); i++) { - Number thisX = series.getX(i); - Number thisY = series.getY(i); - if (isPointVisible(thisX, thisY)) { - // only calculate if a static value has not been set: - if (userMinX == null) { - if (thisX != null && (calculatedMinX == null || - thisX.doubleValue() < calculatedMinX.doubleValue())) { - calculatedMinX = thisX; - } - } - - if (userMaxX == null) { - if (thisX != null && (calculatedMaxX == null || - thisX.doubleValue() > calculatedMaxX.doubleValue())) { - calculatedMaxX = thisX; - } - } - - if (userMinY == null) { - if (thisY != null && (calculatedMinY == null || - thisY.doubleValue() < calculatedMinY.doubleValue())) { - calculatedMinY = thisY; - } - } - - if (userMaxY == null) { - if (thisY != null && (calculatedMaxY == null || thisY.doubleValue() > calculatedMaxY.doubleValue())) { - calculatedMaxY = thisY; - } - } - } + prevMinX = bounds.isMinXSet() ? bounds.getMinX() : null; + prevMaxX = bounds.isMaxXSet() ? bounds.getMaxX() : null; + prevMinY = bounds.isMinYSet() ? bounds.getMinY() : null; + prevMaxY = bounds.isMaxYSet() ? bounds.getMaxY() : null; + + bounds.setMinX(constraints.getMinX()); + bounds.setMaxX(constraints.getMaxX()); + bounds.setMinY(constraints.getMinY()); + bounds.setMaxY(constraints.getMaxY()); + + // only calculate if we must: + if(!bounds.isFullyDefined()) { + + RectRegion b = SeriesUtils.minMax(constraints, getRegistry().getSeriesList()); + + if(!bounds.isMinXSet()) { + bounds.setMinX(b.getMinX()); + } + if(!bounds.isMaxXSet()) { + bounds.setMaxX(b.getMaxX()); + } + + if(!bounds.isMinYSet()) { + bounds.setMinY(b.getMinY()); + } + if(!bounds.isMaxYSet()) { + bounds.setMaxY(b.getMaxY()); } } // at this point we now know what points are going to be visible on our // plot, but we still need to make corrections based on modes being used: // (grow, shrink etc.) - switch (domainFramingModel) { + switch (constraints.getDomainFramingModel()) { case ORIGIN: updateDomainMinMaxForOriginModel(); break; case EDGE: - calculatedMaxX = getCalculatedUpperBoundary(domainUpperBoundaryMode, prevMaxX, calculatedMaxX); - calculatedMinX = getCalculatedLowerBoundary(domainLowerBoundaryMode, prevMinX, calculatedMinX); - calculatedMinX = ApplyUserMinMax(calculatedMinX, domainLeftMin, - domainLeftMax); - calculatedMaxX = ApplyUserMinMax(calculatedMaxX, - domainRightMin, domainRightMax); + bounds.setMaxX(applyUserMinMax(getCalculatedUpperBoundary( + constraints.getDomainUpperBoundaryMode(), prevMaxX, bounds.getMaxX()), + innerLimits.getMaxX(), outerLimits.getMaxX())); + bounds.setMinX(applyUserMinMax(getCalculatedLowerBoundary( + constraints.getDomainLowerBoundaryMode(), + prevMinX, bounds.getMinX()), + outerLimits.getMinX(), innerLimits.getMinX())); break; default: throw new UnsupportedOperationException( - "Domain Framing Model not yet supported: " + domainFramingModel); + "Domain Framing Model not yet supported: " + constraints.getDomainFramingModel()); } - switch (rangeFramingModel) { + switch (constraints.getRangeFramingModel()) { case ORIGIN: updateRangeMinMaxForOriginModel(); break; case EDGE: - if (getSeriesRegistry().size() > 0) { - calculatedMaxY = getCalculatedUpperBoundary(rangeUpperBoundaryMode, prevMaxY, calculatedMaxY); - calculatedMinY = getCalculatedLowerBoundary(rangeLowerBoundaryMode, prevMinY, calculatedMinY); - calculatedMinY = ApplyUserMinMax(calculatedMinY, - rangeBottomMin, rangeBottomMax); - calculatedMaxY = ApplyUserMinMax(calculatedMaxY, rangeTopMin, - rangeTopMax); + if (getRegistry().size() > 0) { + bounds.setMaxY(applyUserMinMax(getCalculatedUpperBoundary( + constraints.getRangeUpperBoundaryMode(), + prevMaxY, bounds.getMaxY()), innerLimits.getMaxY(), outerLimits.getMaxY())); + bounds.setMinY(applyUserMinMax(getCalculatedLowerBoundary( + constraints.getRangeLowerBoundaryMode(), + prevMinY, bounds.getMinY()), outerLimits.getMinY(), innerLimits.getMinY())); } break; default: throw new UnsupportedOperationException( - "Range Framing Model not yet supported: " + domainFramingModel); + "Range Framing Model not yet supported: " + constraints.getRangeFramingModel()); } - calculatedDomainOrigin = userDomainOrigin != null ? userDomainOrigin : getCalculatedMinX(); - calculatedRangeOrigin = this.userRangeOrigin != null ? userRangeOrigin : getCalculatedMinY(); + + calculatedOrigin.x = userDomainOrigin != null ? + userDomainOrigin : bounds.getMinX(); + + calculatedOrigin.y = this.userRangeOrigin != null ? + userRangeOrigin : bounds.getMinY(); } protected Number getCalculatedUpperBoundary(BoundaryMode mode, Number previousMax, Number calculatedMax) { @@ -561,7 +546,7 @@ protected Number getCalculatedUpperBoundary(BoundaryMode mode, Number previousMa calculatedMax = previousMax; } break; - case SHRINNK: + case SHRINK: if (!(previousMax == null || calculatedMax.doubleValue() < previousMax.doubleValue())) { calculatedMax = previousMax; } @@ -583,7 +568,7 @@ protected Number getCalculatedLowerBoundary(BoundaryMode mode, Number previousMi return previousMin; } break; - case SHRINNK: + case SHRINK: if (!(previousMin == null || calculatedMin.doubleValue() > previousMin.doubleValue())) { return previousMin; } @@ -602,7 +587,7 @@ protected Number getCalculatedLowerBoundary(BoundaryMode mode, Number previousMi * @param min * @param max */ - private Number ApplyUserMinMax(Number value, Number min, Number max) { + private static Number applyUserMinMax(Number value, Number min, Number max) { value = (((min == null) || (value == null) || (value.doubleValue() > min.doubleValue())) ? value : min); @@ -617,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); } @@ -629,18 +614,18 @@ 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."); } - domainFramingModel = XYFramingModel.ORIGIN; + constraints.setDomainFramingModel(XYFramingModel.ORIGIN); setUserDomainOrigin(origin); domainOriginExtent = extent; domainOriginBoundaryMode = mode; Number[] minMax = getOriginMinMax(domainOriginBoundaryMode, userDomainOrigin, domainOriginExtent); - userMinX = minMax[0]; - userMaxX = minMax[1]; + constraints.setMinX(minMax[0]); + constraints.setMaxX(minMax[1]); } /** @@ -648,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); } @@ -661,18 +646,18 @@ 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."); } - rangeFramingModel = XYFramingModel.ORIGIN; + constraints.setRangeFramingModel(XYFramingModel.ORIGIN); setUserRangeOrigin(origin); rangeOriginExtent = extent; rangeOriginBoundaryMode = mode; Number[] minMax = getOriginMinMax(rangeOriginBoundaryMode, userRangeOrigin, rangeOriginExtent); - userMinY = minMax[0]; - userMaxY = minMax[1]; + constraints.setMinY(minMax[0]); + constraints.setMaxY(minMax[1]); } /** @@ -687,7 +672,6 @@ protected Number[] getOriginMinMax(BoundaryMode mode, Number origin, Number exte double o = origin.doubleValue(); double e = extent.doubleValue(); return new Number[] {o - e, o + e}; - } return new Number[] {null, null}; } @@ -700,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 { @@ -710,15 +694,15 @@ private double distance(double x, double y) { public void updateDomainMinMaxForOriginModel() { double origin = userDomainOrigin.doubleValue(); - double maxXDelta = distance(calculatedMaxX.doubleValue(), origin); - double minXDelta = distance(calculatedMinX.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: - calculatedMinX = dlb; - calculatedMaxX = dub; + bounds.setMinX(lowerBoundary); + bounds.setMaxX(upperBoundary); break; // if fixed, then the value already exists within "user" vals. @@ -726,30 +710,30 @@ public void updateDomainMinMaxForOriginModel() { break; case GROW: { - if (prevMinX == null || dlb < prevMinX.doubleValue()) { - calculatedMinX = dlb; + if (prevMinX == null || lowerBoundary < prevMinX.doubleValue()) { + bounds.setMinX(lowerBoundary); } else { - calculatedMinX = prevMinX; + bounds.setMinX(prevMinX); } - if (prevMaxX == null || dub > prevMaxX.doubleValue()) { - calculatedMaxX = dub; + if (prevMaxX == null || upperBoundary > prevMaxX.doubleValue()) { + bounds.setMaxX(upperBoundary); } else { - calculatedMaxX = prevMaxX; + bounds.setMaxX(prevMaxX); } } break; - case SHRINNK: - if (prevMinX == null || dlb > prevMinX.doubleValue()) { - calculatedMinX = dlb; + case SHRINK: + if (prevMinX == null || lowerBoundary > prevMinX.doubleValue()) { + bounds.setMinX(lowerBoundary); } else { - calculatedMinX = prevMinX; + bounds.setMinX(prevMinX); } - if (prevMaxX == null || dub < prevMaxX.doubleValue()) { - calculatedMaxX = dub; + if (prevMaxX == null || upperBoundary < prevMaxX.doubleValue()) { + bounds.setMaxX(upperBoundary); } else { - calculatedMaxX = prevMaxX; + bounds.setMaxX(prevMaxX); } break; default: @@ -761,19 +745,19 @@ public void updateRangeMinMaxForOriginModel() { switch (rangeOriginBoundaryMode) { case AUTO: double origin = userRangeOrigin.doubleValue(); - double maxYDelta = distance(calculatedMaxY.doubleValue(), origin); - double minYDelta = distance(calculatedMinY.doubleValue(), origin); - if (maxYDelta > minYDelta) { - calculatedMinY = origin - maxYDelta; - calculatedMaxY = 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 { - calculatedMinY = origin - minYDelta; - calculatedMaxY = origin + minYDelta; + bounds.setMinY(origin - minDelta); + bounds.setMaxY(origin + minDelta); } break; case FIXED: case GROW: - case SHRINNK: + case SHRINK: default: throw new UnsupportedOperationException( "Range Origin Boundary Mode not yet supported: " + rangeOriginBoundaryMode); @@ -781,50 +765,50 @@ public void updateRangeMinMaxForOriginModel() { } /** - * Convenience method - wraps XYGraphWidget.getTicksPerRangeLabel(). - * Equivalent to getGraphWidget().getTicksPerRangeLabel(). + * Convenience method - wraps XYGraphWidget.getLinesPerRangeLabel(). + * Equivalent to getGraphWidget().getLinesPerRangeLabel(). * * @return */ - public int getTicksPerRangeLabel() { - return graphWidget.getTicksPerRangeLabel(); + public int getLinesPerRangeLabel() { + return graph.getLinesPerRangeLabel(); } /** - * Convenience method - wraps XYGraphWidget.setTicksPerRangeLabel(). - * Equivalent to getGraphWidget().setTicksPerRangeLabel(). + * Convenience method - wraps XYGraphWidget.setLinesPerRangeLabel(). + * Equivalent to getGraphWidget().setLinesPerRangeLabel(). * - * @param ticksPerRangeLabel + * @param linesPerLabel */ - public void setTicksPerRangeLabel(int ticksPerRangeLabel) { - graphWidget.setTicksPerRangeLabel(ticksPerRangeLabel); + public void setLinesPerRangeLabel(int linesPerLabel) { + graph.setLinesPerRangeLabel(linesPerLabel); } /** - * Convenience method - wraps XYGraphWidget.getTicksPerDomainLabel(). - * Equivalent to getGraphWidget().getTicksPerDomainLabel(). + * Convenience method - wraps XYGraphWidget.getLinesPerDomainLabel(). + * Equivalent to getGraphWidget().getLinesPerDomainLabel(). * * @return */ - public int getTicksPerDomainLabel() { - return graphWidget.getTicksPerDomainLabel(); + public int getLinesPerDomainLabel() { + return graph.getLinesPerDomainLabel(); } /** - * Convenience method - wraps XYGraphWidget.setTicksPerDomainLabel(). - * Equivalent to getGraphWidget().setTicksPerDomainLabel(). + * Convenience method - wraps XYGraphWidget.setLinesPerDomainLabel(). + * Equivalent to getGraphWidget().setLinesPerDomainLabel(). * - * @param ticksPerDomainLabel + * @param linesPerDomainLabel */ - public void setTicksPerDomainLabel(int ticksPerDomainLabel) { - graphWidget.setTicksPerDomainLabel(ticksPerDomainLabel); + public void setLinesPerDomainLabel(int linesPerDomainLabel) { + graph.setLinesPerDomainLabel(linesPerDomainLabel); } - public XYStepMode getDomainStepMode() { + public StepMode getDomainStepMode() { return domainStepModel.getMode(); } - public void setDomainStepMode(XYStepMode domainStepMode) { + public void setDomainStepMode(StepMode domainStepMode) { domainStepModel.setMode(domainStepMode); } @@ -836,16 +820,16 @@ public void setDomainStepValue(double domainStepValue) { domainStepModel.setValue(domainStepValue); } - public void setDomainStep(XYStepMode mode, double value) { + public void setDomainStep(StepMode mode, double value) { setDomainStepMode(mode); setDomainStepValue(value); } - public XYStepMode getRangeStepMode() { + public StepMode getRangeStepMode() { return rangeStepModel.getMode(); } - public void setRangeStepMode(XYStepMode rangeStepMode) { + public void setRangeStepMode(StepMode rangeStepMode) { rangeStepModel.setMode(rangeStepMode); } @@ -857,93 +841,49 @@ public void setRangeStepValue(double rangeStepValue) { rangeStepModel.setValue(rangeStepValue); } - public void setRangeStep(XYStepMode mode, double value) { + public void setRangeStep(StepMode mode, double value) { setRangeStepMode(mode); setRangeStepValue(value); } - public String getDomainLabel() { - return getDomainLabelWidget().getText(); - } - - public void setDomainLabel(String domainLabel) { - getDomainLabelWidget().setText(domainLabel); - } - - public String getRangeLabel() { - return getRangeLabelWidget().getText(); - } - - public void setRangeLabel(String rangeLabel) { - getRangeLabelWidget().setText(rangeLabel); - } - - public XYLegendWidget getLegendWidget() { - return legendWidget; - } - - public void setLegendWidget(XYLegendWidget legendWidget) { - this.legendWidget = legendWidget; + public XYLegendWidget getLegend() { + return legend; } - public XYGraphWidget getGraphWidget() { - return graphWidget; + public void setLegend(XYLegendWidget legend) { + this.legend = legend; } - public void setGraphWidget(XYGraphWidget graphWidget) { - this.graphWidget = graphWidget; + public XYGraphWidget getGraph() { + return graph; } - public TextLabelWidget getDomainLabelWidget() { - return domainLabelWidget; + public void setGraph(XYGraphWidget graph) { + this.graph = graph; } - public void setDomainLabelWidget(TextLabelWidget domainLabelWidget) { - this.domainLabelWidget = domainLabelWidget; + public TextLabelWidget getDomainTitle() { + return domainTitle; } - public TextLabelWidget getRangeLabelWidget() { - return rangeLabelWidget; + public void setDomainTitle(TextLabelWidget domainTitle) { + this.domainTitle = domainTitle; } - public void setRangeLabelWidget(TextLabelWidget rangeLabelWidget) { - this.rangeLabelWidget = rangeLabelWidget; - } - - /** - * Convenience method - wraps XYGraphWidget.getRangeValueFormat(). - * - * @return - */ - public Format getRangeValueFormat() { - return graphWidget.getRangeValueFormat(); + public void setDomainLabel(String domainLabel) { + getDomainTitle().setText(domainLabel); } - /** - * Convenience method - wraps XYGraphWidget.setRangeValueFormat(). - * - * @param rangeValueFormat - */ - public void setRangeValueFormat(Format rangeValueFormat) { - graphWidget.setRangeValueFormat(rangeValueFormat); + public TextLabelWidget getRangeTitle() { + return rangeTitle; } - /** - * Convenience method - wraps XYGraphWidget.getDomainValueFormat(). - * - * @return - */ - public Format getDomainValueFormat() { - return graphWidget.getDomainValueFormat(); + public void setRangeTitle(TextLabelWidget rangeTitle) { + this.rangeTitle = rangeTitle; } - /** - * Convenience method - wraps XYGraphWidget.setDomainValueFormat(). - * - * @param domainValueFormat - */ - public void setDomainValueFormat(Format domainValueFormat) { - graphWidget.setDomainValueFormat(domainValueFormat); + public void setRangeLabel(String rangeLabel) { + getRangeTitle().setText(rangeLabel); } /** @@ -997,11 +937,11 @@ public synchronized void setRangeBoundaries(Number lowerBoundary, BoundaryMode l } protected synchronized void setDomainUpperBoundaryMode(BoundaryMode mode) { - this.domainUpperBoundaryMode = mode; + constraints.setDomainUpperBoundaryMode(mode); } - protected synchronized void setUserMaxX(Number boundary) { - this.userMaxX = boundary; + protected synchronized void setUserMaxX(Number maxX) { + constraints.setMaxX(maxX); } /** @@ -1017,11 +957,11 @@ public synchronized void setDomainUpperBoundary(Number boundary, BoundaryMode mo } protected synchronized void setDomainLowerBoundaryMode(BoundaryMode mode) { - this.domainLowerBoundaryMode = mode; + constraints.setDomainLowerBoundaryMode(mode); } - protected synchronized void setUserMinX(Number boundary) { - this.userMinX = boundary; + protected synchronized void setUserMinX(Number minX) { + constraints.setMinX(minX); } /** @@ -1037,11 +977,11 @@ public synchronized void setDomainLowerBoundary(Number boundary, BoundaryMode mo } protected synchronized void setRangeUpperBoundaryMode(BoundaryMode mode) { - this.rangeUpperBoundaryMode = mode; + constraints.setRangeUpperBoundaryMode(mode); } - protected synchronized void setUserMaxY(Number boundary) { - this.userMaxY = boundary; + protected synchronized void setUserMaxY(Number maxY) { + constraints.setMaxY(maxY); } /** @@ -1057,11 +997,11 @@ public synchronized void setRangeUpperBoundary(Number boundary, BoundaryMode mod } protected synchronized void setRangeLowerBoundaryMode(BoundaryMode mode) { - this.rangeLowerBoundaryMode = mode; + constraints.setRangeLowerBoundaryMode(mode); } - protected synchronized void setUserMinY(Number boundary) { - this.userMinY = boundary; + protected synchronized void setUserMinY(Number minY) { + constraints.setMinY(minY); } /** @@ -1076,44 +1016,16 @@ public synchronized void setRangeLowerBoundary(Number boundary, BoundaryMode mod setRangeFramingModel(XYFramingModel.EDGE); } - private Number getUserMinX() { - return userMinX; - } - - private Number getUserMaxX() { - return userMaxX; - } - - private Number getUserMinY() { - return userMinY; - } - - private Number getUserMaxY() { - return userMaxY; + public XYCoords getOrigin() { + return calculatedOrigin; } public Number getDomainOrigin() { - return calculatedDomainOrigin; + return calculatedOrigin.x; } public Number getRangeOrigin() { - return calculatedRangeOrigin; - } - - protected BoundaryMode getDomainUpperBoundaryMode() { - return domainUpperBoundaryMode; - } - - protected BoundaryMode getDomainLowerBoundaryMode() { - return domainLowerBoundaryMode; - } - - protected BoundaryMode getRangeUpperBoundaryMode() { - return rangeUpperBoundaryMode; - } - - protected BoundaryMode getRangeLowerBoundaryMode() { - return rangeLowerBoundaryMode; + return calculatedOrigin.y; } public synchronized void setUserDomainOrigin(Number origin) { @@ -1130,75 +1042,23 @@ public synchronized void setUserRangeOrigin(Number origin) { this.userRangeOrigin = origin; } - public XYFramingModel getDomainFramingModel() { - return domainFramingModel; - } - @SuppressWarnings("SameParameterValue") - protected void setDomainFramingModel(XYFramingModel domainFramingModel) { - this.domainFramingModel = domainFramingModel; - } - - public XYFramingModel getRangeFramingModel() { - - return rangeFramingModel; + protected void setDomainFramingModel(@NonNull XYFramingModel model) { + constraints.setDomainFramingModel(model); } @SuppressWarnings("SameParameterValue") - protected void setRangeFramingModel(XYFramingModel rangeFramingModel) { - this.rangeFramingModel = rangeFramingModel; - } - - /** - * CalculatedMinX value after the the framing model has been applied. - * - * @return - */ - public Number getCalculatedMinX() { - return calculatedMinX != null ? calculatedMinX : getDefaultBounds().getMinX(); - } - - /** - * CalculatedMaxX value after the the framing model has been applied. - * - * @return - */ - public Number getCalculatedMaxX() { - return calculatedMaxX != null ? calculatedMaxX : getDefaultBounds().getMaxX(); + protected void setRangeFramingModel(@NonNull XYFramingModel model) { + constraints.setRangeFramingModel(model); } /** - * CalculatedMinY value after the the framing model has been applied. * - * @return - */ - public Number getCalculatedMinY() { - return calculatedMinY != null ? calculatedMinY : getDefaultBounds().getMinY(); - } - - /** - * CalculatedMaxY value after the the framing model has been applied. - * - * @return + * @return The current min/max values for real domain and range that fall within the visible + * graph space. */ - public Number getCalculatedMaxY() { - return calculatedMaxY != null ? calculatedMaxY : getDefaultBounds().getMaxY(); - } - - public boolean isDrawDomainOriginEnabled() { - return drawDomainOriginEnabled; - } - - public void setDrawDomainOriginEnabled(boolean drawDomainOriginEnabled) { - this.drawDomainOriginEnabled = drawDomainOriginEnabled; - } - - public boolean isDrawRangeOriginEnabled() { - return drawRangeOriginEnabled; - } - - public void setDrawRangeOriginEnabled(boolean drawRangeOriginEnabled) { - this.drawRangeOriginEnabled = drawRangeOriginEnabled; + public RectRegion getBounds() { + return bounds; } /** @@ -1236,9 +1096,7 @@ public YValueMarker removeMarker(YValueMarker marker) { * @return */ public int removeMarkers() { - int removed = removeXMarkers(); - removed += removeYMarkers(); - return removed; + return removeXMarkers() + removeYMarkers(); } /** @@ -1296,139 +1154,33 @@ protected List getXValueMarkers() { return xValueMarkers; } - public RectRegion getDefaultBounds() { - return defaultBounds; - } - - public void setDefaultBounds(RectRegion defaultBounds) { - this.defaultBounds = defaultBounds; - } - - /** - * @return the rangeTopMin - */ - public Number getRangeTopMin() { - return rangeTopMin; - } - - /** - * @param rangeTopMin the rangeTopMin to set - */ - public synchronized void setRangeTopMin(Number rangeTopMin) { - this.rangeTopMin = rangeTopMin; - } - - /** - * @return the rangeTopMax - */ - public Number getRangeTopMax() { - return rangeTopMax; - } - - /** - * @param rangeTopMax the rangeTopMax to set - */ - public synchronized void setRangeTopMax(Number rangeTopMax) { - this.rangeTopMax = rangeTopMax; - } - - /** - * @return the rangeBottomMin - */ - public Number getRangeBottomMin() { - return rangeBottomMin; - } - - /** - * @param rangeBottomMin the rangeBottomMin to set - */ - public synchronized void setRangeBottomMin(Number rangeBottomMin) { - this.rangeBottomMin = rangeBottomMin; - } - - /** - * @return the rangeBottomMax - */ - public Number getRangeBottomMax() { - return rangeBottomMax; - } - - /** - * @param rangeBottomMax the rangeBottomMax to set - */ - public synchronized void setRangeBottomMax(Number rangeBottomMax) { - this.rangeBottomMax = rangeBottomMax; - } - - /** - * @return the domainLeftMin - */ - public Number getDomainLeftMin() { - return domainLeftMin; - } - - /** - * @param domainLeftMin the domainLeftMin to set - */ - public synchronized void setDomainLeftMin(Number domainLeftMin) { - this.domainLeftMin = domainLeftMin; - } - - /** - * @return the domainLeftMax - */ - public Number getDomainLeftMax() { - return domainLeftMax; - } - - /** - * @param domainLeftMax the domainLeftMax to set - */ - public synchronized void setDomainLeftMax(Number domainLeftMax) { - this.domainLeftMax = domainLeftMax; - } - - /** - * @return the domainRightMin - */ - public Number getDomainRightMin() { - return domainRightMin; - } - - /** - * @param domainRightMin the domainRightMin to set - */ - public synchronized void setDomainRightMin(Number domainRightMin) { - this.domainRightMin = domainRightMin; + public RectRegion getInnerLimits() { + return innerLimits; } - /** - * @return the domainRightMax - */ - public Number getDomainRightMax() { - return domainRightMax; + public RectRegion getOuterLimits() { + return outerLimits; } - /** - * @param domainRightMax the domainRightMax to set - */ - public synchronized void setDomainRightMax(Number domainRightMax) { - this.domainRightMax = domainRightMax; - } - - public XYStepModel getDomainStepModel() { + public StepModel getDomainStepModel() { return domainStepModel; } - public void setDomainStepModel(XYStepModel domainStepModel) { + public void setDomainStepModel(StepModel domainStepModel) { this.domainStepModel = domainStepModel; } - public XYStepModel getRangeStepModel() { + public StepModel getRangeStepModel() { return rangeStepModel; } - public void setRangeStepModel(XYStepModel rangeStepModel) { + public void setRangeStepModel(StepModel rangeStepModel) { this.rangeStepModel = rangeStepModel; } + + @Override + protected XYSeriesRegistry getRegistryInstance() { + XYSeriesRegistry registry = new XYSeriesRegistry(); + return registry; + } } \ No newline at end of file diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlotZoomPan.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlotZoomPan.java deleted file mode 100644 index b8a8b92b..00000000 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlotZoomPan.java +++ /dev/null @@ -1,400 +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.xy; - -import android.content.Context; -import android.graphics.PointF; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnTouchListener; - -public class XYPlotZoomPan extends XYPlot implements OnTouchListener { - private static final float MIN_DIST_2_FING = 5f; - - // Definition of the touch states - private enum State - { - NONE, - ONE_FINGER_DRAG, - TWO_FINGERS_DRAG - } - - private State mode = State.NONE; - private float minXLimit = Float.MAX_VALUE; - private float maxXLimit = Float.MAX_VALUE; - private float minYLimit = Float.MAX_VALUE; - private float maxYLimit = Float.MAX_VALUE; - private float lastMinX = Float.MAX_VALUE; - private float lastMaxX = Float.MAX_VALUE; - private float lastMinY = Float.MAX_VALUE; - private float lastMaxY = Float.MAX_VALUE; - private PointF firstFingerPos; - private float mDistX; - private boolean mZoomEnabled; //default is enabled - private boolean mZoomVertically; - private boolean mZoomHorizontally; - private boolean mCalledBySelf; - private boolean mZoomEnabledInit; - private boolean mZoomVerticallyInit; - private boolean mZoomHorizontallyInit; - - public XYPlotZoomPan(Context context, String title, RenderMode mode) { - super(context, title, mode); - setZoomEnabled(true); //Default is ZoomEnabled if instantiated programmatically - } - - public XYPlotZoomPan(final Context context, final AttributeSet attrs) { - super(context, attrs); - if(mZoomEnabled || !mZoomEnabledInit) { - setZoomEnabled(true); - } - if(!mZoomHorizontallyInit) { - mZoomHorizontally = true; - } - if(!mZoomVerticallyInit) { - mZoomVertically = true; - } - } - - public XYPlotZoomPan(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - if(mZoomEnabled || !mZoomEnabledInit) { - setZoomEnabled(true); - } - if(!mZoomHorizontallyInit) { - mZoomHorizontally = true; - } - if(!mZoomVerticallyInit) { - mZoomVertically = true; - } - } - - public XYPlotZoomPan(final Context context, final String title) { - super(context, title); - } - - @Override - public void setOnTouchListener(OnTouchListener l) { - if(l != this) { - mZoomEnabled = false; - } - super.setOnTouchListener(l); - } - - public boolean getZoomVertically() { - return mZoomVertically; - } - - public void setZoomVertically(boolean zoomVertically) { - mZoomVertically = zoomVertically; - mZoomVerticallyInit = true; - } - - public boolean getZoomHorizontally() { - return mZoomHorizontally; - } - - public void setZoomHorizontally(boolean zoomHorizontally) { - mZoomHorizontally = zoomHorizontally; - mZoomHorizontallyInit = true; - } - - public void setZoomEnabled(boolean enabled) { - if(enabled) { - setOnTouchListener(this); - } else { - setOnTouchListener(null); - } - mZoomEnabled = enabled; - mZoomEnabledInit = true; - } - - public boolean getZoomEnabled() { - return mZoomEnabled; - } - - private float getMinXLimit() { - if(minXLimit == Float.MAX_VALUE) { - minXLimit = getCalculatedMinX().floatValue(); - lastMinX = minXLimit; - } - return minXLimit; - } - - private float getMaxXLimit() { - if(maxXLimit == Float.MAX_VALUE) { - maxXLimit = getCalculatedMaxX().floatValue(); - lastMaxX = maxXLimit; - } - return maxXLimit; - } - - private float getMinYLimit() { - if(minYLimit == Float.MAX_VALUE) { - minYLimit = getCalculatedMinY().floatValue(); - lastMinY = minYLimit; - } - return minYLimit; - } - - private float getMaxYLimit() { - if(maxYLimit == Float.MAX_VALUE) { - maxYLimit = getCalculatedMaxY().floatValue(); - lastMaxY = maxYLimit; - } - return maxYLimit; - } - - private float getLastMinX() { - if(lastMinX == Float.MAX_VALUE) { - lastMinX = getCalculatedMinX().floatValue(); - } - return lastMinX; - } - - private float getLastMaxX() { - if(lastMaxX == Float.MAX_VALUE) { - lastMaxX = getCalculatedMaxX().floatValue(); - } - return lastMaxX; - } - - private float getLastMinY() { - if(lastMinY == Float.MAX_VALUE) { - lastMinY = getCalculatedMinY().floatValue(); - } - return lastMinY; - } - - private float getLastMaxY() { - if(lastMaxY == Float.MAX_VALUE) { - lastMaxY = getCalculatedMaxY().floatValue(); - } - return lastMaxY; - } - - @Override - public synchronized void setDomainBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, final Number upperBoundary, final BoundaryMode upperBoundaryMode) { - super.setDomainBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode); - if(mCalledBySelf) { - mCalledBySelf = false; - } else { - minXLimit = lowerBoundaryMode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinX().floatValue(); - maxXLimit = upperBoundaryMode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxX().floatValue(); - lastMinX = minXLimit; - lastMaxX = maxXLimit; - } - } - - @Override - public synchronized void setRangeBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, final Number upperBoundary, final BoundaryMode upperBoundaryMode) { - super.setRangeBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode); - if(mCalledBySelf) { - mCalledBySelf = false; - } else { - minYLimit = lowerBoundaryMode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinY().floatValue(); - maxYLimit = upperBoundaryMode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxY().floatValue(); - lastMinY = minYLimit; - lastMaxY = maxYLimit; - } - } - - @Override - public synchronized void setDomainBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) { - super.setDomainBoundaries(lowerBoundary, upperBoundary, mode); - if(mCalledBySelf) { - mCalledBySelf = false; - } else { - minXLimit = mode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinX().floatValue(); - maxXLimit = mode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxX().floatValue(); - lastMinX = minXLimit; - lastMaxX = maxXLimit; - } - } - - @Override - public synchronized void setRangeBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) { - super.setRangeBoundaries(lowerBoundary, upperBoundary, mode); - if(mCalledBySelf) { - mCalledBySelf = false; - } else { - minYLimit = mode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinY().floatValue(); - maxYLimit = mode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxY().floatValue(); - lastMinY = minYLimit; - lastMaxY = maxYLimit; - } - } - - @Override - public boolean onTouch(final View view, final MotionEvent event) { - switch (event.getAction() & MotionEvent.ACTION_MASK) - { - case MotionEvent.ACTION_DOWN: // start gesture - firstFingerPos = new PointF(event.getX(), event.getY()); - mode = State.ONE_FINGER_DRAG; - break; - case MotionEvent.ACTION_POINTER_DOWN: // second finger - { - mDistX = getXDistance(event); - // the distance check is done to avoid false alarms - if(mDistX > MIN_DIST_2_FING || mDistX < -MIN_DIST_2_FING) { - mode = State.TWO_FINGERS_DRAG; - } - break; - } - case MotionEvent.ACTION_POINTER_UP: // end zoom - mode = State.NONE; - break; - case MotionEvent.ACTION_MOVE: - if(mode == State.ONE_FINGER_DRAG) { - pan(event); - } else if(mode == State.TWO_FINGERS_DRAG) { - zoom(event); - } - break; - } - return true; - } - - private float getXDistance(final MotionEvent event) { - return event.getX(0) - event.getX(1); - } - - private void pan(final MotionEvent motionEvent) { - final PointF oldFirstFinger = firstFingerPos; //save old position of finger - firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position - PointF newX = new PointF(); - if(mZoomHorizontally) { - calculatePan(oldFirstFinger, newX, true); - mCalledBySelf = true; - super.setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED); - lastMinX = newX.x; - lastMaxX = newX.y; - } - if(mZoomVertically) { - calculatePan(oldFirstFinger, newX, false); - mCalledBySelf = true; - super.setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED); - lastMinY = newX.x; - lastMaxY = newX.y; - } - redraw(); - } - - private void calculatePan(final PointF oldFirstFinger, PointF newX, final boolean horizontal) { - final float offset; - // multiply the absolute finger movement for a factor. - // the factor is dependent on the calculated min and max - if(horizontal) { - newX.x = getLastMinX(); - newX.y = getLastMaxX(); - offset = (oldFirstFinger.x - firstFingerPos.x) * ((newX.y - newX.x) / getWidth()); - } else { - newX.x = getLastMinY(); - newX.y = getLastMaxY(); - offset = -(oldFirstFinger.y - firstFingerPos.y) * ((newX.y - newX.x) / getHeight()); - } - // move the calculated offset - newX.x = newX.x + offset; - newX.y = newX.y + offset; - //get the distance between max and min - final float diff = newX.y - newX.x; - //check if we reached the limit of panning - if(horizontal) { - if(newX.x < getMinXLimit()) { - newX.x = getMinXLimit(); - newX.y = newX.x + diff; - } - if(newX.y > getMaxXLimit()) { - newX.y = getMaxXLimit(); - newX.x = newX.y - diff; - } - } else { - if(newX.x < getMinYLimit()) { - newX.x = getMinYLimit(); - newX.y = newX.x + diff; - } - if(newX.y > getMaxYLimit()) { - newX.y = getMaxYLimit(); - newX.x = newX.y - diff; - } - } - } - - private void zoom(final MotionEvent motionEvent) { - final float oldDist = mDistX; - final float newDist = getXDistance(motionEvent); - // sign change! Fingers have crossed ;-) - if(oldDist > 0 && newDist < 0 || oldDist < 0 && newDist > 0) { - return; - } - mDistX = newDist; - float scale = (oldDist / mDistX); - // sanity check - if(Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) { - return; - } - PointF newX = new PointF(); - if(mZoomHorizontally) { - calculateZoom(scale, newX, true); - mCalledBySelf = true; - super.setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED); - lastMinX = newX.x; - lastMaxX = newX.y; - } - if(mZoomVertically) { - calculateZoom(scale, newX, false); - mCalledBySelf = true; - super.setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED); - lastMinY = newX.x; - lastMaxY = newX.y; - } - redraw(); - } - - private void calculateZoom(float scale, PointF newX, final boolean horizontal) { - final float calcMax; - final float span; - if(horizontal) { - calcMax = getLastMaxX(); - span = calcMax - getLastMinX(); - } else { - calcMax = getLastMaxY(); - span = calcMax - getLastMinY(); - } - final float midPoint = calcMax - (span / 2.0f); - final float offset = span * scale / 2.0f; - newX.x = midPoint - offset; - newX.y = midPoint + offset; - if(horizontal) { - if(newX.x < getMinXLimit()) { - newX.x = getMinXLimit(); - } - if(newX.y > getMaxXLimit()) { - newX.y = getMaxXLimit(); - } - } else { - if(newX.x < getMinYLimit()) { - newX.x = getMinYLimit(); - } - if(newX.y > getMaxYLimit()) { - newX.y = getMaxYLimit(); - } - } - } -} 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 517595f3..0492c6c2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java @@ -18,14 +18,14 @@ import android.content.Context; import android.graphics.Paint; -import com.androidplot.util.Configurator; + +import com.halfhp.fig.*; /** * Base class of all XYRegionFormatters. */ public class XYRegionFormatter { - //private int color; private Paint paint = new Paint(); { @@ -43,16 +43,16 @@ public class XYRegionFormatter { public XYRegionFormatter(Context ctx, int xmlCfgId) { // prevent configuration of classes derived from this one: if (getClass().equals(XYRegionFormatter.class)) { - Configurator.configure(ctx, this, xmlCfgId); + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } } } public XYRegionFormatter(int color) { - //paint = new Paint(); paint.setColor(color); - //paint.setStyle(Paint.Style.FILL); - //paint.setAntiAlias(true); - //this.color = color; } public int getColor() { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java index a9053095..5afb8963 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java @@ -16,7 +16,6 @@ package com.androidplot.xy; -import android.util.Pair; import com.androidplot.Series; /** @@ -27,7 +26,7 @@ public interface XYSeries extends Series { /** * @return Number of elements in this Series. */ - public int size(); + int size(); /** * Returns the x-value for an index within a series. @@ -37,7 +36,7 @@ public interface XYSeries extends Series { * * @return The x-value. */ - public Number getX(int index); + Number getX(int index); /** * Returns the y-value for an index within a series. @@ -47,5 +46,5 @@ public interface XYSeries extends Series { * * @return The y-value. */ - public Number getY(int index); + Number getY(int index); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java new file mode 100644 index 00000000..fce15b59 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java @@ -0,0 +1,13 @@ +package com.androidplot.xy; + +import com.androidplot.ui.*; + +/** + * Created by halfhp on 10/6/16. + */ +public class XYSeriesBundle extends SeriesBundle { + + public XYSeriesBundle(XYSeries series, XYSeriesFormatter formatter) { + super(series, formatter); + } +} 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 7244982c..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,15 +16,34 @@ package com.androidplot.xy; +import android.content.Context; + import com.androidplot.ui.Formatter; -import com.androidplot.util.ZHash; -import com.androidplot.util.ZIndexable; +import com.androidplot.util.LayerHash; +import com.androidplot.util.Layerable; public abstract class XYSeriesFormatter extends Formatter { - ZHash regions; + + // instantiate a default implementation prints point's yVal: + private PointLabeler pointLabeler = new PointLabeler() { + @Override + public String getLabel(XYSeries series, int index) { + return String.valueOf(series.getY(index)); + } + }; + + private PointLabelFormatter pointLabelFormatter; + + LayerHash regions; { - regions = new ZHash(); + regions = new LayerHash<>(); + } + + public XYSeriesFormatter() {} + + public XYSeriesFormatter(Context context, int xmlCfgId) { + super(context, xmlCfgId); } public void addRegion(RectRegion region, XYRegionFormatterType regionFormatter) { @@ -39,7 +58,7 @@ public void removeRegion(RectRegion region) { * Can be used to access z-index manipulation methods of ZIndexable. * @return */ - public ZIndexable getRegions() { + public Layerable getRegions() { return regions; } @@ -50,4 +69,28 @@ public ZIndexable getRegions() { public XYRegionFormatterType getRegionFormatter(RectRegion region) { return regions.get(region); } + + + public PointLabeler getPointLabeler() { + return pointLabeler; + } + + public void setPointLabeler(PointLabeler pointLabeler) { + this.pointLabeler = pointLabeler; + } + + public boolean hasPointLabelFormatter() { + return pointLabelFormatter != null; + } + + public PointLabelFormatter getPointLabelFormatter() { + if(pointLabelFormatter == null) { + pointLabelFormatter = new PointLabelFormatter(); + } + return pointLabelFormatter; + } + + public void setPointLabelFormatter(PointLabelFormatter pointLabelFormatter) { + this.pointLabelFormatter = pointLabelFormatter; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRegistry.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRegistry.java new file mode 100644 index 00000000..e7934a5a --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRegistry.java @@ -0,0 +1,36 @@ +package com.androidplot.xy; + +import com.androidplot.*; + +/** + * Maintains the "registry" of mappings of XYSeries instances and their associated formatters. + */ +public class XYSeriesRegistry extends SeriesRegistry { + + private Estimator estimator; + + public void estimate(XYPlot plot) { + if(estimator != null) { + for (XYSeriesBundle sf : getSeriesAndFormatterList()) { + getEstimator().run(plot, sf); + } + } + } + + @Override + protected XYSeriesBundle newSeriesBundle(XYSeries series, XYSeriesFormatter formatter) { + return new XYSeriesBundle(series, formatter); + } + + /** + * + * @return The currently active Estimator, or null if none is set. + */ + public Estimator getEstimator() { + return estimator; + } + + public void setEstimator(Estimator estimator) { + this.estimator = estimator; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java index a9d976a5..5617d411 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java @@ -16,9 +16,9 @@ package com.androidplot.xy; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.SeriesRenderer; -import com.androidplot.util.ZIndexable; +import com.androidplot.util.Layerable; import java.util.Hashtable; @@ -26,8 +26,8 @@ * Base class for all Renderers that render XYSeries data. * @param */ -public abstract class XYSeriesRenderer - extends SeriesRenderer { +public abstract class XYSeriesRenderer + extends SeriesRenderer { public XYSeriesRenderer(XYPlot plot) { super(plot); @@ -39,9 +39,9 @@ public XYSeriesRenderer(XYPlot plot) { */ public Hashtable getUniqueRegionFormatters() { - Hashtable found = new Hashtable(); - for(SeriesAndFormatter sfPair : getSeriesList()) { - ZIndexable regionIndexer = sfPair.getFormatter().getRegions(); + Hashtable found = new Hashtable<>(); + for(SeriesBundle sfPair : getSeriesAndFormatterList()) { + Layerable regionIndexer = sfPair.getFormatter().getRegions(); for (RectRegion region : regionIndexer.elements()) { XYRegionFormatter f = sfPair.getFormatter().getRegionFormatter(region); found.put(f, region.getLabel()); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java b/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java index 6fda2894..34de518d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java @@ -17,7 +17,8 @@ package com.androidplot.xy; import android.graphics.RectF; -import com.androidplot.util.ValPixConverter; + +import com.androidplot.*; /** * Calculates "stepping" values for a plot. These values are most commonly used for @@ -30,46 +31,50 @@ public class XYStepCalculator { * Convenience method - wraps other form of getStep(). * @param plot * @param axisType - * @param rect - * @param minVal - * @param maxVal + * @param pixRect * @return */ - public static XYStep getStep(XYPlot plot, XYAxisType axisType, RectF rect, Number minVal, Number maxVal) { - XYStep step = null; + public static Step getStep(XYPlot plot, Axis axisType, RectF pixRect) { + Step step = null; switch(axisType) { case DOMAIN: - step = getStep(plot.getDomainStepMode(), rect.width(), plot.getDomainStepValue(), minVal, maxVal); + step = getStep(plot.getDomainStepMode(), + plot.getDomainStepValue(), + plot.getBounds().getxRegion(), + new Region(pixRect.left, pixRect.right)); break; case RANGE: - step = getStep(plot.getRangeStepMode(), rect.height(), plot.getRangeStepValue(), minVal, maxVal); + step = getStep(plot.getRangeStepMode(), + plot.getRangeStepValue(), + plot.getBounds().getyRegion(), + new Region(pixRect.top, pixRect.bottom)); break; } return step; } - public static XYStep getStep(XYStepMode typeXY, float plotPixelSize, double stepValue, Number minVal, Number maxVal) { - //XYStep step = new XYStep(); + public static Step getStep(StepMode typeXY, double stepValue, Region realBounds, Region pixelBounds) { double stepVal = 0; - float stepPix = 0; - float stepCount = 0; + double stepPix = 0; + double stepCount = 0; switch(typeXY) { case INCREMENT_BY_VAL: + case INCREMENT_BY_FIT: stepVal = stepValue; - stepPix = (float)(stepValue/ ValPixConverter.valPerPix(minVal.doubleValue(), maxVal.doubleValue(), plotPixelSize)); - stepCount = plotPixelSize /stepPix; + stepPix = stepValue / realBounds.ratio(pixelBounds).doubleValue(); + stepCount = pixelBounds.length().doubleValue() / stepPix; break; case INCREMENT_BY_PIXELS: - stepPix = new Double(stepValue).floatValue(); - stepCount = plotPixelSize /stepPix; - stepVal = ValPixConverter.valPerPix(minVal.doubleValue(), maxVal.doubleValue(), plotPixelSize)*stepPix; + stepPix = stepValue; + stepVal = realBounds.ratio(pixelBounds).doubleValue() * stepPix; + stepCount = pixelBounds.length().doubleValue() / stepPix; break; case SUBDIVIDE: - stepCount = new Double(stepValue).floatValue(); - stepPix = (plotPixelSize /(stepCount-1)); - stepVal = ValPixConverter.valPerPix(minVal.doubleValue(), maxVal.doubleValue(), plotPixelSize)*stepPix; + stepCount = stepValue; + stepPix = pixelBounds.length().doubleValue() / (stepCount - 1); + stepVal = realBounds.ratio(pixelBounds).doubleValue() * stepPix; break; } - return new XYStep(stepCount, stepPix, stepVal); + return new Step(stepCount, stepPix, stepVal); } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/YValueMarker.java b/androidplot-core/src/main/java/com/androidplot/xy/YValueMarker.java index 7c08ec82..0e4aa435 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/YValueMarker.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/YValueMarker.java @@ -16,11 +16,14 @@ package com.androidplot.xy; +import android.graphics.Canvas; import android.graphics.Paint; -import com.androidplot.ui.XLayoutStyle; -import com.androidplot.ui.XPositionMetric; +import android.graphics.RectF; -public class YValueMarker extends ValueMarker { +import com.androidplot.ui.HorizontalPositioning; +import com.androidplot.ui.HorizontalPosition; + +public class YValueMarker extends ValueMarker { /** @@ -29,7 +32,7 @@ public class YValueMarker extends ValueMarker { * @param text Set to null to use the plot's default getFormatter. */ public YValueMarker(Number value, String text) { - super(value, text, new XPositionMetric(3, XLayoutStyle.ABSOLUTE_FROM_LEFT)); + super(value, text, new HorizontalPosition(3, HorizontalPositioning.ABSOLUTE_FROM_LEFT)); } /** @@ -40,7 +43,7 @@ public YValueMarker(Number value, String text) { * @param linePaint * @param textPaint */ - public YValueMarker(Number value, String text, XPositionMetric textPosition, Paint linePaint, Paint textPaint) { + public YValueMarker(Number value, String text, HorizontalPosition textPosition, Paint linePaint, Paint textPaint) { super(value, text, textPosition, linePaint, textPaint); } @@ -52,7 +55,24 @@ public YValueMarker(Number value, String text, XPositionMetric textPosition, Pai * @param linePaint * @param textPaint */ - public YValueMarker(Number value, String text, XPositionMetric textPosition, int linePaint, int textPaint) { + public YValueMarker(Number value, String text, HorizontalPosition 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 yPix = (float) plot.getBounds().yRegion + .transform(getValue() + .doubleValue(), gridRect.top, gridRect.bottom, true); + canvas.drawLine(gridRect.left, yPix, + gridRect.right, yPix, getLinePaint() + ); + + float xPix = getTextPosition().getPixelValue( + gridRect.width()); + xPix += gridRect.left; + drawMarkerText(canvas, getText(), gridRect, xPix, yPix); + } + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java b/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java new file mode 100644 index 00000000..14b7583b --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java @@ -0,0 +1,26 @@ +package com.androidplot.xy; + +/** + * Estimates optimal zoom level to be applied to a {@link SampledXYSeries} based on the current + * visible bounds of the owning {@link XYPlot}. + */ +public class ZoomEstimator extends Estimator { + + @Override + public void run(XYPlot plot, XYSeriesBundle sf) { + if(sf.getSeries() instanceof SampledXYSeries) { + SampledXYSeries oxy = (SampledXYSeries) sf.getSeries(); + final double factor = calculateZoom(oxy, plot.getBounds()); + oxy.setZoomFactor(factor); + } + } + + protected double calculateZoom(SampledXYSeries series, RectRegion visibleBounds) { + RectRegion seriesBounds = series.getBounds(); + final double ratio = seriesBounds.getxRegion().ratio(visibleBounds.getxRegion()).doubleValue(); + final double maxFactor = series.getMaxZoomFactor(); + final double factor = Math.abs(Math.round(maxFactor / ratio)); + return factor > 0 ? factor : 1; + } + +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/package-info.java b/androidplot-core/src/main/java/com/androidplot/xy/package-info.java new file mode 100644 index 00000000..60a9f91c --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes involved in the creation and presentation of {@link com.androidplot.xy.XYPlot}. + */ +package com.androidplot.xy; \ No newline at end of file diff --git a/androidplot-core/src/main/res/values/attrs.xml b/androidplot-core/src/main/res/values/attrs.xml index c00a963d..5c714912 100644 --- a/androidplot-core/src/main/res/values/attrs.xml +++ b/androidplot-core/src/main/res/values/attrs.xml @@ -1,5 +1,5 @@ - + + - + + + + + + + @@ -34,9 +52,56 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -48,7 +113,7 @@ - + @@ -60,7 +125,7 @@ - + @@ -72,55 +137,76 @@ - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + @@ -129,7 +215,7 @@ - + @@ -138,7 +224,7 @@ - + @@ -147,7 +233,7 @@ - + @@ -156,7 +242,7 @@ - + @@ -165,7 +251,7 @@ - + @@ -174,113 +260,875 @@ - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +`VerticalPositioning` component of the `VerticalPosition` of the `TextLabelWidget` +that displays the domain title. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. +--> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + - - - - - - - +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" +``` +--> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + - - + + + + + + + + + + + - - + + + \ No newline at end of file diff --git a/androidplot-core/src/main/res/values/color.xml b/androidplot-core/src/main/res/values/color.xml new file mode 100644 index 00000000..659c1ac8 --- /dev/null +++ b/androidplot-core/src/main/res/values/color.xml @@ -0,0 +1,27 @@ + + + + + + + #00000000 + #000 + #111 + #FFF + #EEE + #888 + \ No newline at end of file diff --git a/androidplot-core/src/main/res/values/style.xml b/androidplot-core/src/main/res/values/style.xml new file mode 100644 index 00000000..5c861e55 --- /dev/null +++ b/androidplot-core/src/main/res/values/style.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/androidplot-core/src/test/java/android/net/http/AndroidHttpClient.java b/androidplot-core/src/test/java/android/net/http/AndroidHttpClient.java deleted file mode 100644 index c27de3cf..00000000 --- a/androidplot-core/src/test/java/android/net/http/AndroidHttpClient.java +++ /dev/null @@ -1,24 +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 android.net.http; - -/** - * Workaround for Robolectric bug: - * https://github.com/robolectric/robolectric/issues/1862 - * we can and should remove this package/class once Robolectric 3.1 is available. - */ -public class AndroidHttpClient {} \ No newline at end of file diff --git a/androidplot-core/src/test/java/com/androidplot/LineRegionTest.java b/androidplot-core/src/test/java/com/androidplot/LineRegionTest.java deleted file mode 100644 index a2415840..00000000 --- a/androidplot-core/src/test/java/com/androidplot/LineRegionTest.java +++ /dev/null @@ -1,85 +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; - -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; - -public class LineRegionTest { - - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testConstructor() throws Exception { - LineRegion lr = new LineRegion(0d, 0d); - assertEquals(0d, lr.getMinVal()); - assertEquals(0d, lr.getMaxVal()); - - lr = new LineRegion(1.5d, -2d); - assertEquals(-2d, lr.getMinVal()); - assertEquals(1.5d, lr.getMaxVal()); - - lr = new LineRegion(10d, 20d); - assertEquals(10d, lr.getMinVal()); - assertEquals(20d, lr.getMaxVal()); - } - - - @Test - public void testContains() throws Exception { - - } - - @Test - public void testIntersects() throws Exception { - LineRegion line1 = new LineRegion(1, 10); - LineRegion line2 = new LineRegion(11, 20); - assertFalse(line1.intersects(line2)); - - line1.setMaxVal(15); - assertTrue(line1.intersects(line2)); - - //l1end = 30; - line1.setMaxVal(30); - assertTrue(line1.intersects(line2)); - - //l1start = 21; - line1.setMinVal(21); - assertFalse(line1.intersects(line2)); - } - - @Test - public void testLength() throws Exception { - LineRegion lr = new LineRegion(0, 10); - assertEquals(10d, lr.length().doubleValue(), 0); - - lr = new LineRegion(-5, 5); - assertEquals(10d, lr.length().doubleValue(), 0); - } -} diff --git a/androidplot-core/src/test/java/com/androidplot/PlotTest.java b/androidplot-core/src/test/java/com/androidplot/PlotTest.java index 074a217d..9e4753bf 100644 --- a/androidplot-core/src/test/java/com/androidplot/PlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/PlotTest.java @@ -18,17 +18,13 @@ import android.content.res.TypedArray; import android.graphics.*; -import com.androidplot.exception.PlotRenderException; -import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesAndFormatter; -import com.androidplot.ui.SeriesRenderer; -import com.androidplot.ui.Formatter; -import com.androidplot.util.Configurator; -import mockit.*; -import org.junit.After; +import android.util.*; + +import com.androidplot.test.*; +import com.androidplot.ui.*; +import com.halfhp.fig.*; import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; +import org.mockito.Mock; import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; import java.util.HashMap; @@ -37,162 +33,51 @@ import static junit.framework.Assert.assertNotSame; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; -@RunWith(RobolectricTestRunner.class) -public class PlotTest { - - 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 getRendererClass() { - return MockRenderer1.class; - } - - @Override - public SeriesRenderer getRendererInstance(MockPlot plot) { - return new MockRenderer1(plot); - } - } - - static class MockFormatter2 extends Formatter { - - @Override - public Class getRendererClass() { - return MockRenderer2.class; - } - - @Override - public SeriesRenderer getRendererInstance(MockPlot plot) { - return new MockRenderer2(plot); - } - } - - //@MockClass(realClass = Plot.class) - public static class MockPlot extends Plot { - public MockPlot(String title) { - super(RuntimeEnvironment.application, title); - } - - @Override - protected void onPreInit() { - - } +public class PlotTest extends AndroidplotTest { - @Override - protected void processAttrs(TypedArray attrs) { + @Mock + SeriesRegistry mockSeriesRegistry; - } + @Test + public void testInit_withoutAttrs() { + //Plot plot = mock(Plot.class); + Plot plot = spy(new MockPlot("MockPlot")); + plot.init(RuntimeEnvironment.application, null, 0); - /*@Override - protected SeriesRenderer doGetRendererInstance(Class clazz) { - if(clazz == MockRenderer1.class) { - return new MockRenderer1(this); - } else if(clazz == MockRenderer2.class) { - return new MockRenderer2(this); - } else { - return null; - } - }*/ + verify(plot, times(1)).onPreInit(); + verify(plot, times(1)).onAfterConfig(); } - /*@Before - public void setUp() throws Exception { - Mockit.setUpMocks(MockPaint.class,MockContext.class); - }*/ - - @After - public void tearDown() throws Exception { + @Test + public void testInit_withAttrs() { + Plot plot = spy(new MockPlot("MockPlot")); + AttributeSet attrs = mock(AttributeSet.class); + plot.init(RuntimeEnvironment.application, attrs, 0); + verify(plot, times(1)).onPreInit(); + verify(plot, times(1)).onAfterConfig(); } @Test - public void testAddSeries() throws Exception { - //Context context = Mockit.setUpMock(new MockContext()); - //Plot plot = Mockit.setUpMock(Plot.class, new MockPlot(context, "MockPlot")); - //Plot plot = Mockit.setUpMock(new MockPlot()); + public void testAddSeries() { Plot plot = new MockPlot("MockPlot"); MockSeries m1 = new MockSeries(); Class cl = MockRenderer1.class; - - plot.addSeries(m1, new MockFormatter1()); + assertEquals(1, plot.getRegistry().size()); - ArrayList> registry = - Deencapsulation.getField(plot, "seriesRegistry"); - assertEquals(1, registry.size()); - //assertEquals(1, registry.get(cl).size()); - + // a new copy of m1 is added: plot.addSeries(m1, new MockFormatter1()); - // duplicate Renderer added, registry size should not grow: - //assertEquals(1, registry.size()); - //assertEquals(1, registry.get(cl).size()); assertEquals(1, plot.getRenderers().size()); - assertEquals(1, plot.getRenderer(cl).getSeriesList().size()); + assertEquals(2, plot.getRenderer(cl).getSeriesList().size()); MockSeries m2 = new MockSeries(); @@ -201,9 +86,8 @@ public void testAddSeries() throws Exception { // still should only be one renderer type: assertEquals(1, plot.getRendererList().size()); - // we added a new instance of cl to the renderer so there should be 2 in the subregistry: - //assertEquals(2, registry.get(cl).size()); - assertEquals(2, plot.getRenderer(cl).getSeriesList().size()); + // we added a new instance of cl to the renderer so there should be 3 in the subregistry: + assertEquals(3, plot.getRenderer(cl).getSeriesList().size()); // lets add another renderer: @@ -213,12 +97,9 @@ public void testAddSeries() throws Exception { } @Test - public void testRemoveSeries() throws Exception { + public void testRemoveSeries() { - //Context context = Mockit.setUpMock(new MockContext()); Plot plot = new MockPlot("MockPlot"); - ArrayList> registry = - Deencapsulation.getField(plot, "seriesRegistry"); MockSeries m1 = new MockSeries(); MockSeries m2 = new MockSeries(); @@ -233,33 +114,22 @@ public void testRemoveSeries() throws Exception { plot.addSeries(m3, new MockFormatter2()); - // a quick sanity check: + // a quick sanity run: assertEquals(2, plot.getRendererList().size()); - //assertEquals(3, registry.get(MockRenderer1.class).size()); assertEquals(3, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); - //assertEquals(3, registry.get(MockRenderer2.class).size()); assertEquals(3, plot.getRenderer(MockRenderer2.class).getSeriesList().size()); plot.removeSeries(m1, MockRenderer1.class); - //assertEquals(2, registry.get(MockRenderer1.class).size()); assertEquals(2, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); plot.removeSeries(m2, MockRenderer1.class); - //assertEquals(1, registry.get(MockRenderer1.class).size()); assertEquals(1, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); plot.removeSeries(m2, MockRenderer1.class); - //assertEquals(1, registry.get(MockRenderer1.class).size()); assertEquals(1, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); plot.removeSeries(m3, MockRenderer1.class); - // all the elements should be gone from MockRenderer1, thus the renderer should - // also be gone: - //assertNull(registry.get(MockRenderer1.class)); - //assertNull(plot.getRenderer(MockRenderer1.class)); - - // add em all back plot.addSeries(m1, new MockFormatter1()); plot.addSeries(m2, new MockFormatter1()); @@ -270,38 +140,25 @@ public void testRemoveSeries() throws Exception { plot.addSeries(m3, new MockFormatter1()); - // a quick sanity check: + // a quick sanity run: assertEquals(2, plot.getRendererList().size()); - //assertEquals(3, registry.get(MockRenderer1.class).size()); - assertEquals(3, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); - //assertEquals(3, registry.get(MockRenderer2.class).size()); + assertEquals(6, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); assertEquals(3, plot.getRenderer(MockRenderer2.class).getSeriesList().size()); // now lets try removing a series from all renderers: plot.removeSeries(m1); - //assertEquals(2, registry.get(MockRenderer1.class).size()); - assertEquals(2, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); - //assertEquals(2, registry.get(MockRenderer2.class).size()); + assertEquals(4, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); assertEquals(2, plot.getRenderer(MockRenderer2.class).getSeriesList().size()); // and now lets remove the remaining series: plot.removeSeries(m2); plot.removeSeries(m3); - - // nothing should be left: - //assertNull(registry.get(MockRenderer1.class)); - //assertNull(plot.getRenderer(MockRenderer1.class)); - //assertNull(registry.get(MockRenderer2.class)); - //assertNull(plot.getRenderer(MockRenderer2.class)); } @Test - public void testGetFormatter() throws Exception { - //Context context = Mockit.setUpMock(new MockContext()); + public void testGetFormatter() { Plot plot = new MockPlot("MockPlot"); - ArrayList> registry = - Deencapsulation.getField(plot, "seriesRegistry"); MockSeries m1 = new MockSeries(); MockSeries m2 = new MockSeries(); @@ -319,25 +176,18 @@ public void testGetFormatter() throws Exception { plot.addSeries(m2, f3); plot.addSeries(m3, new MockFormatter1()); - - //assertEquals(registry.get(MockRenderer1.class).getFormatter(m1), f1); assertEquals(plot.getRenderer(MockRenderer1.class).getFormatter(m1), f1); - //assertEquals(registry.get(MockRenderer1.class).getFormatter(m2), f2); assertEquals(plot.getRenderer(MockRenderer1.class).getFormatter(m2), f2); - //assertEquals(registry.get(MockRenderer2.class).getFormatter(m2), f3); assertEquals(plot.getRenderer(MockRenderer2.class).getFormatter(m2), f3); - //assertNotSame(registry.get(MockRenderer2.class).getFormatter(m2), f1); assertNotSame(plot.getRenderer(MockRenderer2.class).getFormatter(m2), f1); } @Test - public void testGetRendererList() throws Exception { + public void testGetRendererList() { - //Context context = Mockit.setUpMock(new MockContext()); Plot plot = new MockPlot("MockPlot"); - //LinkedHashMap, SeriesAndFormatterList> registry = Deencapsulation.getField(plot, "seriesRegistry"); MockSeries m1 = new MockSeries(); MockSeries m2 = new MockSeries(); @@ -353,17 +203,12 @@ public void testGetRendererList() throws Exception { List rList = plot.getRendererList(); assertEquals(2, rList.size()); - - //assertEquals(MockRenderer1.class, rList.get(0).getClass()); - //assertEquals(MockRenderer2.class, rList.get(1).getClass()); } @Test - public void testAddListener() throws Exception { - //Context context = Mockit.setUpMock(new MockContext()); + public void testAddListener() { Plot plot = new MockPlot("MockPlot"); - ArrayList listeners = Deencapsulation.getField(plot, "listeners"); - //LinkedHashMap, SeriesAndFormatterList> registry = Deencapsulation.getField(plot, "seriesRegistry"); + ArrayList listeners = plot.getListeners(); assertEquals(0, listeners.size()); @@ -383,15 +228,13 @@ public void testAddListener() throws Exception { plot.addListener(pl2); assertEquals(2, listeners.size()); - + } @Test - public void testRemoveListener() throws Exception { - //Context context = Mockit.setUpMock(new MockContext()); + public void testRemoveListener() { Plot plot = new MockPlot("MockPlot"); - ArrayList listeners = Deencapsulation.getField(plot, "listeners"); - //LinkedHashMap, SeriesAndFormatterList> registry = Deencapsulation.getField(plot, "seriesRegistry"); + ArrayList listeners = plot.getListeners(); assertEquals(0, listeners.size()); @@ -420,49 +263,206 @@ public void testRemoveListener() throws Exception { } - /*@Test - public void testGuessGetterName() throws Exception { - Context context = Mockit.setUpMock(new MockContext()); - Plot plot = new MockPlot(context, "MockPlot"); - - Method m = Plot.class.getDeclaredMethod("guessGetterMethod", Object.class, String.class); - assertNotNull(m); - } - - @Test - public void testGuessSetterName() throws Exception { - Context context = Mockit.setUpMock(new MockContext()); - Plot plot = new MockPlot(context, "MockPlot"); - - Method m = Plot.class.getDeclaredMethod("guessSetterMethod", Object.class, String.class, Class.class); - assertNotNull(m); - }*/ - - - @Test public void testConfigure() throws Exception { - //Context context = Mockit.setUpMock(new MockContext.MockContext2()); - //Context context = new MockContext.MockContext2(); Plot plot = new MockPlot("MockPlot"); HashMap params = new HashMap(); String param1 = "this is a test."; - //String param2 = Plot.RenderMode.USE_BACKGROUND_THREAD.toString(); String param2 = "use_background_thread"; String param3 = "#FF0000"; - params.put("title", param1); + params.put("title.text", param1); params.put("renderMode", param2); params.put("backgroundPaint.color", param3); + Fig.configure(RuntimeEnvironment.application, plot, params); - //Method m = Plot.class.getDeclaredMethod("configure", params.getClass()); - //m.setAccessible(true); - //m.invoke(plot, params); - Configurator.configure(RuntimeEnvironment.application, plot, params); - - assertEquals(param1, plot.getTitle()); + assertEquals(param1, plot.getTitle().getText()); 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 getRendererClass() { + return MockRenderer1.class; + } + + @Override + public SeriesRenderer doGetRendererInstance(MockPlot plot) { + return new MockRenderer1(plot); + } + } + + static class MockFormatter2 extends Formatter { + + @Override + public Class 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 27ae4d34..c38ded3c 100644 --- a/androidplot-core/src/test/java/com/androidplot/RegionTest.java +++ b/androidplot-core/src/test/java/com/androidplot/RegionTest.java @@ -1,57 +1,162 @@ -/* - * 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 org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class RegionTest { - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testContains() throws Exception { - - } - - @Test - public void testIntersects() throws Exception { - LineRegion line1 = new LineRegion(1, 10); - LineRegion line2 = new LineRegion(11, 20); - assertFalse(line1.intersects(line2)); - - line1.setMaxVal(15); - assertTrue(line1.intersects(line2)); - - line1.setMaxVal(30); - assertTrue(line1.intersects(line2)); - - line1.setMinVal(21); - assertFalse(line1.intersects(line2)); - } -} +/* + * 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 org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertNotSame; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class RegionTest { + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testConstructor() throws Exception { + Region lr = new Region(0d, 0d); + assertEquals(0d, lr.getMin().doubleValue(), 0); + assertEquals(0d, lr.getMax().doubleValue(), 0); + + lr = new Region(1.5d, -2d); + assertEquals(-2f, lr.getMin().floatValue(), 0); + assertEquals(1.5f, lr.getMax().floatValue(), 0); + + lr = new Region(10d, 20d); + assertEquals(10l, lr.getMin().longValue(), 0); + assertEquals(20l, lr.getMax().longValue(), 0); + } + + + @Test + public void testContains() throws Exception { + + } + + @Test + public void testIntersects() throws Exception { + Region line1 = new Region(1, 10); + Region line2 = new Region(11, 20); + assertFalse(line1.intersects(line2)); + + line1.setMax(15); + assertTrue(line1.intersects(line2)); + + //l1end = 30; + line1.setMax(30); + assertTrue(line1.intersects(line2)); + + //l1start = 21; + line1.setMin(21); + assertFalse(line1.intersects(line2)); + } + + @Test + public void testLength() throws Exception { + Region lr = new Region(0, 10); + assertEquals(10d, lr.length().doubleValue(), 0); + + lr = new Region(-5, 5); + assertEquals(10d, lr.length().doubleValue(), 0); + } + + @Test + public void testCenter() throws Exception { + Region r1 = new Region(1, 2); + assertEquals(1.5, r1.center()); + + Region r2 = new Region(-10, 10); + assertEquals(0.0, r2.center()); + + Region r3 = new Region(-2, -1); + assertEquals(-1.5, r3.center()); + } + + @Test + public void testTransform() throws Exception { + Region r1 = new Region(1, 2); + Region r2 = new Region(0, 10); + + assertEquals(5.0, r1.transform(1.5, r2)); + assertEquals(0.0, r1.transform(1, r2)); + assertEquals(10.0, r1.transform(2, r2)); + + Region r3 = new Region(-10, 10); + assertEquals(-10.0, r1.transform(1, r3)); + } + + @Test + public void testTransformWithFlip() throws Exception { + Region r1 = new Region(1, 2); + Region r2 = new Region(0, 10); + + assertEquals(5.0, r1.transform(1.5, r2, true)); + assertEquals(10.0, r1.transform(1, r2, true)); + assertEquals(0.0, r1.transform(2, r2, true)); + + Region r3 = new Region(-10, 10); + assertEquals(10.0, r1.transform(1, r3, true)); + } + + @Test + public void testRatio() throws Exception { + Region r1 = new Region(1, 2); + Region r2 = new Region(0, 100); + assertEquals(0.01, r1.ratio(r2)); + assertEquals(100.0, r2.ratio(r1)); + + r1 = new Region(0f, 21402646f); + r2 = new Region(0, 999); + + //assertTrue(r2.ratio(r1).doubleValue() > 0); + } + + @Test + public void testRegionRegionUnion() throws Exception { + Region r1 = new Region(1, 2); + Region r2 = new Region(0, 100); + + r1.union(r2); + assertEquals(0, r1.getMin().doubleValue(), 0); + assertEquals(100, r1.getMax().doubleValue(), 0); + } + + @Test + public void testRegionPointUnion() throws Exception { + Region r1 = new Region(1, 2); + + r1.union(0); + assertEquals(0, r1.getMin().doubleValue(), 0); + assertEquals(2, r1.getMax().doubleValue(), 0); + + r1.union(100); + assertEquals(0, r1.getMin().doubleValue(), 0); + assertEquals(100, r1.getMax().doubleValue(), 0); + } + +} 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 new file mode 100644 index 00000000..e504582a --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java @@ -0,0 +1,234 @@ +/* + * 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.pie; + +import android.content.res.*; +import android.graphics.*; +import android.view.*; + +import com.androidplot.*; +import com.androidplot.test.*; +import com.androidplot.ui.*; + +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 { + + RectF plotArea = new RectF(0, 0, 100, 100); + + @Mock + LayoutManager layoutManager; + + PieChart pieChart; + + @Mock + TypedArray typedArray; + + @Mock + SeriesRegistry seriesRegistry; + + @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)); + + SegmentFormatter formatterWithoutTextPaint = new SegmentFormatter(Color.GREEN); + formatterWithoutTextPaint.setLabelPaint(null); + + pieRenderer.drawSegment( + new Canvas(), + plotArea, + segment, + formatterWithoutTextPaint, + 100, 100, 100); + + SegmentFormatter formatterWithTextPaint = new SegmentFormatter(Color.GREEN); + + pieRenderer.drawSegment( + canvas, + plotArea, + segment, + formatterWithTextPaint, + 100, 100, 100); + + verify(pieRenderer, times(0)) + .drawSegmentLabel( + any(Canvas.class), + any(PointF.class), + any(Segment.class), + eq(formatterWithoutTextPaint)); + + verify(pieRenderer, times(1)) + .drawSegmentLabel( + eq(canvas), + any(PointF.class), + eq(segment), + eq(formatterWithTextPaint)); + } + + @Test + public void testOnRender() throws Exception { + Segment segment = spy(new Segment("My Segment", 100)); + SegmentFormatter formatter = spy( + new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN)); + PieRenderer pr = formatter.getRendererInstance(pieChart); + PieRenderer renderer = spy(pr); + doReturn(renderer.getClass()).when(formatter).getRendererClass(); + doReturn(renderer).when(formatter).getRendererInstance(any(PieChart.class)); + 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/AndroidplotTest.java b/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java index 16a356d8..4648ccd2 100644 --- a/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java @@ -17,17 +17,34 @@ package com.androidplot.test; import android.content.Context; -import org.junit.BeforeClass; + +import com.androidplot.util.*; + +import org.junit.*; import org.junit.runner.RunWith; +import org.mockito.junit.*; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.*; /** * Base class for all Androidplot tests that make use of Android platform classes. */ -@RunWith(RobolectricTestRunner.class) +@RunWith(MyTestRunner.class) +@Config(manifest=Config.NONE) public abstract class AndroidplotTest { + /** + * Enable mockito without requiring usage of {@link org.mockito.runners.MockitoJUnitRunner}. + * See: http://site.mockito.org/mockito/docs/current/org/mockito/junit/MockitoRule.html + */ + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + { + PixelUtils.init(getContext()); + } + /** * Convience method - to access the application context. * @return diff --git a/androidplot-core/src/test/java/com/androidplot/test/MyTestRunner.java b/androidplot-core/src/test/java/com/androidplot/test/MyTestRunner.java new file mode 100644 index 00000000..f268087a --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/test/MyTestRunner.java @@ -0,0 +1,17 @@ +package com.androidplot.test; + +import org.junit.runners.model.*; +import org.robolectric.*; + +public class MyTestRunner extends RobolectricTestRunner { + + /** + * Constructs a new instance of the test runner. + * + * @throws InitializationError if the test class is malformed + */ + public MyTestRunner(Class testClass) throws InitializationError + { + super(testClass); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java new file mode 100644 index 00000000..1f8f1457 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java @@ -0,0 +1,67 @@ +package com.androidplot.test; + +import android.annotation.*; +import android.view.*; + +import com.androidplot.xy.*; + +import org.robolectric.shadows.*; + +import static org.robolectric.Shadows.shadowOf; + +/** + * 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() * max - min); + } + return series; + } + + public static XYSeries generateXYSeriesWithNulls(String title, int size) { + SimpleXYSeries series = new SimpleXYSeries(title); + for(int i = 0; i < size; i++) { + if(Math.random() > 0.5) { + series.addLast(i, null); + } else { + series.addLast(i, Math.random()); + } + } + return series; + } + + /** + * Generate a two finger {@link MotionEvent}. + * @param finger1x + * @param finger1y + * @param finger2x + * @param finger2y + * @return + */ + @SuppressLint("NewApi") + public static MotionEvent newPointerDownEvent(int finger1x, int finger1y, int finger2x, int finger2y) { + MotionEvent me = MotionEvent.obtain(0, 0, MotionEvent.ACTION_POINTER_DOWN, finger1x, finger1y, 0); + ShadowMotionEvent sme = shadowOf(me); + //sme.setAction(MotionEvent.ACTION_POINTER_DOWN); + sme.setPointerIds(0, 1); + //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/ConfiguratorTest.java b/androidplot-core/src/test/java/com/androidplot/util/ConfiguratorTest.java deleted file mode 100644 index c084d1ee..00000000 --- a/androidplot-core/src/test/java/com/androidplot/util/ConfiguratorTest.java +++ /dev/null @@ -1,91 +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.util; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import java.lang.reflect.Method; -import static junit.framework.Assert.assertEquals; - -@RunWith(RobolectricTestRunner.class) -public class ConfiguratorTest { - - class A { - int d = 0; - - public int getD() { - return d; - } - - public void setD(int d) { - this.d = d; - } - } - - class B { - A a = new A(); - - public A getA() { - return a; - } - - public void setA(A a) { - this.a = a; - } - } - - class C { - B b = new B(); - - public B getB() { - return b; - } - - public void setB(B a) { - this.b = b; - } - } - - @org.junit.Before - public void setUp() throws Exception { - - } - - @org.junit.After - public void tearDown() throws Exception { - - } - - - @Test - public void testGetFieldAt() throws Exception { - C c = new C(); - assertEquals(c, Configurator.getObjectContaining(c, "b")); - assertEquals(c.getB(), Configurator.getObjectContaining(c, "b.a")); - assertEquals(c.getB().getA(), Configurator.getObjectContaining(c, "b.a.d")); - } - - @Test - public void testGetSetter() throws Exception { - C c = new C(); - - Method m = Configurator.getSetter(c.getClass(), "b"); - assertEquals(1, m.getParameterTypes().length); - assertEquals(B.class, m.getParameterTypes()[0]); - } -} 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/InstrumentedXYPlot.java b/androidplot-core/src/test/java/com/androidplot/util/InstrumentedXYPlot.java new file mode 100644 index 00000000..ab1a71b4 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/util/InstrumentedXYPlot.java @@ -0,0 +1,24 @@ +package com.androidplot.util; + +import android.content.*; +import android.graphics.*; + +import com.androidplot.xy.*; + +/** + * Created by halfhp on 10/21/16. + */ +public class InstrumentedXYPlot extends XYPlot { + + // may be set by consumers if necessary + public Canvas canvas = new Canvas(); + + public InstrumentedXYPlot(Context context) { + super(context, "a test plot"); + } + + @Override + public void redraw() { + super.onDraw(canvas); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java b/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java new file mode 100644 index 00000000..3dbd6970 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java @@ -0,0 +1,130 @@ +/* + * 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 org.junit.*; + +import static junit.framework.Assert.assertEquals; + +public class LayerHashTest { + + Object obj1 = new Object(); + Object obj2 = new Object(); + Object obj3 = new Object(); + + LayerHash layerHash; + + @Before + public void setUp() throws Exception { + layerHash = new LayerHash<>(); + layerHash.addToBottom(obj1, obj1); + layerHash.addToBottom(obj2, obj2); + layerHash.addToBottom(obj3, obj3); + + assertEquals(obj1, layerHash.getTop()); + assertEquals(obj3, layerHash.getBottom()); + } + + // "top" == last element in the list: + @Test + public void moveUp() { + layerHash.moveUp(obj3); + assertEquals(obj1, layerHash.getTop()); + assertEquals(obj3, layerHash.getBeneath(obj1)); + assertEquals(obj2, layerHash.getBeneath(obj3)); + + layerHash.moveUp(obj3); + assertEquals(obj3, layerHash.getTop()); + assertEquals(obj1, layerHash.getBeneath(obj3)); + assertEquals(obj2, layerHash.getBeneath(obj1)); + + layerHash.moveUp(obj3); + assertEquals(obj3, layerHash.getTop()); + assertEquals(obj1, layerHash.getBeneath(obj3)); + assertEquals(obj2, layerHash.getBeneath(obj1)); + } + + // "bottom" == first element in the list: + @Test + public void moveDown() { + layerHash.moveDown(obj1); + assertEquals(obj3, layerHash.getKeysAsList().get(0)); + assertEquals(obj1, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + + layerHash.moveDown(obj1); + assertEquals(obj1, layerHash.getKeysAsList().get(0)); + assertEquals(obj3, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + + layerHash.moveDown(obj1); + assertEquals(obj1, layerHash.getKeysAsList().get(0)); + assertEquals(obj3, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + } + + @Test + public void moveAbove() { + layerHash.moveAbove(obj2, obj1); + assertEquals(obj3, layerHash.getKeysAsList().get(0)); + assertEquals(obj1, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + } + + @Test + 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/ListOrganizerTest.java b/androidplot-core/src/test/java/com/androidplot/util/LinkedLayerListOrganizerTest.java similarity index 91% rename from androidplot-core/src/test/java/com/androidplot/util/ListOrganizerTest.java rename to androidplot-core/src/test/java/com/androidplot/util/LinkedLayerListOrganizerTest.java index a7a3d0ad..a554d9d6 100644 --- a/androidplot-core/src/test/java/com/androidplot/util/ListOrganizerTest.java +++ b/androidplot-core/src/test/java/com/androidplot/util/LinkedLayerListOrganizerTest.java @@ -1,95 +1,95 @@ -/* - * 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 org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.util.LinkedList; - -import static junit.framework.Assert.assertEquals; - -public class ListOrganizerTest { - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testMoveToTop() throws Exception { - - } - - @Test - public void testMoveAbove() throws Exception { - - } - - @Test - public void testMoveBeneath() throws Exception { - - } - - @Test - public void testMoveToBottom() throws Exception { - Object obj1 = new Object(); - Object obj2 = new Object(); - Object obj3 = new Object(); - LinkedList list = new LinkedList(); - - list.add(obj1); - list.add(obj2); - list.add(obj3); - - assertEquals(obj1, list.getFirst()); - assertEquals(obj3, list.getLast()); - - ListOrganizer organizer = new ListOrganizer(list); - - organizer.moveToBottom(obj3); - - assertEquals(obj2, list.getLast()); - assertEquals(obj3, list.getFirst()); - - } - - @Test - public void testMoveUp() throws Exception { - - } - - @Test - public void testMoveDown() throws Exception { - - } - - @Test - public void testAddFirst() throws Exception { - - } - - @Test - public void testAddLast() throws Exception { - - } -} +/* + * 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 org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.LinkedList; + +import static junit.framework.Assert.assertEquals; + +public class LinkedLayerListOrganizerTest { + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testMoveToTop() throws Exception { + + } + + @Test + public void testMoveAbove() throws Exception { + + } + + @Test + public void testMoveBeneath() throws Exception { + + } + + @Test + public void testMoveToBottom() throws Exception { + Object obj1 = new Object(); + Object obj2 = new Object(); + Object obj3 = new Object(); + LinkedList list = new LinkedList(); + + list.add(obj1); + list.add(obj2); + list.add(obj3); + + assertEquals(obj1, list.getFirst()); + assertEquals(obj3, list.getLast()); + + LayerListOrganizer organizer = new LayerListOrganizer(list); + + organizer.moveToBottom(obj3); + + assertEquals(obj2, list.getLast()); + assertEquals(obj3, list.getFirst()); + + } + + @Test + public void testMoveUp() throws Exception { + + } + + @Test + public void testMoveDown() throws Exception { + + } + + @Test + public void testAddFirst() throws Exception { + + } + + @Test + public void testAddLast() throws Exception { + + } +} 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 new file mode 100644 index 00000000..df9014cd --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java @@ -0,0 +1,311 @@ +/* + * Copyright 2016 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 com.androidplot.*; +import com.androidplot.xy.*; + +import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import static junit.framework.Assert.assertEquals; +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 { + + // 8 element lists: + final List LINEAR = Arrays.asList(new Number[]{ 1, 2, 3, 4 ,5 , 6, 7, 8}); + final List LINEAR_INVERSE = Arrays.asList(new Number[]{ 8, 7, 6, 5, 4, 3, 2, 1}); + final List ZIG_ZAG = Arrays.asList(new Number[]{1, 10, 1, 10, 1, 10, 1, 10}); + final List NULLS = Arrays.asList(new Number[]{null, 2, null, 4, null, 0, -1, null}); + + // single element lists: + final List SINGLE_VALUE = Arrays.asList(new Number[]{3}); + final List SINGLE_VALUE_NULL = Arrays.asList(new Number[]{null}); + + // empty list: + final List EMPTY = new ArrayList<>(); + + @org.junit.After + public void tearDown() throws Exception { + + } + + @Test + 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); + assertEquals(7, minMax.getMaxX().doubleValue(), 0); + assertEquals(1, minMax.getMinY().doubleValue(), 0); + assertEquals(8, minMax.getMaxY().doubleValue(), 0); + + series = new SimpleXYSeries(LINEAR_INVERSE, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); + minMax = SeriesUtils.minMax(series); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(7, minMax.getMaxX().doubleValue(), 0); + assertEquals(1, minMax.getMinY().doubleValue(), 0); + assertEquals(8, minMax.getMaxY().doubleValue(), 0); + + series = new SimpleXYSeries(ZIG_ZAG, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); + minMax = SeriesUtils.minMax(series); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(7, minMax.getMaxX().doubleValue(), 0); + assertEquals(1, minMax.getMinY().doubleValue(), 0); + assertEquals(10, minMax.getMaxY().doubleValue(), 0); + + series = new SimpleXYSeries(NULLS, NULLS, null); + minMax = SeriesUtils.minMax(series); + assertEquals(-1, minMax.getMinX().doubleValue(), 0); + assertEquals(4, minMax.getMaxX().doubleValue(), 0); + assertEquals(-1, minMax.getMinY().doubleValue(), 0); + assertEquals(4, minMax.getMaxY().doubleValue(), 0); + + series = new SimpleXYSeries(SINGLE_VALUE, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); + minMax = SeriesUtils.minMax(series); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(0, minMax.getMaxX().doubleValue(), 0); + assertEquals(3, minMax.getMinY().doubleValue(), 0); + assertEquals(3, minMax.getMaxY().doubleValue(), 0); + + series = new SimpleXYSeries(SINGLE_VALUE_NULL, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); + minMax = SeriesUtils.minMax(series); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(0, minMax.getMaxX().doubleValue(), 0); + assertEquals(null, minMax.getMinY()); + assertEquals(null, minMax.getMaxY()); + + series = new SimpleXYSeries(EMPTY, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); + minMax = SeriesUtils.minMax(series); + assertEquals(null, minMax.getMinX()); + assertEquals(null, minMax.getMaxX()); + assertEquals(null, minMax.getMinY()); + assertEquals(null, minMax.getMaxY()); + } + + @Test + 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); + + minMax = SeriesUtils.minMax(LINEAR_INVERSE); + assertEquals(1, minMax.getMin().doubleValue(), 0); + assertEquals(8, minMax.getMax().doubleValue(), 0); + + minMax = SeriesUtils.minMax(ZIG_ZAG); + assertEquals(1, minMax.getMin().doubleValue(), 0); + assertEquals(10, minMax.getMax().doubleValue(), 0); + + minMax = SeriesUtils.minMax(NULLS); + assertEquals(-1, minMax.getMin().doubleValue(), 0); + assertEquals(4, minMax.getMax().doubleValue(), 0); + + minMax = SeriesUtils.minMax(SINGLE_VALUE); + assertEquals(3, minMax.getMin().doubleValue(), 0); + assertEquals(3, minMax.getMax().doubleValue(), 0); + + minMax = SeriesUtils.minMax(SINGLE_VALUE_NULL); + assertEquals(null, minMax.getMin()); + assertEquals(null, minMax.getMax()); + + minMax = SeriesUtils.minMax(EMPTY); + assertEquals(null, minMax.getMin()); + assertEquals(null, minMax.getMax()); + } + + @Test + public void getNullRegion_producesExpectedResult() { + XYSeries s1 = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "s1", + 0, 0, // 0 + 1, 1, // 1 + 2, 2, // 2 + null, null, // 3 + null, null, // 4 + 5, 5, // 5 + 6, 6); // 6 + + try { + Region r1 = SeriesUtils.getNullRegion(s1, 0); + fail("IllegalArgumentException expected."); + } catch(IllegalArgumentException e) { + // expected + } + + try { + Region r2 = SeriesUtils.getNullRegion(s1, s1.size() - 1); + fail("IllegalArgumentException expected."); + } catch(IllegalArgumentException e) { + // expected + } + + Region r3 = SeriesUtils.getNullRegion(s1, 3); + assertEquals(2, r3.getMin().intValue()); + assertEquals(5, r3.getMax().intValue()); + + Region r4 = SeriesUtils.getNullRegion(s1, 4); + assertEquals(2, r4.getMin().intValue()); + assertEquals(5, r4.getMax().intValue()); + } + + @Test + 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), + "s1"); + + assertEquals(0, SeriesUtils.iBoundsMin(s1, 0, 1)); + assertEquals(6, SeriesUtils.iBoundsMin(s1, 6, 1)); + assertEquals(12, SeriesUtils.iBoundsMin(s1, 12, 1)); + + // now test with null vals: + XYSeries s2 = new SimpleXYSeries( + Arrays.asList(null, 1, 2, null, null, 5, 6, 7, 8, null, 10, 11, null), + Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), + "s2"); + + assertEquals(0, SeriesUtils.iBoundsMin(s2, 0, 1)); + assertEquals(2, SeriesUtils.iBoundsMin(s2, 3, 1)); + assertEquals(11, SeriesUtils.iBoundsMin(s2, 12, 1)); + + // test with a higher step value: + assertEquals(0, SeriesUtils.iBoundsMin(s2, 0, 5)); + assertEquals(2, SeriesUtils.iBoundsMin(s2, 3, 5)); + assertEquals(11, SeriesUtils.iBoundsMin(s2, 12, 5)); + } + + @Test + 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), + "s1"); + + assertEquals(12, SeriesUtils.iBoundsMax(s1, 12, 1)); + assertEquals(6, SeriesUtils.iBoundsMax(s1, 6, 1)); + assertEquals(12, SeriesUtils.iBoundsMax(s1, 12, 1)); + + // now test with null vals: + XYSeries s2 = new SimpleXYSeries( + Arrays.asList(null, 1, 2, null, null, 5, 6, 7, 8, null, 10, 11, null), + Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), + "s2"); + + assertEquals(1, SeriesUtils.iBoundsMax(s2, 0, 1)); + assertEquals(5, SeriesUtils.iBoundsMax(s2, 3, 1)); + assertEquals(12, SeriesUtils.iBoundsMax(s2, 12, 1)); + + // test with a higher step value: + assertEquals(1, SeriesUtils.iBoundsMax(s2, 0, 5)); + assertEquals(5, SeriesUtils.iBoundsMax(s2, 3, 5)); + assertEquals(12, SeriesUtils.iBoundsMax(s2, 12, 5)); + } + + @Test + public void iBounds_findsMinMax() { + FastXYSeries series = mock(FastXYSeries.class); + when(series.size()).thenReturn(3); + when(series.getX(0)).thenReturn(0); + when(series.getX(1)).thenReturn(1); + when(series.getX(2)).thenReturn(2); + + Region result = SeriesUtils.iBounds(series, new RectRegion(0, 1, 0, 1)); + assertEquals(0, result.getMin().intValue()); + assertEquals(1, result.getMax().intValue()); + + // test with nulls: + when(series.size()).thenReturn(6); + when(series.getX(0)).thenReturn(0); + when(series.getX(1)).thenReturn(0.5); + when(series.getX(2)).thenReturn(null); + when(series.getX(3)).thenReturn(null); + when(series.getX(4)).thenReturn(1); + when(series.getX(5)).thenReturn(3); + + result = SeriesUtils.iBounds(series, new RectRegion(0, 1, 0, 1)); + assertEquals(0, result.getMin().intValue()); + assertEquals(4, result.getMax().intValue()); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/util/ValPixConverterTest.java b/androidplot-core/src/test/java/com/androidplot/util/ValPixConverterTest.java deleted file mode 100644 index ecfe6955..00000000 --- a/androidplot-core/src/test/java/com/androidplot/util/ValPixConverterTest.java +++ /dev/null @@ -1,125 +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.util; - -import org.junit.Test; - -import static junit.framework.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class ValPixConverterTest { - @org.junit.Before - public void setUp() throws Exception { - - } - - @org.junit.After - public void tearDown() throws Exception { - - } - - @org.junit.Test - public void testValToPix() throws Exception { - int sizeInPix = 100; - int min = 0; - int max = 100; - - int value = 50; - - assertEquals(50.0f, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be closer to the top: - // (remember that 0,0 is the top left pixel) - value = 75; - assertEquals(25.0f, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be at the very top: - value = 100; - assertEquals(0.0f, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be at the very top: - value = 0; - assertEquals(100.0f, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be smack in the middle: - min = -100; - value = 0; - assertEquals(50.0f, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be at the very bottom: - value = 100; - assertEquals(0.0f, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be in the middle: - value = 0; - assertEquals(50.0f, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - min = -100; - max = 100; - sizeInPix = 200; - assertEquals(0f, ValPixConverter.valToPix(-100, min, max, sizeInPix, false)); - assertEquals(200f, ValPixConverter.valToPix(100, min, max, sizeInPix, false)); - } - - @org.junit.Test - public void testpixToVal() throws Exception { - int sizeInPix = 100; - int min = 0; - int max = 100; - - double value = 50; - - float pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - value = 75; - pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - - min = -100; - value = 50; - pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - min = -100; - value = 60; - pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - min = 20; - value = 50; - pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - try { - ValPixConverter.pixToVal(-5, 0, 0, 0, true); - fail("IllegalArgumentException expected."); - } catch(IllegalArgumentException ex) {} - } - - @Test - public void testValPerPix() { - assertEquals(1.0, ValPixConverter.valPerPix(0, 100, 100)); - double expected = 200d/100; - assertEquals(expected, ValPixConverter.valPerPix(100, 300, 100)); - expected = 50d/100; - assertEquals(expected, ValPixConverter.valPerPix(0, 50, 100)); - expected = 200d/100; - assertEquals(expected, ValPixConverter.valPerPix(-100, 100, 100)); - } -} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java new file mode 100644 index 00000000..b1600828 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java @@ -0,0 +1,64 @@ +/* + * 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.*; + +import com.androidplot.test.*; +import com.androidplot.ui.*; + +import org.junit.*; +import org.mockito.*; + +import static org.mockito.Mockito.*; + +/** + * Tests {@link AdvancedLineAndPointRenderer} and some of + * {@link com.androidplot.xy.AdvancedLineAndPointRenderer.Formatter}. + */ +public class AdvanceLineAndPointRendererTest extends AndroidplotTest { + + XYPlot xyPlot; + + Canvas canvas; + + RectF plotArea = new RectF(0, 0, 100, 100); + + @Mock + RenderStack renderStack; + + @Before + public void setUp() throws Exception { + canvas = new Canvas(); + xyPlot = spy(new XYPlot(getContext(), "My Plot")); + } + + @Test + public void testOnRender() throws Exception { + AdvancedLineAndPointRenderer.Formatter formatter = spy(new AdvancedLineAndPointRenderer.Formatter()); + AdvancedLineAndPointRenderer renderer = formatter.getRendererInstance(xyPlot); + + doReturn(renderer.getClass()).when(formatter).getRendererClass(); + doReturn(renderer).when(formatter).getRendererInstance(any(XYPlot.class)); + + XYSeries s = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Series1", 1, 2, 3, 4); + + xyPlot.addSeries(s, formatter); + + renderer.onRender(canvas, plotArea, s, formatter, renderStack); + } +} 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 new file mode 100644 index 00000000..94a0be50 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java @@ -0,0 +1,85 @@ +/* + * 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.*; + +import com.androidplot.test.*; +import com.androidplot.ui.*; + +import org.junit.*; +import org.mockito.*; + +import static org.mockito.Mockito.*; + +/** + * Tests {@link BubbleRenderer} and some of {@link BubbleFormatter}. + */ +public class BubbleRendererTest extends AndroidplotTest { + + XYPlot xyPlot; + + Canvas canvas; + + RectF plotArea = new RectF(0, 0, 100, 100); + + @Mock + RenderStack renderStack; + + @Before + public void setUp() throws Exception { + canvas = spy(new Canvas()); + xyPlot = spy(new XYPlot(getContext(), "My Plot")); + } + + @Test + public void testOnRender() throws Exception { + BubbleFormatter formatter = spy(new BubbleFormatter()); + BubbleRenderer br = formatter.getRendererInstance(xyPlot); + BubbleRenderer renderer = spy(br); + + doReturn(renderer.getClass()).when(formatter).getRendererClass(); + doReturn(renderer).when(formatter).getRendererInstance(any(XYPlot.class)); + + BubbleSeries bs = new BubbleSeries(1, 2, 3, 4, 5, 6, 7, 8, 9); + + xyPlot.addSeries(bs, formatter); + + renderer.onRender(canvas, plotArea, bs, formatter, renderStack); + } + + @Test + public void testOnRender_withPointLabeler() throws Exception { + BubbleFormatter formatter = spy(new BubbleFormatter()); + PointLabelFormatter plf = new PointLabelFormatter(Color.MAGENTA); + formatter.setPointLabelFormatter(plf); + BubbleRenderer br = formatter.getRendererInstance(xyPlot); + BubbleRenderer renderer = spy(br); + + doReturn(renderer.getClass()).when(formatter).getRendererClass(); + doReturn(renderer).when(formatter).getRendererInstance(any(XYPlot.class)); + + BubbleSeries bs = new BubbleSeries(2, 2, 22); + + xyPlot.addSeries(bs, formatter); + + renderer.onRender(canvas, plotArea, bs, formatter, renderStack); + + // verify the z-val is the one labeled: + verify(canvas).drawText(eq("22"), anyFloat(), anyFloat(), eq(plf.getTextPaint())); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java new file mode 100644 index 00000000..8eda23b9 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java @@ -0,0 +1,61 @@ +/* + * 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.*; + +import com.androidplot.test.*; +import com.androidplot.ui.*; + +import org.junit.*; +import org.mockito.*; + +import static org.mockito.Mockito.*; + +public class CandlestickRendererTest extends AndroidplotTest { + + XYPlot xyPlot; + + Canvas canvas; + + RectF plotArea = new RectF(0, 0, 100, 100); + + @Mock + RenderStack renderStack; + + @Before + public void setUp() throws Exception { + canvas = new Canvas(); + xyPlot = spy(new XYPlot(getContext(), "My Plot")); + } + + @Test + public void testOnRender() throws Exception { + CandlestickFormatter formatter = spy(new CandlestickFormatter()); + CandlestickRenderer renderer = spy((CandlestickRenderer) formatter.doGetRendererInstance(xyPlot)); + doReturn(renderer.getClass()).when(formatter).getRendererClass(); + doReturn(renderer).when(formatter).doGetRendererInstance(any(XYPlot.class)); + + XYSeries openVals = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "open", 1, 2, 3, 4); + XYSeries closeVals = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "open", 1, 2, 3, 4); + XYSeries highVals = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "open", 1, 2, 3, 4); + XYSeries lowVals = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "open", 1, 2, 3, 4); + CandlestickMaker.make(xyPlot, formatter, openVals, closeVals, highVals, lowVals); + + renderer.onRender(canvas, plotArea, openVals, formatter, renderStack); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/CandlestickSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/CandlestickSeriesTest.java new file mode 100644 index 00000000..919d3b58 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/CandlestickSeriesTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 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 org.junit.Test; + +import java.util.Arrays; + +import static junit.framework.Assert.assertEquals; + +public class CandlestickSeriesTest { + + @Test + public void testConstructors() throws Exception { + 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)); + + XYSeries lowSeries = candlestickSeries.getLowSeries(); + assertEquals(1d, lowSeries.getY(0)); + assertEquals(4d, lowSeries.getY(1)); + assertEquals(3d, lowSeries.getY(2)); + + XYSeries highSeries = candlestickSeries.getHighSeries(); + assertEquals(10d, highSeries.getY(0)); + assertEquals(18d, highSeries.getY(1)); + assertEquals(11d, highSeries.getY(2)); + + XYSeries openSeries = candlestickSeries.getOpenSeries(); + assertEquals(2d, openSeries.getY(0)); + assertEquals(6d, openSeries.getY(1)); + assertEquals(5d, openSeries.getY(2)); + + XYSeries closeSeries = candlestickSeries.getCloseSeries(); + assertEquals(9d, closeSeries.getY(0)); + assertEquals(5d, closeSeries.getY(1)); + assertEquals(10d, closeSeries.getY(2)); + + CandlestickMaker.check(candlestickSeries); + + } +} 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 new file mode 100644 index 00000000..cf083d9e --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java @@ -0,0 +1,105 @@ +/* + * 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.*; + +import com.androidplot.test.*; + +import org.junit.*; +import org.junit.runner.*; +import org.mockito.*; + +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; + +public class FastLineAndPointRendererTest extends AndroidplotTest { + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testOnRender() throws Exception { + RectF gridRect = new RectF(5, 5, 105, 105); + XYPlot plot = new XYPlot(getContext(), "Test"); + + FastLineAndPointRenderer.Formatter formatter = + new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null); + + + // create a series composed of 3 "segments"; series portions separated by null values: + XYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, null, 3, 4, 5, 6, 7, null, null, 8); + + FastLineAndPointRenderer renderer = Mockito.spy(new FastLineAndPointRenderer(plot)); + Canvas canvas = mock(Canvas.class); + + renderer.onRender(canvas, gridRect, series, formatter, null); + + // first segment + verify(renderer, times(1)).drawSegment( + eq(canvas), + any(float[].class), + eq(0), + eq(4), + eq(formatter)); + + verify(canvas, times(1)).drawPoints( + any(float[].class), + eq(0), + eq(4), + eq(formatter.getVertexPaint())); + + // second segment + verify(renderer, times(1)).drawSegment( + eq(canvas), + any(float[].class), + eq(6), + eq(10), + eq(formatter)); + + verify(canvas, times(1)).drawPoints( + any(float[].class), + eq(6), + eq(10), + eq(formatter.getVertexPaint())); + + // third segment + verify(renderer, times(1)).drawSegment( + eq(canvas), + any(float[].class), + eq(20), + eq(2), + eq(formatter)); + + verify(canvas, times(1)).drawPoints( + any(float[].class), + eq(20), + eq(2), + eq(formatter.getVertexPaint())); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java new file mode 100644 index 00000000..800992f9 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java @@ -0,0 +1,67 @@ +/* + * 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.*; + +import com.androidplot.test.*; +import com.androidplot.ui.*; + +import org.junit.*; +import org.mockito.*; + +import java.util.*; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.*; + +public class LTTBSamplerTest extends AndroidplotTest { + + @Test + public void testSomething() throws Exception { + + } + +// @Test +// public void testDownsample() throws Exception { +// +// Number[][] rawNumbers = new Number[][] { {1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}, {11, 12}, {13, 14}, {15, 16}, {17, 18}, {19, 20} }; +// XYSeries rawSeries = new SimpleXYSeries( +// Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15, 17, 19), +// Arrays.asList(2, 4, 6, 8, 10, 12, 14, 16, 18, 20), "raw"); +// compareSeriesToRaw(rawNumbers, rawSeries); +// +// LTTBDownsampler downsampler = new LTTBDownsampler(); +// SimpleXYSeries sampled = new SimpleXYSeries( +// Arrays.asList(new Number[]{0, 0, 0, 0, 0}), +// Arrays.asList(new Number[]{0, 0, 0, 0, 0}), "sampled"); +// downsampler.downsample(rawSeries, sampled); +// +// Number[][] downsampled = LTTBDownsampler.downsample(rawNumbers, 5); +// compareSeriesToRaw(downsampled, sampled); +// } +// +// protected void compareSeriesToRaw(Number[][] raw, XYSeries series) { +// assertEquals(raw.length, series.size()); +// int i = 0; +// for(Number[] xy : raw) { +// assertEquals(xy[0], series.getX(i)); +// assertEquals(xy[1], series.getY(i)); +// i++; +// } +// } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java new file mode 100644 index 00000000..44a7b22d --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java @@ -0,0 +1,261 @@ +/* + * 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.*; + +import com.androidplot.test.*; + +import org.junit.*; +import org.mockito.*; + +import java.util.*; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.*; + +public class LineAndPointRendererTest extends AndroidplotTest { + + private XYPlot xyPlot; + + @Mock + Canvas canvas; + + RectF plotArea = new RectF(0, 0, 100, 100); + + @Before + public void setUp() throws Exception { + xyPlot = spy(new XYPlot(getContext(), "XYPlot")); + } + + @Test + public void testDrawSeries_withInterpolation() throws Exception { + LineAndPointRenderer renderer = spy(new LineAndPointRenderer(xyPlot)); + LineAndPointFormatter formatter = new LineAndPointFormatter(); + formatter.setInterpolationParams( + new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal)); + XYSeries series = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Series1", 1, 2, 3); + renderer.drawSeries(canvas, plotArea, series, formatter); + } + + /** + * Sanity run to make sure that at the end of the day, points are being drawn at the expected + * screen-coords. + * @throws Exception + */ + @Test + public void testDrawSeries() throws Exception { + + // 100x100 plot space: + 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: + 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(xyPlot)); + + xyPlot.addSeries(series, formatter); + + xyPlot.calculateMinMaxVals(); + renderer.drawSeries(canvas, plotArea, series, formatter); + + PointF[] expectedPoints = new PointF[] { + new PointF(0, 99), + new PointF(11, 88), + new PointF(22, 77), + new PointF(33, 66), + new PointF(44, 55), + new PointF(55, 44), + new PointF(66, 33), + new PointF(77, 22), + new PointF(88, 11), + new PointF(99, 0) + }; + ArgumentCaptor capturedPoints= ArgumentCaptor.forClass(List.class); + + verify(renderer, times(1)).renderPoints( + eq(canvas), + eq(plotArea), + eq(series), + eq(0), + eq(expectedPoints.length), + capturedPoints.capture(), + eq(formatter)); + + List pList = capturedPoints.getValue(); + + // {0, 1} + assertEquals(expectedPoints[0].x, pList.get(0).x); + assertEquals(expectedPoints[0].y, pList.get(0).y); + + // {1, 2} + assertEquals(expectedPoints[1].x, pList.get(1).x); + assertEquals(expectedPoints[1].y, pList.get(1).y); + + // {2, 3} + assertEquals(expectedPoints[2].x, pList.get(2).x); + assertEquals(expectedPoints[2].y, pList.get(2).y); + + // {3, 4} + assertEquals(expectedPoints[3].x, pList.get(3).x); + assertEquals(expectedPoints[3].y, pList.get(3).y); + + // {4, 5} + assertEquals(expectedPoints[4].x, pList.get(4).x); + assertEquals(expectedPoints[4].y, pList.get(4).y); + + // {5, 6} + assertEquals(expectedPoints[5].x, pList.get(5).x); + assertEquals(expectedPoints[5].y, pList.get(5).y); + + // {6, 7} + assertEquals(expectedPoints[6].x, pList.get(6).x); + assertEquals(expectedPoints[6].y, pList.get(6).y); + + // {7, 8} + assertEquals(expectedPoints[7].x, pList.get(7).x); + assertEquals(expectedPoints[7].y, pList.get(7).y); + + // {8, 9} + assertEquals(expectedPoints[8].x, pList.get(8).x); + assertEquals(expectedPoints[8].y, pList.get(8).y); + + // {9, 10} + 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 new file mode 100644 index 00000000..50d3c16c --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java @@ -0,0 +1,252 @@ +/* + * 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.content.res.*; +import android.graphics.*; +import android.view.*; + +import com.androidplot.Region; +import com.androidplot.test.*; +import com.androidplot.ui.*; +import com.androidplot.util.*; + +import org.junit.*; +import org.mockito.*; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.*; + +public class PanZoomTest extends AndroidplotTest { + + @Mock + LayoutManager layoutManager; + + @Mock + XYPlot xyPlot; + + @Mock + TypedArray typedArray; + + @Mock + XYSeriesRegistry seriesRegistry; + + RectRegion bounds = new RectRegion(0, 100, 0, 100); + + @Before + public void setUp() throws Exception { + when(xyPlot.getRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getBounds()).thenReturn(bounds); + when(xyPlot.getInnerLimits()).thenReturn(new RectRegion()); + when(xyPlot.getOuterLimits()).thenReturn(new RectRegion()); + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testOnTouch_notifiesOnTouchListener() throws Exception { + PanZoom panZoom = new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE); + + View.OnTouchListener listener = mock(View.OnTouchListener.class); + panZoom.setDelegate(listener); + + MotionEvent motionEvent = mock(MotionEvent.class); + panZoom.onTouch(xyPlot, motionEvent); + + verify(listener, times(1)).onTouch(xyPlot, motionEvent); + } + + @Test + public void testOnTouch_oneFingerMovePansButDoesNotZoom() throws Exception { + PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE)); + + View.OnTouchListener listener = mock(View.OnTouchListener.class); + panZoom.setDelegate(listener); + + MotionEvent moveEvent = mock(MotionEvent.class); + + doNothing().when(panZoom).calculatePan( + any(PointF.class), any(Region.class), anyBoolean()); + + when(moveEvent.getAction()) + .thenReturn(MotionEvent.ACTION_DOWN) + .thenReturn(MotionEvent.ACTION_MOVE) + .thenReturn(MotionEvent.ACTION_UP); + + panZoom.onTouch(xyPlot, moveEvent); // fires ACTION_DOWN + panZoom.onTouch(xyPlot, moveEvent); // fires ACTION_MOVE + panZoom.onTouch(xyPlot, moveEvent); // fires ACTION_UP + + verify(panZoom).pan(moveEvent); + verify(panZoom, never()).zoom(moveEvent); + verify(panZoom).reset(); + } + + @Test + public void testOnTouch_twoFingersZoom() throws Exception { + PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE)); + MotionEvent moveEvent = mock(MotionEvent.class); + + // simulate a zoom gesture sequence: + when(moveEvent.getAction()) + .thenReturn(MotionEvent.ACTION_DOWN) + .thenReturn(MotionEvent.ACTION_POINTER_DOWN) + .thenReturn(MotionEvent.ACTION_MOVE) + .thenReturn(MotionEvent.ACTION_UP); + + when(panZoom.fingerDistance(moveEvent)) + .thenReturn(new RectF(0, 0, 10, 10)) + .thenReturn(new RectF(0, 0, 11, 11)) + .thenReturn(new RectF(0, 0, 12, 12)) + .thenReturn(new RectF(0, 0, 13, 13)); + + panZoom.onTouch(xyPlot, moveEvent); // ACTION_DOWN + panZoom.onTouch(xyPlot, moveEvent); // ACTION_POINTER_DOWN + panZoom.onTouch(xyPlot, moveEvent); // ACTION_MOVE + panZoom.onTouch(xyPlot, moveEvent); // ACTION_UP + + verify(xyPlot).redraw(); + verify(panZoom, never()).pan(any(MotionEvent.class)); + verify(panZoom).zoom(moveEvent); + verify(panZoom).reset(); + } + + @Test + public void testZoom() { + xyPlot = spy(new InstrumentedXYPlot(getContext())); + xyPlot.setDomainBoundaries(0, 100, BoundaryMode.FIXED); + xyPlot.setRangeBoundaries(0, 100, BoundaryMode.FIXED); + xyPlot.redraw(); + + PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE)); + + // cap our pan/zoom boundaries: + xyPlot.getOuterLimits().set(0, 100, 0, 100); + + panZoom.setFingersRect(new RectF(0, 0, 20, 20)); + + InOrder inOrder = inOrder(xyPlot); + inOrder.verify(xyPlot).setDomainBoundaries(0, 100, BoundaryMode.FIXED); + + // should result in a 2x zoom on domain centerpoint: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 40, 40)); + inOrder.verify(xyPlot).setDomainBoundaries(25f, 75f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(25f, 75f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // should result in another 2x zoom on domain centerpoint: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 80, 80)); + inOrder.verify(xyPlot).setDomainBoundaries(37.5f, 62.5f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(37.5f, 62.5f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // should zoom out and take us back to the original bounds: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 20, 20)); + inOrder.verify(xyPlot).setDomainBoundaries(0f, 100f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(0f, 100f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // zooming out past capped bounds should not result in any change: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 1, 1)); + inOrder.verify(xyPlot).setDomainBoundaries(0f, 100f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(0f, 100f, BoundaryMode.FIXED); + // TODO: if nothing changed, then why bother redrawing?? + 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 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)); + RectF distance = panZoom.fingerDistance(TestUtils.newPointerDownEvent(0, 0, 10, 10)); + assertEquals(0f, distance.left); + assertEquals(0f, distance.top); + assertEquals(10f, distance.right); + assertEquals(10f, distance.bottom); + + // no matter what order the coords are supplied, make sure the same rect is calculated: + distance = panZoom.fingerDistance(10, 10, 0, 0); + assertEquals(0f, distance.left); + assertEquals(0f, distance.top); + assertEquals(10f, distance.right); + assertEquals(10f, distance.bottom); + } +} 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 d22762fa..c6e11ae7 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java @@ -16,11 +16,14 @@ package com.androidplot.xy; +import android.graphics.*; + import com.androidplot.test.AndroidplotTest; import org.junit.After; import org.junit.Before; import org.junit.Test; +import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -81,4 +84,115 @@ public void testIsWithin() throws Exception { assertTrue(region1.intersects(region5)); assertTrue(region5.intersects(region1)); } + + @Test + public void testTransform() throws Exception { + RectRegion r1 = new RectRegion(1, 2, 0, 10); + RectRegion r2 = new RectRegion(2, 4, 10, 20); + + final XYCoords trans1 = r1.transform(new XYCoords(1.5, 5), r2); + assertEquals(3.0, trans1.x); + assertEquals(15.0, trans1.y); + + // try a screen transform: + RectF screen = new RectF(0, 0, 100, 100); + PointF result = r1.transform(2, 10, screen, false, true); + assertEquals(100f, result.x); + assertEquals(0f, result.y); + + // now transform back to real: + RectRegion screenRegion = new RectRegion(screen); + XYCoords result2 = screenRegion.transform(result.x, result.y, r1, false, true); + assertEquals(2d, result2.x); + assertEquals(10d, result2.y); + } + + @Test + public void testIntersects() throws Exception { + RectRegion r1 = new RectRegion(0, 10, 0, 10); + RectRegion r2 = new RectRegion(5, 15, 5, 15); + RectRegion r3 = new RectRegion(11, 21, 11, 21); + + assertTrue(r1.intersects(r2)); + assertTrue(r2.intersects(r1)); + assertFalse(r1.intersects(r3)); + assertFalse(r3.intersects(r1)); + assertTrue(r2.intersects(r3)); + assertTrue(r3.intersects(r2)); + } + + @Test + public void testClip_sameDimensions() throws Exception { + RectRegion r1 = new RectRegion(0, 10, 0, 10); + RectRegion r2 = new RectRegion(0, 10, 0, 10); + + r1.intersect(r2); + assertEquals(0, r1.getMinX().doubleValue(), 0); + assertEquals(10, r1.getMaxX().doubleValue(), 0); + assertEquals(0, r1.getMinY().doubleValue(), 0); + assertEquals(10, r1.getMaxY().doubleValue(), 0); + } + + @Test + public void testClip_intersectingDimensions() throws Exception { + RectRegion r1 = new RectRegion(0, 10, 0, 10); + RectRegion r2 = new RectRegion(5, 15, 5, 15); + + r1.intersect(r2); + assertEquals(5, r1.getMinX().doubleValue(), 0); + assertEquals(10, r1.getMaxX().doubleValue(), 0); + assertEquals(5, r1.getMinY().doubleValue(), 0); + assertEquals(10, r1.getMaxY().doubleValue(), 0); + + r1 = new RectRegion(0, 10, 0, 10); + r2 = new RectRegion(-5, 5, -5, 5); + r1.intersect(r2); + + assertEquals(0, r1.getMinX().doubleValue(), 0); + assertEquals(5, r1.getMaxX().doubleValue(), 0); + assertEquals(0, r1.getMinY().doubleValue(), 0); + assertEquals(5, r1.getMaxY().doubleValue(), 0); + } + + @Test + public void testClip_nonIntersectingDimensions() throws Exception { + RectRegion r1 = new RectRegion(0, 10, 0, 10); + RectRegion r2 = new RectRegion(100, 200, 100, 200); + r1.intersect(r2); + + assertEquals(null, r1.getMinX()); + assertEquals(null, r1.getMaxX()); + assertEquals(null, r1.getMinY()); + assertEquals(null, r1.getMaxY()); + + r1 = new RectRegion(0, 10, 0, 10); + r2 = new RectRegion(-200, -100, -200, -100); + r1.intersect(r2); + + assertEquals(null, r1.getMinX()); + assertEquals(null, r1.getMaxX()); + assertEquals(null, r1.getMinY()); + assertEquals(null, r1.getMaxY()); + } + + @Test + public void testUnion() throws Exception { + RectRegion r1 = new RectRegion(0, 10, 0, 10); + RectRegion r2 = new RectRegion(100, 200, 100, 200); + + r1.union(r2); + + assertEquals(0, r1.getMinX().doubleValue(), 0); + assertEquals(200, r1.getMaxX().doubleValue(), 0); + assertEquals(0, r1.getMinY().doubleValue(), 0); + assertEquals(200, r1.getMaxY().doubleValue(), 0); + + r1 = new RectRegion(0, 10, 0, 10); + r2.union(r1); + + assertEquals(0, r2.getMinX().doubleValue(), 0); + assertEquals(200, r2.getMaxX().doubleValue(), 0); + assertEquals(0, r2.getMinY().doubleValue(), 0); + assertEquals(200, r2.getMaxY().doubleValue(), 0); + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java new file mode 100644 index 00000000..95cea342 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java @@ -0,0 +1,92 @@ +package com.androidplot.xy; + +import com.androidplot.test.*; + +import org.junit.*; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests {@link SampledXYSeries} + */ +public class SampledXYSeriesTest extends AndroidplotTest { + + @Test + public void testInit() throws Exception { + + XYSeries rawData = TestUtils.generateXYSeries("my series", 2000); + SampledXYSeries s1 = spy(new SampledXYSeries(rawData, 2, 100)); + + // expect 5 setZoomFactor levels (besides 1x): 2x, 4x, 8x, 16x, 32x + assertEquals(4, s1.getZoomLevels().size()); + assertEquals(1000, s1.getZoomLevels().get(0).size()); // 2x + assertEquals(500, s1.getZoomLevels().get(1).size()); // 4x + assertEquals(250, s1.getZoomLevels().get(2).size()); // 8x + assertEquals(125, s1.getZoomLevels().get(3).size()); // 16x + + SampledXYSeries s2 = spy(new SampledXYSeries(rawData, 4, 100)); + + // expect 2 setZoomFactor levels (besides 1x): 4x & 16x: + assertEquals(2, s2.getZoomLevels().size()); + assertEquals(500, s2.getZoomLevels().get(0).size()); // 4x + assertEquals(125, s2.getZoomLevels().get(1).size()); // 16x + } + + @Test + public void testGetZoomIndex() throws Exception { + assertEquals(0, SampledXYSeries.getZoomIndex(2, 2)); + assertEquals(1, SampledXYSeries.getZoomIndex(3, 2)); + assertEquals(1, SampledXYSeries.getZoomIndex(4, 2)); + assertEquals(2, SampledXYSeries.getZoomIndex(8, 2)); + assertEquals(2, SampledXYSeries.getZoomIndex(9, 2)); + assertEquals(2, SampledXYSeries.getZoomIndex(10, 2)); + assertEquals(3, SampledXYSeries.getZoomIndex(15, 2)); + assertEquals(3, SampledXYSeries.getZoomIndex(16, 2)); + assertEquals(3, SampledXYSeries.getZoomIndex(17, 2)); + assertEquals(4, SampledXYSeries.getZoomIndex(31, 2)); + assertEquals(4, SampledXYSeries.getZoomIndex(32, 2)); + + assertEquals(0, SampledXYSeries.getZoomIndex(1, 4)); + assertEquals(0, SampledXYSeries.getZoomIndex(4, 4)); + assertEquals(1, SampledXYSeries.getZoomIndex(15, 4)); + assertEquals(1, SampledXYSeries.getZoomIndex(16, 4)); + assertEquals(1, SampledXYSeries.getZoomIndex(17, 4)); + assertEquals(2, SampledXYSeries.getZoomIndex(63, 4)); + assertEquals(2, SampledXYSeries.getZoomIndex(64, 4)); + assertEquals(2, SampledXYSeries.getZoomIndex(65, 4)); + } + + @Test + public void testSetZoomFactor() throws Exception { + XYSeries rawData = TestUtils.generateXYSeries("my series", 2000); + SampledXYSeries sampledXYSeries = spy(new SampledXYSeries(rawData, 2, 100)); + sampledXYSeries.setZoomFactor(2); + assertEquals(1000, sampledXYSeries.size()); + sampledXYSeries.setZoomFactor(4); + assertEquals(500, sampledXYSeries.size()); + sampledXYSeries.setZoomFactor(8); + assertEquals(250, sampledXYSeries.size()); + sampledXYSeries.setZoomFactor(16); + assertEquals(125, sampledXYSeries.size()); + } + + @Test + public void testResample() { + XYSeries rawData = TestUtils.generateXYSeries("my series", 10000); + SampledXYSeries sampledXYSeries = new SampledXYSeries(rawData, 2, 200); + assertEquals(5, sampledXYSeries.getZoomLevels().size()); + } + + /** + * Ignored until null support is added to {@link LTTBSampler}. + */ + @Ignore + @Test + public void testResample_supportsNullVals() { + XYSeries rawData = TestUtils.generateXYSeriesWithNulls("my series", 10000); + SampledXYSeries sampledXYSeries = new SampledXYSeries(rawData, 2, 200); + assertEquals(5, sampledXYSeries.getZoomLevels().size()); + } +} 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 4128034d..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"); @@ -127,4 +128,78 @@ public void testSet() throws Exception { assertEquals(size, series.size()); } + @Test + public void resize_emptySeries_resizesSeries() { + SimpleXYSeries series = new SimpleXYSeries("series"); + series.resize(10); + assertEquals(10, series.size()); + + series.resize(20); + assertEquals(20, series.size()); + + series.resize(1); + assertEquals(1, series.size()); + + series.resize(0); + assertEquals(0, series.size()); + + series.resize(0); + 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/XYStepCalculatorTest.java b/androidplot-core/src/test/java/com/androidplot/xy/StepCalculatorTest.java similarity index 53% rename from androidplot-core/src/test/java/com/androidplot/xy/XYStepCalculatorTest.java rename to androidplot-core/src/test/java/com/androidplot/xy/StepCalculatorTest.java index 130b0a26..91cc0f9c 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYStepCalculatorTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/StepCalculatorTest.java @@ -1,67 +1,77 @@ -/* - * 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 org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static junit.framework.Assert.assertEquals; - -public class XYStepCalculatorTest { - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testGetStep() throws Exception { - - } - - @Test - public void testSubdivide() throws Exception { - int numSegments = 10; - float plotSize = 100; - double minVal = 0; - double maxVal = 100; - XYStep step = XYStepCalculator.getStep(XYStepMode.SUBDIVIDE, plotSize, numSegments, minVal, maxVal); - - assertEquals(plotSize/(numSegments-1), step.getStepPix()); - - // make sure large values dont break anything: - minVal = 1000000000; - maxVal = 2000000000; - step = XYStepCalculator.getStep(XYStepMode.SUBDIVIDE, plotSize, numSegments, minVal, maxVal); - assertEquals(plotSize/(numSegments-1), step.getStepPix()); - } - - @Test - public void testIncrementByVal() throws Exception { - - } - - @Test - public void testIncrementByPixels() throws Exception { - - } -} +/* + * 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 com.androidplot.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +public class StepCalculatorTest { + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testGetStep() throws Exception { + + } + + @Test + public void testSubdivide() throws Exception { + int numSegments = 10; + + Region pixBounds = new Region(0, 100); + Region realBounds = new Region(0, 100); + Step step = XYStepCalculator.getStep(StepMode.SUBDIVIDE,numSegments, realBounds, pixBounds); + + assertEquals(pixBounds.length().doubleValue()/(numSegments-1), step.getStepPix()); + + // make sure large values dont break anything: + realBounds.setMin(1000000000); + realBounds.setMax(2000000000); + step = XYStepCalculator.getStep(StepMode.SUBDIVIDE, + numSegments, realBounds, pixBounds); + + assertEquals(pixBounds.length().doubleValue()/(numSegments-1), step.getStepPix()); + } + + @Test + public void testIncrementByVal() throws Exception { + Region pixBounds = new Region(50, 150); + Region realBounds = new Region(100, 200); + Step step = XYStepCalculator.getStep(StepMode.INCREMENT_BY_VAL, 1, realBounds, pixBounds); + assertEquals(1.0, step.getStepPix()); + } + + @Test + public void testIncrementByPixels() throws Exception { + Region pixBounds = new Region(50, 150); + Region realBounds = new Region(100, 200); + Step step = XYStepCalculator.getStep(StepMode.INCREMENT_BY_PIXELS, 1, realBounds, pixBounds); + assertEquals(1.0, step.getStepPix()); + } +} 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 new file mode 100644 index 00000000..a9182501 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java @@ -0,0 +1,329 @@ +/* + * 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.content.res.*; +import android.graphics.*; + +import com.androidplot.test.*; +import com.androidplot.ui.*; + +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.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; +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 XYGraphWidgetTest extends AndroidplotTest { + + @Mock + LayoutManager layoutManager; + + XYPlot xyPlot; + + @Mock + TypedArray typedArray; + + @Mock + XYSeriesRegistry seriesRegistry; + + RectRegion bounds = new RectRegion(0, 100, 0, 100); + + @Mock + Canvas canvas; + + // this is a spy + Size size; + + // this is a spy + XYGraphWidget graphWidget; + + + @Before + public void setUp() { + size = spy(new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE)); + xyPlot = spy(new XYPlot(getContext(), "XYPlot")); + when(xyPlot.getRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getBounds()).thenReturn(bounds); + when(xyPlot.getDomainOrigin()).thenReturn(0); + when(xyPlot.getRangeOrigin()).thenReturn(0); + + xyPlot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1); + xyPlot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1); + + graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); + graphWidget.setGridRect(new RectF(0, 0, 10, 100)); + graphWidget.setLabelRect(new RectF(0, 0, 100, 100)); + } + + @Test + public void processAttrs_withDefaults_disablesGridClipping() { + XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); + + graphWidget.processAttrs(typedArray); + verify(graphWidget, times(1)).setGridClippingEnabled(false); + } + + @Test + 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); + + 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); + io.verify(graphWidget).drawData(canvas); + } + + @Test + 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); + + 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); + io.verify(graphWidget).drawGrid(canvas); + } + + @Test + public void drawMarkers_drawsLineForEachMarker() { + xyPlot.addMarker(new XValueMarker(1, "x")); + xyPlot.addMarker(new YValueMarker(-1, "y")); + + graphWidget.drawMarkers(canvas); + + // TODO: verifications + verify(canvas, times(2)) + .drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), any(Paint.class)); + } + + @Test + public void drawGrid_nullOrigin_drawsGrid() { + when(xyPlot.getDomainOrigin()).thenReturn(null); + when(xyPlot.getRangeOrigin()).thenReturn(null); + runDrawGridTest(); + } + + @Test + public void drawGrid_zeroOrigin_drawsGrid() { + when(xyPlot.getDomainOrigin()).thenReturn(0); + when(xyPlot.getRangeOrigin()).thenReturn(0); + runDrawGridTest(); + } + + @Test + public void drawGrid_centeredOrigin_drawsGrid() { + + // set origin to midpoint so we exercise + // code to draw lines on both sides of the origin: + when(xyPlot.getDomainOrigin()).thenReturn(50); + when(xyPlot.getRangeOrigin()).thenReturn(50); + runDrawGridTest(); + } + + @Test + 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: + verify(canvas, times(1)).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), eq(domainCursorPaint)); + verify(canvas, times(1)).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), eq(rangeCursorPaint)); + } + + @Test + public void testDrawCursors_noCursorPaintOrPosition_drawsNoCursorLines() { + graphWidget.setDomainCursorPaint(null); + graphWidget.setRangeCursorPaint(null); + + graphWidget.drawCursors(canvas); + + // no lines should be drawn onto the canvas: + verify(canvas, times(0)).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), any(Paint.class)); + } + + @Test + 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"); + when(graphWidget.getCursorLabelFormatter()).thenReturn(clf); + graphWidget.drawCursorLabel(canvas); + verify(canvas, times(1)).drawText(eq("bla"), anyFloat(), anyFloat(), any(Paint.class)); + } + + @Test + public void setLineLabelEdges_setsEdges() { + graphWidget.setLineLabelEdges(XYGraphWidget.Edge.LEFT, XYGraphWidget.Edge.BOTTOM); + assertTrue(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.LEFT)); + assertTrue(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.BOTTOM)); + + graphWidget.setLineLabelEdges(XYGraphWidget.Edge.NONE); + assertFalse(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.TOP)); + assertFalse(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.BOTTOM)); + assertFalse(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.LEFT)); + assertFalse(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.RIGHT)); + } + + @Test + public void setLineLabelEdges_bitfield_setsEdges() { + + graphWidget.setLineLabelEdges( + XYGraphWidget.Edge.TOP.getValue() | XYGraphWidget.Edge.RIGHT.getValue()); + + assertTrue(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.TOP)); + assertTrue(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.RIGHT)); + 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 e59205c7..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,47 +17,100 @@ package com.androidplot.xy; import android.graphics.*; -import com.androidplot.Plot; import com.androidplot.test.AndroidplotTest; -import mockit.*; -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 { - @After - public void tearDown() throws Exception {} + @Mock LayoutManager layoutManager; + @Mock XYPlot xyPlot; + @Mock Canvas canvas; + @Mock XYRegionFormatter xyRegionFormatter; + LineAndPointRenderer lineAndPointRenderer; - @Test - public void testDoOnDraw() throws Exception { - XYPlot plot = new XYPlot(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; - SimpleXYSeries s1 = new SimpleXYSeries((Arrays.asList(1, 2, 3)), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1"); + XYLegendWidget legendWidget; - plot.addSeries(s1, new LineAndPointFormatter( - Color.RED, Color.GREEN, Color.BLUE, null)); + @Before + public void before() { + seriesRegistry = new XYSeriesRegistry(); + legendWidget = spy(new XYLegendWidget(layoutManager, xyPlot, widgetSize, + new DynamicTableModel(4, 4), iconSize)); - assertEquals(1, plot.getSeriesRegistry().size()); + lineAndPointRenderer = new LineAndPointRenderer(xyPlot); + + when(xyPlot.getRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getRendererList()).thenReturn(Lists.newArrayList(lineAndPointRenderer)); + when(xyPlot.getRenderer(any(Class.class))).thenReturn(lineAndPointRenderer); + } + + @Test + public void draw_drawsLegendIcons_forEnabledItemsOnly() throws Exception { + final XYSeries s1 = mock(XYSeries.class); + final XYSeriesFormatter f1 = new LineAndPointFormatter(); + f1.setLegendIconEnabled(true); - Deencapsulation.invoke(plot, "onSizeChanged", 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. - Deencapsulation.invoke(plot, "onDraw", new Canvas()); + final XYSeries s2 = mock(XYSeries.class); + final XYSeriesFormatter f2 = new LineAndPointFormatter(); + f2.setLegendIconEnabled(false); - plot.removeSeries(s1); - assertEquals(0, plot.getSeriesRegistry().size()); - plot.addSeries(s1, new BarFormatter(Color.RED, Color.GREEN)); - plot.redraw(); + 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)); - // throws NullPointerException before fix - // for ANDROIDPLOT-166 was applied. - Deencapsulation.invoke(plot, "onDraw", new Canvas()); + 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)); } + @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"); + + final List legendItems = Lists.newArrayList(i1, i2, i3); + doReturn(legendItems).when(legendWidget).getLegendItems(); + + legendWidget.draw(canvas); + + InOrder inOrder = Mockito.inOrder(legendWidget); + + 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 45de14c8..2e8c6094 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java @@ -19,7 +19,8 @@ import android.graphics.*; import com.androidplot.Plot; import com.androidplot.test.AndroidplotTest; -import com.androidplot.util.Configurator; +import com.halfhp.fig.*; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -30,6 +31,7 @@ import java.util.List; import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.mock; public class XYPlotTest extends AndroidplotTest { @@ -37,7 +39,7 @@ public class XYPlotTest extends AndroidplotTest { List numList1; List numList2; - SimpleXYSeries series1; + SimpleXYSeries series0To100; @Before public void setUp() throws Exception { @@ -45,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 @@ -55,384 +57,405 @@ 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(); - assertEquals(3.0, plot.getCalculatedMinX()); - assertEquals(7.0, plot.getCalculatedMaxX()); + assertEquals(3.0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(7.0, plot.getBounds().getMaxX().doubleValue(), 0); } @Test public void testOriginAutoMode() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.centerOnDomainOrigin(5); plot.calculateMinMaxVals(); - assertEquals(10.0, plot.getCalculatedMaxX()); // symmetry is @ 10, not 9 - assertEquals(0.0, plot.getCalculatedMinX()); + assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0); // symmetry is @ 10, not 9 + assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0); plot.centerOnRangeOrigin(50); plot.calculateMinMaxVals(); - assertEquals(100.0, plot.getCalculatedMaxY()); - assertEquals(0.0, plot.getCalculatedMinY()); + assertEquals(100.0, plot.getBounds().getMaxY().doubleValue(), 0); + assertEquals(0.0, plot.getBounds().getMinY().doubleValue(), 0); } @Test public void testOriginGrowMode() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.centerOnDomainOrigin(5, null, BoundaryMode.GROW); plot.calculateMinMaxVals(); - assertEquals(0.0, plot.getCalculatedMinX()); - assertEquals(10.0, plot.getCalculatedMaxX()); + assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0); + 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.getCalculatedMinX()); - assertEquals(11.0, plot.getCalculatedMaxX()); + 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.getCalculatedMinX()); - assertEquals(11.0, plot.getCalculatedMaxX()); + assertEquals(-1.0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11.0, plot.getBounds().getMaxX().doubleValue(), 0); } @Test public void testOriginShrinkMode() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); - plot.centerOnDomainOrigin(5, null, BoundaryMode.SHRINNK); + plot.addSeries(series0To100, new LineAndPointFormatter()); + plot.centerOnDomainOrigin(5, null, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); - assertEquals(0.0, plot.getCalculatedMinX()); - assertEquals(10.0, plot.getCalculatedMaxX()); + assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0); + 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.getCalculatedMinX()); - assertEquals(10.0, plot.getCalculatedMaxX()); + 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 check them - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + // default to auto so run them + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); plot.setDomainBoundaries(2, BoundaryMode.FIXED, 8, BoundaryMode.FIXED); plot.calculateMinMaxVals(); // fixed - assertEquals(2, plot.getCalculatedMinX()); - assertEquals(8, plot.getCalculatedMaxX()); + assertEquals(2, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(8, plot.getBounds().getMaxX().doubleValue(), 0); // back to auto plot.setDomainBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); - // check again - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + // run again + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); // we are not testing MinY well with this dataset. // try grow plot.setDomainBoundaries(2, BoundaryMode.GROW, 8, BoundaryMode.GROW); plot.calculateMinMaxVals(); - // check inital - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + // run inital + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + 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 - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + 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. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + 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.SHRINNK, 8, BoundaryMode.SHRINNK); + plot.setDomainBoundaries(2, BoundaryMode.SHRINK, 8, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); - // check inital - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + // run inital + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + 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 - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + 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. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); // back to auto plot.setDomainBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); // should of changed. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); } @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 check them - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + // default to auto so run them + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); plot.setRangeBoundaries(5, BoundaryMode.FIXED, 80, BoundaryMode.FIXED); plot.calculateMinMaxVals(); // fixed - assertEquals(5, plot.getCalculatedMinY()); - assertEquals(80, plot.getCalculatedMaxY()); + assertEquals(5, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(80, plot.getBounds().getMaxY().doubleValue(), 0); // back to auto plot.setRangeBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); - // check again - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + // run again + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); // try grow plot.setRangeBoundaries(2, BoundaryMode.GROW, 8, BoundaryMode.GROW); plot.calculateMinMaxVals(); - // check inital - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + // run inital + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + 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 - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + 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. - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + 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.SHRINNK, 8, BoundaryMode.SHRINNK); + plot.setRangeBoundaries(2, BoundaryMode.SHRINK, 8, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); - // check inital - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + // run inital + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + 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 - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + 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. - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); // back to auto plot.setRangeBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); // should of changed. - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); } @Test public void testSetDomainRightMinMax() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.calculateMinMaxVals(); - // default to auto so check them - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); - - plot.setDomainRightMax(10); + // default to auto so run them + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); + + plot.getOuterLimits().setMaxX(10); plot.calculateMinMaxVals(); // same values. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + 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.getCalculatedMinX()); - assertEquals(10, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(10, plot.getBounds().getMaxX().doubleValue(), 0); - plot.setDomainRightMax(null); + plot.getOuterLimits().setMaxX(null); plot.calculateMinMaxVals(); // back to full - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // now the RightMin - plot.setDomainRightMin(10); + plot.getInnerLimits().setMaxX(10); plot.calculateMinMaxVals(); // still to full - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + 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 - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(10, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(10, plot.getBounds().getMaxX().doubleValue(), 0); // now off again - plot.setDomainRightMin(null); + plot.getInnerLimits().setMaxX(null); plot.calculateMinMaxVals(); // small values. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); } @Test public void testSetRangeTopBottomMinMax() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.calculateMinMaxVals(); - // default to auto so check them - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); - - plot.setRangeTopMax(110); - plot.setRangeBottomMin(-50); + // default to auto so run them + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); + + plot.getOuterLimits().setMaxY(110); + plot.getOuterLimits().setMinY(-50); plot.calculateMinMaxVals(); // same values. - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + 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 - assertEquals(-50, plot.getCalculatedMinY()); - assertEquals(110, plot.getCalculatedMaxY()); + assertEquals(-50, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(110, plot.getBounds().getMaxY().doubleValue(), 0); - plot.setRangeTopMax(null); - plot.setRangeBottomMin(null); + plot.getOuterLimits().setMaxY(null); + plot.getOuterLimits().setMinY(null); plot.calculateMinMaxVals(); // back to full - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // now the Min - plot.setRangeTopMin(150); - plot.setRangeBottomMax(-60); + plot.getInnerLimits().setMaxY(150); + plot.getInnerLimits().setMinY(-60); plot.calculateMinMaxVals(); // still to full - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + 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 - assertEquals(-60, plot.getCalculatedMinY()); - assertEquals(150, plot.getCalculatedMaxY()); + assertEquals(-60, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(150, plot.getBounds().getMaxY().doubleValue(), 0); // now off again - plot.setRangeTopMin(null); - plot.setRangeBottomMax(null); + plot.getInnerLimits().setMaxY(null); + plot.getInnerLimits().setMinY(null); plot.calculateMinMaxVals(); // small values. - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); } @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 @@ -441,15 +464,90 @@ public void testConfigure() throws Exception { String param1 = "this is a test."; String param2 = Plot.RenderMode.USE_BACKGROUND_THREAD.toString(); String param3 = "#FF0000"; - params.put("title", param1); + params.put("title.text", param1); params.put("renderMode", param2); params.put("backgroundPaint.color", param3); - params.put("graphWidget.domainTickLabelPaint.color", param3); - Configurator.configure(RuntimeEnvironment.application, plot, params); - assertEquals(param1, plot.getTitle()); + Fig.configure(RuntimeEnvironment.application, plot, params); + assertEquals(param1, plot.getTitle().getText()); assertEquals(Plot.RenderMode.USE_BACKGROUND_THREAD, plot.getRenderMode()); assertEquals(Color.parseColor(param3), plot.getBackgroundPaint().getColor()); - assertEquals(Color.parseColor(param3), plot.getGraphWidget().getDomainTickLabelPaint().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/androidplot-core/src/test/java/com/androidplot/xy/XYSeriesRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYSeriesRendererTest.java index 5ee9f736..484dde39 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYSeriesRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYSeriesRendererTest.java @@ -16,9 +16,10 @@ package com.androidplot.xy; -import android.graphics.*; +import android.graphics.RectF; + +import com.androidplot.*; import com.androidplot.test.AndroidplotTest; -import com.androidplot.util.ValPixConverter; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -42,27 +43,26 @@ public void tearDown() throws Exception { public void testDataToGridCorrelation() throws Exception { RectF gridRect = new RectF(5, 5, 105, 105); XYPlot plot = new XYPlot(RuntimeEnvironment.application, "Test"); - plot.setDomainStepMode(XYStepMode.SUBDIVIDE); + plot.setDomainStepMode(StepMode.SUBDIVIDE); plot.setDomainStepValue(10); plot.setDomainBoundaries(0, 100, BoundaryMode.FIXED); plot.setRangeBoundaries(0, 100, BoundaryMode.FIXED); plot.calculateMinMaxVals(); - XYStep domainStep = XYStepCalculator.getStep(plot, XYAxisType.DOMAIN, gridRect, plot.getCalculatedMinX().doubleValue(), plot.getCalculatedMaxX().doubleValue()); + Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect); - int x = 0; - float val = ValPixConverter.valToPix(x, 0, 9, gridRect.width(), false); + Region xBounds = new Region(0, 9); - assertEquals(val, domainStep.getStepPix()*x); + int x = 0; + double val = xBounds.transform(x, gridRect.left, gridRect.right, false); + assertEquals(val, (domainStep.getStepPix()*x) + gridRect.left); x = 1; - val = ValPixConverter.valToPix(x, 0, 9, gridRect.width(), false); - - assertEquals(val, domainStep.getStepPix()*x); + val = xBounds.transform(x, gridRect.left, gridRect.right, false); + assertEquals(val, (domainStep.getStepPix()*x) + gridRect.left); x = 9; - val = ValPixConverter.valToPix(x, 0, 9, gridRect.width(), false); - - assertEquals(val, domainStep.getStepPix()*x); + val = xBounds.transform(x, gridRect.left, gridRect.right, false); + assertEquals(val, (domainStep.getStepPix()*x) + gridRect.left); } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/ZoomEstimatorTest.java b/androidplot-core/src/test/java/com/androidplot/xy/ZoomEstimatorTest.java new file mode 100644 index 00000000..e33ff402 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/ZoomEstimatorTest.java @@ -0,0 +1,65 @@ +package com.androidplot.xy; + +import com.androidplot.test.*; + +import org.junit.*; +import org.mockito.*; + +import static junit.framework.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Created by halfhp on 10/8/16. + */ +public class ZoomEstimatorTest extends AndroidplotTest { + + @Mock + XYPlot xyPlot; + + + @Test + public void testCheck() throws Exception { + ZoomEstimator estimator = spy(new ZoomEstimator()); + + SampledXYSeries series = + spy(new SampledXYSeries(TestUtils + .generateXYSeries("test series", 1000), 2, 100)); + series.resample(); + assertEquals(8d, series.getMaxZoomFactor()); + + XYSeriesBundle bundle = new XYSeriesBundle(series, null); + + when(xyPlot.getBounds()) + .thenReturn(new RectRegion(0, 1000, 0, 1000)) + .thenReturn(new RectRegion(0, 500, 0, 500)) + .thenReturn(new RectRegion(0, 1, 0, 1)); + + estimator.run(xyPlot, bundle); + estimator.run(xyPlot, bundle); + estimator.run(xyPlot, bundle); + + verify(series).setZoomFactor(8); + verify(series).setZoomFactor(4); + verify(series).setZoomFactor(1); + } + + @Test + public void testCalculateZoom() { + ZoomEstimator estimator = spy(new ZoomEstimator()); + + SampledXYSeries series = + spy(new SampledXYSeries(TestUtils + .generateXYSeries("test series", 1000), 2, 100)); + series.resample(); + + when(xyPlot.getBounds()) + .thenReturn(new RectRegion(0, 1000, 0, 1000)); + + // fully zoomed out so max zoom factor should be applied: + assertEquals(series.getMaxZoomFactor(), estimator.calculateZoom(series, new RectRegion(0, 1000, 0, 1000))); + + // fully zoomed in so min zoom factor should be applied: + assertEquals(1d, estimator.calculateZoom(series, new RectRegion(0, 1, 0, 1))); + + } +} diff --git a/build.gradle b/build.gradle index 9faf86bf..4b5e18de 100644 --- a/build.gradle +++ b/build.gradle @@ -1,63 +1,45 @@ -apply plugin: 'java' - -repositories { - mavenCentral() - jcenter() -} +/* + * 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. + */ ext { - theBuildToolsVersion = '21.1.2' - theCompileSdkVersion = 23 - theTargetSdkVersion = 23 - theMinSdkVersion = 4 - theVersionName = '0.9.6' - theVersionCode = 12 + theCompileSdkVersion = 34 + theTargetSdkVersion = 34 + theMinSdkVersion = 5 + 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:1.2.3' - classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.+' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' + classpath 'com.android.tools.build:gradle:8.5.2' + classpath 'com.mxalbert.gradle:jacoco-android:0.2.0' } } allprojects { repositories { mavenCentral() + google() } } - -/* - * 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. - */ - -/*subprojects { -// for now can't do this one here because it has to -// be loaded *before* any other plugin in each submodule -// and unless its listed at the top of the submodule plugin list, -// that doesnt seem to happen. - apply plugin: 'android-sdk-manager' -}*/ - -task wrapper(type: Wrapper) { - gradleVersion = '2.9' -} \ No newline at end of file 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 8eed462d..2c2fbd52 100644 --- a/demoapp/build.gradle +++ b/demoapp/build.gradle @@ -14,30 +14,89 @@ * limitations under the License. */ -apply plugin: 'android-sdk-manager' -apply plugin: 'android' +buildscript { + ext.kotlin_version = '2.0.0' + repositories { + mavenCentral() + } + dependencies { + 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 { - compile project(':androidplot-core') - compile 'com.crittercism:crittercism-android-agent:5.4.0' + 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" } - /** - * TODO: uncomment this and address all the lint issues. - */ - lintOptions { + signingConfigs { + release { + storeFile file(System.getenv("KEYSTORE") ?: "keystore.jks") + storePassword System.getenv("KEYSTORE_PASSWORD") + keyAlias System.getenv("KEY_ALIAS") + keyPassword System.getenv("KEY_PASSWORD") + } + } + + buildTypes { + release { + zipAlignEnabled true + signingConfig signingConfigs.release + } + + debug { + debuggable true + } + } + lint { abortOnError false } + namespace 'com.androidplot.demos' + +} + +play { + // see: https://github.com/Triple-T/gradle-play-publisher + 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 8ea82e53..5b73a2b7 100644 --- a/demoapp/src/main/AndroidManifest.xml +++ b/demoapp/src/main/AndroidManifest.xml @@ -15,89 +15,117 @@ ~ limitations under the License. --> - + - - - + 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:screenOrientation="landscape" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> - + + - + + + + + 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 43f5d5ba..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; @@ -38,17 +39,16 @@ import android.widget.SeekBar; import android.widget.Spinner; -import com.androidplot.LineRegion; +import com.androidplot.Region; import com.androidplot.ui.*; import com.androidplot.ui.widget.TextLabelWidget; -import com.androidplot.util.PixelUtils; +import com.androidplot.util.*; import com.androidplot.xy.*; /** - * The simplest possible example of using AndroidPlot to plot some data. + * An example of a Bar Plot. */ -public class BarPlotExampleActivity extends Activity -{ +public class BarPlotExampleActivity extends Activity { private static final String NO_SELECTION_TXT = "Touch bar to select."; private XYPlot plot; @@ -57,9 +57,10 @@ public class BarPlotExampleActivity extends Activity private CheckBox series2CheckBox; private Spinner spRenderStyle, spWidthStyle, spSeriesSize; private SeekBar sbFixedWidth, sbVariableWidth; - + private XYSeries series1; private XYSeries series2; + private enum SeriesSize { TEN, TWENTY, @@ -86,9 +87,9 @@ private enum SeriesSize { private Pair selection; + @SuppressLint("ClickableViewAccessibility") @Override - public void onCreate(Bundle savedInstanceState) - { + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.bar_plot_example); @@ -96,15 +97,19 @@ public void onCreate(Bundle savedInstanceState) // initialize our XYPlot reference: plot = (XYPlot) findViewById(R.id.plot); - formatter1 = new MyBarFormatter(Color.argb(200, 100, 150, 100), Color.LTGRAY); - formatter2 = new MyBarFormatter(Color.argb(200, 100, 100, 150), Color.LTGRAY); + 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, new Size( - PixelUtils.dpToPix(100), SizeLayoutType.ABSOLUTE, - PixelUtils.dpToPix(100), SizeLayoutType.ABSOLUTE), - TextOrientationType.HORIZONTAL); + PixelUtils.dpToPix(100), SizeMode.ABSOLUTE, + PixelUtils.dpToPix(100), SizeMode.ABSOLUTE), + TextOrientation.HORIZONTAL); selectionWidget.getLabelPaint().setTextSize(PixelUtils.dpToPix(16)); @@ -114,19 +119,16 @@ public void onCreate(Bundle savedInstanceState) selectionWidget.setBackgroundPaint(p); selectionWidget.position( - 0, XLayoutStyle.RELATIVE_TO_CENTER, - PixelUtils.dpToPix(45), YLayoutStyle.ABSOLUTE_FROM_TOP, - AnchorPosition.TOP_MIDDLE); + 0, HorizontalPositioning.RELATIVE_TO_CENTER, + PixelUtils.dpToPix(45), VerticalPositioning.ABSOLUTE_FROM_TOP, + Anchor.TOP_MIDDLE); selectionWidget.pack(); - // reduce the number of range labels - plot.setTicksPerRangeLabel(3); + plot.setLinesPerRangeLabel(3); plot.setRangeLowerBoundary(0, BoundaryMode.FIXED); - plot.getGraphWidget().getGridBox().setPadding(30, 10, 30, 0); - - plot.setTicksPerDomainLabel(2); + plot.setLinesPerDomainLabel(2); // setup checkbox listers: series1CheckBox = (CheckBox) findViewById(R.id.s1CheckBox); @@ -140,200 +142,248 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { series2CheckBox = (CheckBox) findViewById(R.id.s2CheckBox); series2CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) {onS2CheckBoxClicked(b); + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + onS2CheckBoxClicked(b); } }); plot.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { - if(motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { onPlotClicked(new PointF(motionEvent.getX(), motionEvent.getY())); } return true; } }); - + spRenderStyle = (Spinner) findViewById(R.id.spRenderStyle); - ArrayAdapter adapter = new ArrayAdapter (this, android.R.layout.simple_spinner_item, BarRenderer.BarRenderStyle.values() ); + 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.BarRenderStyle.OVERLAID.ordinal()); + spRenderStyle.setSelection(BarRenderer.BarOrientation.OVERLAID.ordinal()); spRenderStyle.setOnItemSelectedListener(new OnItemSelectedListener() { - public void onItemSelected(AdapterView arg0, View arg1,int arg2, long arg3) { + public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { updatePlot(); } - @Override - public void onNothingSelected(AdapterView arg0) { - } - }); - + + @Override + public void onNothingSelected(AdapterView arg0) { + } + }); + spWidthStyle = (Spinner) findViewById(R.id.spWidthStyle); - ArrayAdapter adapter1 = new ArrayAdapter (this, android.R.layout.simple_spinner_item, BarRenderer.BarWidthStyle.values() ); + 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.BarWidthStyle.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.BarWidthStyle.FIXED_WIDTH.equals(spWidthStyle.getSelectedItem())) { - sbFixedWidth.setVisibility(View.VISIBLE); - sbVariableWidth.setVisibility(View.INVISIBLE); - } else { - sbFixedWidth.setVisibility(View.INVISIBLE); - sbVariableWidth.setVisibility(View.VISIBLE); - } + public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { + if (BarRenderer.BarGroupWidthMode.FIXED_WIDTH.equals(spWidthStyle.getSelectedItem())) { + sbFixedWidth.setVisibility(View.VISIBLE); + sbVariableWidth.setVisibility(View.INVISIBLE); + } else { + sbFixedWidth.setVisibility(View.INVISIBLE); + sbVariableWidth.setVisibility(View.VISIBLE); + } updatePlot(); } - @Override - public void onNothingSelected(AdapterView arg0) { - } - }); + + @Override + public void onNothingSelected(AdapterView arg0) { + } + }); spSeriesSize = (Spinner) findViewById(R.id.spSeriesSize); - ArrayAdapter adapter11 = new ArrayAdapter (this, android.R.layout.simple_spinner_item, SeriesSize.values() ); + ArrayAdapter adapter11 = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, SeriesSize.values()); adapter11.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spSeriesSize.setAdapter(adapter11); spSeriesSize.setSelection(SeriesSize.TEN.ordinal()); spSeriesSize.setOnItemSelectedListener(new OnItemSelectedListener() { - public void onItemSelected(AdapterView arg0, View arg1,int arg2, long arg3) { - switch ((SeriesSize)arg0.getSelectedItem()) { - case TEN: - series1Numbers = series1Numbers10; - series2Numbers = series2Numbers10; - break; - case TWENTY: - series1Numbers = series1Numbers20; - series2Numbers = series2Numbers20; - break; - case SIXTY: - series1Numbers = series1Numbers60; - series2Numbers = series2Numbers60; - break; - default: - break; + public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { + final SeriesSize selectedSize = (SeriesSize) arg0.getSelectedItem(); + switch (selectedSize) { + case TEN: + series1Numbers = series1Numbers10; + series2Numbers = series2Numbers10; + break; + case TWENTY: + series1Numbers = series1Numbers20; + series2Numbers = series2Numbers20; + break; + case SIXTY: + series1Numbers = series1Numbers60; + series2Numbers = series2Numbers60; + break; + default: + break; } - updatePlot(); + updatePlot(selectedSize); } - @Override - public void onNothingSelected(AdapterView arg0) { - } - }); - - + + @Override + public void onNothingSelected(AdapterView arg0) { + } + }); + sbFixedWidth = (SeekBar) findViewById(R.id.sbFixed); sbFixedWidth.setProgress(50); sbFixedWidth.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override - public void onProgressChanged(SeekBar seekBar, int i, boolean b) {updatePlot();} + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + updatePlot(); + } + @Override - public void onStartTrackingTouch(SeekBar seekBar) {} + public void onStartTrackingTouch(SeekBar seekBar) { + } @Override - public void onStopTrackingTouch(SeekBar seekBar) {} + public void onStopTrackingTouch(SeekBar seekBar) { + } }); - - + sbVariableWidth = (SeekBar) findViewById(R.id.sbVariable); sbVariableWidth.setProgress(1); sbVariableWidth.setVisibility(View.INVISIBLE); sbVariableWidth.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override - public void onProgressChanged(SeekBar seekBar, int i, boolean b) {updatePlot();} - @Override - public void onStartTrackingTouch(SeekBar seekBar) {} - @Override - public void onStopTrackingTouch(SeekBar seekBar) {} - }); - - plot.setDomainValueFormat(new NumberFormat() { - @Override - public StringBuffer format(double value, StringBuffer buffer, FieldPosition field) { - int year = (int) (value + 0.5d) / 12; - int month = (int) ((value + 0.5d) % 12); - return new StringBuffer(DateFormatSymbols.getInstance().getShortMonths()[month] + " '0" + year); + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + updatePlot(); } @Override - public StringBuffer format(long value, StringBuffer buffer, FieldPosition field) { - throw new UnsupportedOperationException("Not yet implemented."); + public void onStartTrackingTouch(SeekBar seekBar) { } @Override - public Number parse(String string, ParsePosition position) { - throw new UnsupportedOperationException("Not yet implemented."); + public void onStopTrackingTouch(SeekBar seekBar) { } }); - updatePlot(); + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM). + setFormat(new NumberFormat() { + @Override + public StringBuffer format(double value, StringBuffer buffer, + FieldPosition field) { + int year = (int) (value + 0.5d) / 12; + int month = (int) ((value + 0.5d) % 12); + return new StringBuffer(DateFormatSymbols.getInstance() + .getShortMonths()[month] + " '0" + year); + } + + @Override + public StringBuffer format(long value, StringBuffer buffer, + FieldPosition field) { + throw new UnsupportedOperationException("Not yet implemented."); + } + + @Override + public Number parse(String string, ParsePosition position) { + throw new UnsupportedOperationException("Not yet implemented."); + } + }); + updatePlot(); } private void updatePlot() { - - // Remove all current series from each plot + updatePlot(null); + } + + private void updatePlot(SeriesSize seriesSize) { + + // Remove all current series from each plot plot.clear(); // Setup our Series with the selected number of elements - series1 = new SimpleXYSeries(Arrays.asList(series1Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Us"); - series2 = new SimpleXYSeries(Arrays.asList(series2Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Them"); + series1 = new SimpleXYSeries(Arrays.asList(series1Numbers), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Us"); + series2 = new SimpleXYSeries(Arrays.asList(series2Numbers), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Them"); + + plot.setDomainBoundaries(-1, series1.size(), BoundaryMode.FIXED); + plot.setRangeUpperBoundary( + SeriesUtils.minMax(series1, series2). + getMaxY().doubleValue() + 1, BoundaryMode.FIXED); + + if(seriesSize != null) { + switch(seriesSize) { + case TEN: + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 2); + break; + case TWENTY: + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 4); + break; + case SIXTY: + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 6); + break; + } + } // add a new series' to the xyplot: if (series1CheckBox.isChecked()) plot.addSeries(series1, formatter1); - if (series2CheckBox.isChecked()) plot.addSeries(series2, formatter2); + if (series2CheckBox.isChecked()) plot.addSeries(series2, formatter2); // Setup the BarRenderer with our selected options - MyBarRenderer renderer = ((MyBarRenderer)plot.getRenderer(MyBarRenderer.class)); - renderer.setBarRenderStyle((BarRenderer.BarRenderStyle)spRenderStyle.getSelectedItem()); - renderer.setBarWidthStyle((BarRenderer.BarWidthStyle)spWidthStyle.getSelectedItem()); - renderer.setBarWidth(sbFixedWidth.getProgress()); - renderer.setBarGap(sbVariableWidth.getProgress()); - - if (BarRenderer.BarRenderStyle.STACKED.equals(spRenderStyle.getSelectedItem())) { - plot.setRangeTopMin(15); + 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.setRangeTopMin(0); + plot.getInnerLimits().setMaxY(0); } - + plot.redraw(); - - } - + + } + private void onPlotClicked(PointF point) { // make sure the point lies within the graph area. we use gridrect // because it accounts for margins and padding as well. - if (plot.getGraphWidget().getGridDimensions().marginatedRect.contains(point.x, point.y)) { + if (plot.containsPoint(point.x, point.y)) { Number x = plot.getXVal(point); Number y = plot.getYVal(point); - selection = null; double xDistance = 0; double yDistance = 0; - // find the closest value to the selection: - for (SeriesAndFormatter sfPair : plot.getSeriesRegistry()) { + for (SeriesBundle sfPair : plot + .getRegistry().getSeriesAndFormatterList()) { XYSeries series = sfPair.getSeries(); for (int i = 0; i < series.size(); i++) { Number thisX = series.getX(i); Number thisY = series.getY(i); if (thisX != null && thisY != null) { double thisXDistance = - LineRegion.measure(x, thisX).doubleValue(); + Region.measure(x, thisX).doubleValue(); double thisYDistance = - LineRegion.measure(y, thisY).doubleValue(); + Region.measure(y, thisY).doubleValue(); if (selection == null) { - selection = new Pair(i, series); + selection = new Pair<>(i, series); xDistance = thisXDistance; yDistance = thisYDistance; } else if (thisXDistance < xDistance) { - selection = new Pair(i, series); + selection = new Pair<>(i, series); xDistance = thisXDistance; yDistance = thisYDistance; } else if (thisXDistance == xDistance && thisYDistance < yDistance && thisY.doubleValue() >= y.doubleValue()) { - selection = new Pair(i, series); + selection = new Pair<>(i, series); xDistance = thisXDistance; yDistance = thisYDistance; } @@ -346,7 +396,7 @@ private void onPlotClicked(PointF point) { selection = null; } - if(selection == null) { + if (selection == null) { selectionWidget.setText(NO_SELECTION_TXT); } else { selectionWidget.setText("Selected: " + selection.second.getTitle() + @@ -366,7 +416,7 @@ private void onS1CheckBoxClicked(boolean checked) { private void onS2CheckBoxClicked(boolean checked) { if (checked) { - plot.addSeries(series2, formatter2); + plot.addSeries(series2, formatter2); } else { plot.removeSeries(series2); } @@ -374,6 +424,7 @@ private void onS2CheckBoxClicked(boolean checked) { } class MyBarFormatter extends BarFormatter { + public MyBarFormatter(int fillColor, int borderColor) { super(fillColor, borderColor); } @@ -384,7 +435,7 @@ public Class getRendererClass() { } @Override - public SeriesRenderer getRendererInstance(XYPlot plot) { + public SeriesRenderer doGetRendererInstance(XYPlot plot) { return new MyBarRenderer(plot); } } @@ -403,9 +454,9 @@ public MyBarRenderer(XYPlot plot) { * @return */ @Override - public MyBarFormatter getFormatter(int index, XYSeries series) { - if(selection != null && - selection.second == series && + public MyBarFormatter getFormatter(int index, XYSeries series) { + if (selection != null && + selection.second == series && selection.first == index) { return selectionFormatter; } else { diff --git a/demoapp/src/main/java/com/androidplot/demos/BubbleChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/BubbleChartActivity.java new file mode 100644 index 00000000..0ca7537a --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/BubbleChartActivity.java @@ -0,0 +1,71 @@ +/* + * 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.*; +import android.graphics.*; +import android.os.*; + +import com.androidplot.xy.*; + +import java.util.*; + +/** + * An example of a bubble chart. + */ +public class BubbleChartActivity extends Activity { + + private XYPlot plot; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.bubble_chart_example); + + // initialize our XYPlot reference: + plot = (XYPlot) findViewById(R.id.plot); + + // turn the above arrays into XYSeries': + // (Y_VALS_ONLY means use the element index as the x value) + BubbleSeries series1 = new BubbleSeries( + Arrays.asList(new Number[]{3, 5, 2, 3, 6}), + Arrays.asList(new Number[]{1, 5, 2, 2, 3}), "s1"); + + BubbleSeries series2 = new BubbleSeries( + Arrays.asList(new Number[]{2, 7, 3, 1, 3}), + Arrays.asList(new Number[]{2, 1, 2, 6, 7}), "s2"); + + BubbleSeries series3 = new BubbleSeries( + Arrays.asList(new Number[]{7, 2, 5, 6, 5}), + Arrays.asList(new Number[]{3, 2, 4, 6, 7}), "s3"); + + plot.setDomainBoundaries(-1, 5, BoundaryMode.FIXED); + plot.setRangeBoundaries(0, 8, BoundaryMode.FIXED); + + BubbleFormatter bf1 = new BubbleFormatter(this, R.xml.bubble_formatter1); + bf1.setPointLabelFormatter(new PointLabelFormatter(Color.BLACK)); + bf1.getPointLabelFormatter().getTextPaint().setTextAlign(Paint.Align.CENTER); + bf1.getPointLabelFormatter().getTextPaint().setFakeBoldText(true); + + // add series to the xyplot: + plot.addSeries(series1, bf1); + plot.addSeries(series2, new BubbleFormatter(this, R.xml.bubble_formatter2)); + plot.addSeries(series3, new BubbleFormatter(this, R.xml.bubble_formatter3)); + PanZoom.attach(plot); + } +} diff --git a/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java new file mode 100644 index 00000000..86ae1fbb --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java @@ -0,0 +1,167 @@ +/* + * Copyright 2016 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.graphics.Paint; +import android.os.Bundle; +import androidx.annotation.NonNull; + +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.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}. + */ +public class CandlestickChartActivity extends Activity { + + private XYPlot plot; + + private DecimalFormat currencyFormat = new DecimalFormat("$0.00"); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.candlestick_example); + + // initialize our XYPlot reference: + plot = (XYPlot) findViewById(R.id.plot); + + final CandlestickSeries candlestickSeries = new CandlestickSeries( + new CandlestickSeries.Item(1, 10, 2, 9.04), + new CandlestickSeries.Item(4, 18, 6, 5.50), + new CandlestickSeries.Item(3, 11, 5, 9.21), + new CandlestickSeries.Item(2, 17, 2, 15.25), + new CandlestickSeries.Item(6, 11, 11, 7.12), + new CandlestickSeries.Item(8, 16, 10, 15.02)); + + // draw a simple line plot of the close vals: + LineAndPointFormatter lpf = new LineAndPointFormatter(Color.BLACK, Color.BLACK, null, null); + lpf.getLinePaint().setPathEffect( + new DashPathEffect( + new float[] {PixelUtils.dpToPix(5), PixelUtils.dpToPix(5)}, 0)); + lpf.setInterpolationParams( + new CatmullRomInterpolator.Params(20, CatmullRomInterpolator.Type.Centripetal)); + + plot.addSeries(candlestickSeries.getCloseSeries(), lpf); + + CandlestickFormatter formatter = new CandlestickFormatter(this, R.xml.candlestick_formatter); + + // draw candlestick bodies as triangles instead of squares: + // triangles will point up for items that closed higher than they opened + // and down for those that closed lower: + formatter.setBodyStyle(CandlestickFormatter.BodyStyle.SQUARE); + + // add the candlestick series data to the plot: + CandlestickMaker.make(plot, formatter, candlestickSeries); + + // setup the range tick label formatting, etc: + plot.setLinesPerRangeLabel(3); + + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT). + setFormat(currencyFormat); + + // add some padding to range boundaries: + final Region minMax = SeriesUtils.minMax( + candlestickSeries.getHighSeries().getyVals(), + candlestickSeries.getLowSeries().getyVals()); + + plot.setRangeBoundaries( + minMax.getMin().doubleValue() - 1, + minMax.getMax().doubleValue() + 1, + BoundaryMode.FIXED); + + + // setup the domain tick label formatting, etc: + plot.setDomainBoundaries(-1, 6, BoundaryMode.FIXED); + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1); + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() { + @Override + public StringBuffer format(Object object, @NonNull StringBuffer buffer, + @NonNull FieldPosition field) { + int day = ((Number) object).intValue() % 7; + switch (day) { + case 0: + buffer.append("Sun"); + break; + case 1: + buffer.append("Mon"); + break; + case 2: + buffer.append("Tues"); + break; + case 3: + buffer.append("Wed"); + break; + case 4: + buffer.append("Thurs"); + break; + case 5: + buffer.append("Fri"); + break; + case 6: + buffer.append("Sat"); + default: + // show nothing + + } + return buffer; + } + + @Override + public Object parseObject(String string, @NonNull ParsePosition position) { + return null; + } + }); + + formatter.setPointLabelFormatter( + new PointLabelFormatter(Color.BLACK, PixelUtils.dpToPix(8), 0)); + formatter.getPointLabelFormatter().getTextPaint().setFakeBoldText(true); + formatter.getPointLabelFormatter().getTextPaint().setTextAlign(Paint.Align.LEFT); + + // add labels for close vals: + formatter.setPointLabeler(new PointLabeler() { + @Override + public String getLabel(XYSeries series, int index) { + if(series == candlestickSeries.getCloseSeries()) { + return currencyFormat.format(series.getY(index).doubleValue()); + } + return null; + } + }); + } +} diff --git a/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java b/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java new file mode 100644 index 00000000..df3d6117 --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java @@ -0,0 +1,11 @@ +package com.androidplot.demos; + +import android.app.*; + +public class DemoApplication extends Application { + + @Override public void onCreate() { + super.onCreate(); + } +} + 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/DualScaleXYPlotExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/DualScaleXYPlotExampleActivity.java deleted file mode 100644 index cd113b06..00000000 --- a/demoapp/src/main/java/com/androidplot/demos/DualScaleXYPlotExampleActivity.java +++ /dev/null @@ -1,244 +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 java.util.Arrays; - -import android.app.Activity; -import android.graphics.Color; -import android.graphics.Paint.Align; -import android.os.Build; -import android.os.Bundle; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; - -import com.androidplot.ui.AnchorPosition; -import com.androidplot.ui.DynamicTableModel; -import com.androidplot.ui.SizeLayoutType; -import com.androidplot.ui.Size; -import com.androidplot.xy.LineAndPointFormatter; -import com.androidplot.xy.PointLabelFormatter; -import com.androidplot.xy.SimpleXYSeries; -import com.androidplot.ui.XLayoutStyle; -import com.androidplot.xy.XYGraphWidget; -import com.androidplot.xy.XYLegendWidget; -import com.androidplot.xy.XYPlot; -import com.androidplot.xy.XYSeries; -import com.androidplot.ui.YLayoutStyle; -import com.androidplot.util.PixelUtils; - -/** - * The simplest possible example of using AndroidPlot to plot some data. - */ -public class DualScaleXYPlotExampleActivity extends Activity implements OnClickListener { - - private XYPlot plot1, plot2; - private Boolean series2_onRight = true; - private LineAndPointFormatter series1Format, series2Format; - private Button button; - - // Declare and enable buttons to toggle whether the 2nd series is on left or right. - - // Create a couple arrays of y-values to plot: - private Number[] series1Numbers = {1, 8, 5, 2, 7, 4}; - private Number[] series2Numbers = {444, 613, 353, 876, 924, 1004}; - XYSeries series1, series2; - - @Override - public void onCreate(Bundle savedInstanceState) { - - super.onCreate(savedInstanceState); - setContentView(R.layout.dual_scale_xy_plot_example); - - final float f26 = PixelUtils.dpToPix(26); - final float f10 = PixelUtils.dpToPix(10); - final Size sm = new Size(0, SizeLayoutType.FILL, 0, SizeLayoutType.FILL); - - plot1 = (XYPlot) findViewById(R.id.mySimpleXYPlot_L); - plot2 = (XYPlot) findViewById(R.id.mySimpleXYPlot_R); - - // Disable Hardware Acceleration on the xyPlot view object. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - plot1.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - plot2.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } - - /* - * Setup the Plots - */ - plot1.setPlotMargins(0, 0, 0, 0); - plot2.setPlotMargins(0, 0, 0, 0); - - plot1.setPlotPadding(0, 0, 0, 0); - plot2.setPlotPadding(0, 0, 0, 0); - - plot2.getDomainLabelWidget().setVisible(false); - plot2.getRangeLabelWidget().setVisible(false); - plot2.getTitleWidget().setVisible(false); - plot2.setBorderPaint(null); - plot2.setBackgroundPaint(null); - - /* - * Setup the Graph Widgets - */ - XYGraphWidget graphWidget_L = plot1.getGraphWidget(); - XYGraphWidget graphWidget_R = plot2.getGraphWidget(); - - graphWidget_L.setSize(sm); - graphWidget_R.setSize(sm); - - graphWidget_L.setMargins(0, 0, 0, 0); - graphWidget_R.setMargins(0, 0, 0, 0); - - graphWidget_L.setPadding(f26, f10, f26, f26); - graphWidget_R.setPadding(f26, f10, f26, f26); - - graphWidget_L.setRangeAxisPosition(true, false, 4, "10"); - graphWidget_R.setRangeAxisPosition(false, false, 4, "10"); - - graphWidget_L.setRangeTickLabelVerticalOffset(-3); - graphWidget_R.setRangeTickLabelVerticalOffset(-3); - - graphWidget_L.setRangeOriginTickLabelPaint(null); - graphWidget_R.setRangeOriginTickLabelPaint(null); - - graphWidget_L.setRangeTickLabelWidth(0); - graphWidget_R.setRangeTickLabelWidth(0); - - graphWidget_L.setDomainTickLabelWidth(0); - graphWidget_R.setDomainTickLabelWidth(0); - - graphWidget_R.setBackgroundPaint(null); - graphWidget_R.setDomainTickLabelPaint(null); - graphWidget_R.setGridBackgroundPaint(null); - graphWidget_R.setDomainOriginTickLabelPaint(null); - graphWidget_R.setRangeOriginLinePaint(null); - graphWidget_R.setDomainGridLinePaint(null); - graphWidget_R.setRangeGridLinePaint(null); - - graphWidget_L.getRangeTickLabelPaint().setTextSize(PixelUtils.dpToPix(8)); - graphWidget_R.getRangeTickLabelPaint().setTextSize(PixelUtils.dpToPix(8)); - - graphWidget_L.getDomainOriginTickLabelPaint().setTextSize(PixelUtils.dpToPix(8)); - graphWidget_L.getDomainTickLabelPaint().setTextSize(PixelUtils.dpToPix(8)); - - float textSize = graphWidget_L.getRangeTickLabelPaint().getTextSize(); - graphWidget_L.setRangeTickLabelVerticalOffset((textSize / 2) * -1); - graphWidget_R.setRangeTickLabelVerticalOffset(graphWidget_L.getRangeTickLabelVerticalOffset()); - - /* - * Position the Graph Widgets in the Centre - */ - graphWidget_L.position(0, XLayoutStyle.ABSOLUTE_FROM_CENTER, 0, YLayoutStyle.ABSOLUTE_FROM_CENTER, AnchorPosition.CENTER); - graphWidget_R.position(0, XLayoutStyle.ABSOLUTE_FROM_CENTER, 0, YLayoutStyle.ABSOLUTE_FROM_CENTER, AnchorPosition.CENTER); - - /* - * Position the Label Widgets - */ - plot1.getDomainLabelWidget().setWidth(100); - plot1.getRangeLabelWidget().setWidth(100); - plot1.getDomainLabelWidget().position(0, XLayoutStyle.RELATIVE_TO_CENTER, 1, YLayoutStyle.ABSOLUTE_FROM_BOTTOM, AnchorPosition.BOTTOM_MIDDLE); - plot1.getRangeLabelWidget().position(1, XLayoutStyle.ABSOLUTE_FROM_LEFT, -20, YLayoutStyle.ABSOLUTE_FROM_CENTER, AnchorPosition.LEFT_BOTTOM); - - /* - * Setup and Position the LEFT Legend - */ - XYLegendWidget legendWidget_LEFT = plot1.getLegendWidget(); - legendWidget_LEFT.setSize(new Size(100, SizeLayoutType.ABSOLUTE, 200, SizeLayoutType.ABSOLUTE)); - legendWidget_LEFT.setPadding(1, 1, 1, 1); - legendWidget_LEFT.setTableModel(new DynamicTableModel(1, 3)); - legendWidget_LEFT.setIconSize(new Size(PixelUtils.dpToPix(10), SizeLayoutType.ABSOLUTE, PixelUtils.dpToPix(10), SizeLayoutType.ABSOLUTE)); - legendWidget_LEFT.getTextPaint().setTextSize(PixelUtils.dpToPix(9)); - legendWidget_LEFT.position(PixelUtils.dpToPix(30), XLayoutStyle.ABSOLUTE_FROM_LEFT, f10 + 2, YLayoutStyle.ABSOLUTE_FROM_TOP, AnchorPosition.LEFT_TOP); - - - /* - * Setup and Position the RIGHT Legend - */ - XYLegendWidget legendWidget_RIGHT = plot2.getLegendWidget(); - legendWidget_RIGHT.setSize(new Size(100, SizeLayoutType.ABSOLUTE, 200, SizeLayoutType.ABSOLUTE)); - legendWidget_RIGHT.setPadding(1, 1, 1, 1); - legendWidget_RIGHT.setTableModel(new DynamicTableModel(1, 3)); - legendWidget_RIGHT.setIconSize(new Size(PixelUtils.dpToPix(10), SizeLayoutType.ABSOLUTE, PixelUtils.dpToPix(10), SizeLayoutType.ABSOLUTE)); - legendWidget_RIGHT.getTextPaint().setTextSize(PixelUtils.dpToPix(9)); - legendWidget_RIGHT.getTextPaint().setTextAlign(Align.RIGHT); - legendWidget_RIGHT.setMarginLeft(185); - legendWidget_RIGHT.position(PixelUtils.dpToPix(30), XLayoutStyle.ABSOLUTE_FROM_RIGHT, f10 + 2, YLayoutStyle.ABSOLUTE_FROM_TOP, AnchorPosition.RIGHT_TOP); - - // Setup the Series - series1 = new SimpleXYSeries(Arrays.asList(series1Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Series1"); - series2 = new SimpleXYSeries(Arrays.asList(series2Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Series2"); - - // Setup the formatters - series1Format = new LineAndPointFormatter(Color.rgb(0, 200, 0), Color.rgb(0, 100, 0), null, new PointLabelFormatter(Color.WHITE)); - series2Format = new LineAndPointFormatter(Color.rgb(0, 0, 200), Color.rgb(0, 0, 100), null, new PointLabelFormatter(Color.WHITE)); - - // Setup the Button - button = (Button) findViewById(R.id.toggleSeries2); - button.setOnClickListener(this); - } - - @Override - protected void onResume() { - super.onResume(); - - updateView(); - - } - - private void updateView() { - - // Remove all current series from each plot - plot1.clear(); - plot2.clear(); - - // Add series to each plot as needed. - plot1.addSeries(series1, series1Format); - if (series2_onRight) { - plot2.addSeries(series2, series2Format); - } else { - plot1.addSeries(series2, series2Format); - } - - // Finalise each Plot based on whether they have any series or not. - if (!plot2.getSeriesRegistry().isEmpty()) { - plot2.setVisibility(XYPlot.VISIBLE); - plot2.redraw(); - } else { - plot2.setVisibility(XYPlot.INVISIBLE); - } - - if (!plot1.getSeriesRegistry().isEmpty()) { - plot1.setVisibility(XYPlot.VISIBLE); - plot1.redraw(); - } else { - plot1.setVisibility(XYPlot.INVISIBLE); - } - - } - - @Override - public void onClick(View v) { - if (series2_onRight) { - series2_onRight = false; - } else { - series2_onRight = true; - } - updateView(); - } -} diff --git a/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java index 659f1f40..f18b518b 100644 --- a/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java @@ -55,7 +55,7 @@ public void onCreate(Bundle savedInstanceState) { // android boilerplate stuff super.onCreate(savedInstanceState); - setContentView(R.layout.dynamicxyplot_example); + setContentView(R.layout.dynamic_xyplot_example); // get handles to our View defined in layout.xml: dynamicPlot = (XYPlot) findViewById(R.id.dynamicXYPlot); @@ -63,7 +63,8 @@ public void onCreate(Bundle savedInstanceState) { plotUpdater = new MyPlotUpdater(dynamicPlot); // only display whole numbers in domain labels - dynamicPlot.getGraphWidget().setDomainValueFormat(new DecimalFormat("0")); + dynamicPlot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM). + setFormat(new DecimalFormat("0")); // getInstance and position datasets: data = new SampleDynamicXYDatasource(); @@ -71,7 +72,7 @@ public void onCreate(Bundle savedInstanceState) { SampleDynamicSeries sine2Series = new SampleDynamicSeries(data, 1, "Sine 2"); LineAndPointFormatter formatter1 = new LineAndPointFormatter( - Color.rgb(0, 0, 0), null, null, null); + Color.rgb(0, 200, 0), null, null, null); formatter1.getLinePaint().setStrokeJoin(Paint.Join.ROUND); formatter1.getLinePaint().setStrokeWidth(10); dynamicPlot.addSeries(sine1Series, @@ -89,13 +90,14 @@ public void onCreate(Bundle savedInstanceState) { data.addObserver(plotUpdater); // thin out domain tick labels so they dont overlap each other: - dynamicPlot.setDomainStepMode(XYStepMode.INCREMENT_BY_VAL); + dynamicPlot.setDomainStepMode(StepMode.INCREMENT_BY_VAL); dynamicPlot.setDomainStepValue(5); - dynamicPlot.setRangeStepMode(XYStepMode.INCREMENT_BY_VAL); + dynamicPlot.setRangeStepMode(StepMode.INCREMENT_BY_VAL); dynamicPlot.setRangeStepValue(10); - dynamicPlot.setRangeValueFormat(new DecimalFormat("###.#")); + dynamicPlot.getGraph().getLineLabelStyle( + XYGraphWidget.Edge.LEFT).setFormat(new DecimalFormat("###.#")); // uncomment this line to freeze the range boundaries: dynamicPlot.setRangeBoundaries(-100, 100, BoundaryMode.FIXED); @@ -103,10 +105,8 @@ public void onCreate(Bundle savedInstanceState) { // create a dash effect for domain and range grid lines: DashPathEffect dashFx = new DashPathEffect( new float[] {PixelUtils.dpToPix(3), PixelUtils.dpToPix(3)}, 0); - dynamicPlot.getGraphWidget().getDomainGridLinePaint().setPathEffect(dashFx); - dynamicPlot.getGraphWidget().getRangeGridLinePaint().setPathEffect(dashFx); - - + dynamicPlot.getGraph().getDomainGridLinePaint().setPathEffect(dashFx); + dynamicPlot.getGraph().getRangeGridLinePaint().setPathEffect(dashFx); } @Override @@ -138,9 +138,9 @@ 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; - private static final int SAMPLE_SIZE = 30; + 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; private MyObservable notifier; @@ -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 new file mode 100644 index 00000000..2bd7dec5 --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/ECGExample.java @@ -0,0 +1,211 @@ +/* + * Copyright 2016 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.Paint; +import android.os.Bundle; +import com.androidplot.Plot; +import com.androidplot.util.Redrawer; +import com.androidplot.xy.*; + +import java.lang.ref.*; + +/** + * An example of a real-time plot displaying an asynchronously updated model of ECG data. There are three + * key items to pay attention to here: + * 1 - The model data is updated independently of all other data via a background thread. This is typical + * of most signal inputs. + * + * 2 - The main render loop is controlled by a separate thread governed by an instance of {@link Redrawer}. + * The alternative is to try synchronously invoking {@link Plot#redraw()} within whatever system is updating + * the model, which would severely degrade performance. + * + * 3 - The plot is set to render using a background thread via config attr in R.layout.ecg_example.xml. + * This ensures that the rest of the app will remain responsive during rendering. + */ +public class ECGExample extends Activity { + private XYPlot plot; + + /** + * Uses a separate thread to modulate redraw frequency. + */ + private Redrawer redrawer; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.ecg_example); + + // initialize our XYPlot reference: + plot = (XYPlot) findViewById(R.id.plot); + + ECGModel ecgSeries = new ECGModel(2000, 200); + + // add a new series' to the xyplot: + MyFadeFormatter formatter =new MyFadeFormatter(2000); + formatter.setLegendIconEnabled(false); + plot.addSeries(ecgSeries, formatter); + plot.setRangeBoundaries(0, 10, BoundaryMode.FIXED); + plot.setDomainBoundaries(0, 2000, BoundaryMode.FIXED); + + // reduce the number of range labels + plot.setLinesPerRangeLabel(3); + + // start generating ecg data in the background: + ecgSeries.start(new WeakReference<>(plot.getRenderer(AdvancedLineAndPointRenderer.class))); + + // set a redraw rate of 30hz and start immediately: + redrawer = new Redrawer(plot, 30, true); + } + + /** + * Special {@link AdvancedLineAndPointRenderer.Formatter} that draws a line + * that fades over time. Designed to be used in conjunction with a circular buffer model. + */ + public static class MyFadeFormatter extends AdvancedLineAndPointRenderer.Formatter { + + private int trailSize; + + MyFadeFormatter(int trailSize) { + this.trailSize = trailSize; + } + + @Override + public Paint getLinePaint(int thisIndex, int latestIndex, int seriesSize) { + // offset from the latest index: + int offset; + if(thisIndex > latestIndex) { + offset = latestIndex + (seriesSize - thisIndex); + } else { + offset = latestIndex - thisIndex; + } + + float scale = 255f / trailSize; + int alpha = (int) (255 - (offset * scale)); + getLinePaint().setAlpha(alpha > 0 ? alpha : 0); + return getLinePaint(); + } + } + + /** + * Primitive simulation of some kind of signal. For this example, + * we'll pretend its an ecg. This class represents the data as a circular buffer; + * data is added sequentially from left to right. When the end of the buffer is reached, + * i is reset back to 0 and simulated sampling continues. + */ + public static class ECGModel implements XYSeries { + + private final Number[] data; + private final long delayMs; + private final int blipInteral; + private final Thread thread; + private boolean keepRunning; + private int latestIndex; + + private WeakReference rendererRef; + + /** + * + * @param size Sample size contained within this model + * @param updateFreqHz Frequency at which new samples are added to the model + */ + ECGModel(int size, int updateFreqHz) { + data = new Number[size]; + for(int i = 0; i < data.length; i++) { + data[i] = 0; + } + + // translate hz into delay (ms): + delayMs = 1000 / updateFreqHz; + + // add 7 "blips" into the signal: + blipInteral = size / 7; + + thread = new Thread(new Runnable() { + @Override + public void run() { + try { + while (keepRunning) { + if (latestIndex >= data.length) { + latestIndex = 0; + } + + // generate some random data: + if (latestIndex % blipInteral == 0) { + // insert a "blip" to simulate a heartbeat: + data[latestIndex] = (Math.random() * 10) + 3; + } else { + // insert a random sample: + data[latestIndex] = Math.random() * 2; + } + + if(latestIndex < data.length - 1) { + // null out the point immediately following i, to disable + // connecting i and i+1 with a line: + data[latestIndex +1] = null; + } + + if(rendererRef.get() != null) { + rendererRef.get().setLatestIndex(latestIndex); + Thread.sleep(delayMs); + } else { + keepRunning = false; + } + latestIndex++; + } + } catch (InterruptedException e) { + keepRunning = false; + } + } + }); + } + + void start(final WeakReference rendererRef) { + this.rendererRef = rendererRef; + keepRunning = true; + thread.start(); + } + + @Override + public int size() { + return data.length; + } + + @Override + public Number getX(int index) { + return index; + } + + @Override + public Number getY(int index) { + return data[index]; + } + + @Override + public String getTitle() { + return "Signal"; + } + } + + @Override + public void onStop() { + super.onStop(); + redrawer.finish(); + } +} diff --git a/demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java new file mode 100644 index 00000000..19b01cdb --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java @@ -0,0 +1,134 @@ +/* + * Copyright 2016 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.*; +import android.graphics.*; +import android.os.*; + +import com.androidplot.util.*; +import com.androidplot.xy.*; + +import java.text.*; +import java.util.*; + +/** + * A simple XYPlot + */ +public class FXPlotExampleActivity extends Activity { + + private XYPlot plot; + + /** + * Custom line label renderer that highlights origin labels + */ + class MyLineLabelRenderer extends XYGraphWidget.LineLabelRenderer { + + @Override + protected void drawLabel(Canvas canvas, String text, Paint paint, + float x, float y, boolean isOrigin) { + if(isOrigin) { + // make the origin labels red: + final Paint originPaint = new Paint(paint); + originPaint.setColor(Color.RED); + super.drawLabel(canvas, text, originPaint, x, y , isOrigin); + } else { + super.drawLabel(canvas, text, paint, x, y , isOrigin); + } + } + } + + /** + * Draws every other tick label and renders text in gray instead of white. + */ + class MySecondaryLabelRenderer extends MyLineLabelRenderer { + + + @Override + public void drawLabel(Canvas canvas, XYGraphWidget.LineLabelStyle style, + Number val, float x, float y, boolean isOrigin) { + if(val.doubleValue() % 2 == 0) { + final Paint paint = style.getPaint(); + if(!isOrigin) { + paint.setColor(Color.GRAY); + } + super.drawLabel(canvas, style, val, x, y, isOrigin); + } + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.fx_plot_example); + + // initialize our XYPlot reference: + plot = (XYPlot) findViewById(R.id.plot); + + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1); + plot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1); + + plot.centerOnDomainOrigin(0); + plot.centerOnRangeOrigin(0); + + // create formatters to use for drawing a series using LineAndPointRenderer + // and configure them from xml: + LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter); + + // use our custom renderer to make origin labels red + plot.getGraph().setLineLabelRenderer(XYGraphWidget.Edge.BOTTOM, new MyLineLabelRenderer()); + plot.getGraph().setLineLabelRenderer(XYGraphWidget.Edge.LEFT, new MyLineLabelRenderer()); + + // skip every other line for top and right edge labels + plot.getGraph().setLineLabelRenderer(XYGraphWidget.Edge.RIGHT, new MySecondaryLabelRenderer()); + plot.getGraph().setLineLabelRenderer(XYGraphWidget.Edge.TOP, new MySecondaryLabelRenderer()); + + // don't show decimal places for top and right edge labels + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.TOP).setFormat(new DecimalFormat("0")); + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.RIGHT).setFormat(new DecimalFormat("0")); + + // create a dash effect for domain and range grid lines: + DashPathEffect dashFx = new DashPathEffect( + new float[] {PixelUtils.dpToPix(3), PixelUtils.dpToPix(3)}, 0); + plot.getGraph().getDomainGridLinePaint().setPathEffect(dashFx); + plot.getGraph().getRangeGridLinePaint().setPathEffect(dashFx); + + // add a new series' to the xyplot: + plot.addSeries(generateSeries(-5, 5, 100), series1Format); + } + + protected XYSeries generateSeries(double minX, double maxX, double resolution) { + final double range = maxX - minX; + final double step = range / resolution; + List xVals = new ArrayList<>(); + List yVals = new ArrayList<>(); + + double x = minX; + while (x <= maxX) { + xVals.add(x); + yVals.add(fx(x)); + x +=step; + } + + return new SimpleXYSeries(xVals, yVals, "f(x) = (x^2) - 13"); + } + + protected double fx(double x) { + return Math.abs(x*x) - 13; + } +} diff --git a/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java b/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java index b9df57b0..290b7802 100644 --- a/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java @@ -20,16 +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.Series; -import com.androidplot.ui.SeriesAndFormatter; +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; @@ -41,7 +46,7 @@ public class ListViewActivity extends Activity { private static final int NUM_SERIES_PER_PLOT = 5; private ListView lv; - private List>> seriesData = new ArrayList<>(NUM_PLOTS); + private List>> seriesData = new ArrayList<>(NUM_PLOTS); public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -55,7 +60,7 @@ public void onCreate(Bundle savedInstanceState) { protected void generateData() { Random generator = new Random(); for(int i = 0; i < NUM_PLOTS; i++) { - List> seriesList + List> seriesList = new ArrayList<>(NUM_SERIES_PER_PLOT); for (int k = 0; k < NUM_SERIES_PER_PLOT; k++) { @@ -73,17 +78,17 @@ 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: lpf.setInterpolationParams( new CatmullRomInterpolator.Params(20, CatmullRomInterpolator.Type.Centripetal)); - seriesList.add(new SeriesAndFormatter( + seriesList.add(new SeriesBundle( new SimpleXYSeries(nums, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "S" + k), lpf)); } @@ -101,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; @@ -112,10 +118,10 @@ public View getView(int pos, View convertView, ViewGroup parent) { Plot p = (XYPlot) v.findViewById(R.id.xyplot); p.clear(); - p.setTitle("plot" + pos); + p.getTitle().setText("plot" + pos); - List> thisSeriesList = seriesData.get(pos); - for(SeriesAndFormatter sf : thisSeriesList) { + List> thisSeriesList = seriesData.get(pos); + for(SeriesBundle sf : thisSeriesList) { p.addSeries(sf.getSeries(), sf.getFormatter()); } p.redraw(); 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 b1ec4aae..00000000 --- a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java +++ /dev/null @@ -1,151 +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 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 startDualScaleExampleButton = (Button)findViewById(R.id.startDualScaleExampleButton); - startDualScaleExampleButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, DualScaleXYPlotExampleActivity.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)); - } - }); - - } -} 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 b20a8a92..71e721d8 100644 --- a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java @@ -18,7 +18,7 @@ import android.app.Activity; import android.content.Context; -import android.graphics.Color; +import android.graphics.*; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; @@ -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.*; @@ -38,8 +39,7 @@ // See: http://developer.android.com/reference/android/hardware/SensorEvent.html public class OrientationSensorExampleActivity extends Activity implements SensorEventListener { - - private static final int HISTORY_SIZE = 300; // number of points to plot in history + private static final int HISTORY_SIZE = 1000; private SensorManager sensorMgr = null; private Sensor orSensor = null; @@ -48,7 +48,7 @@ public class OrientationSensorExampleActivity extends Activity implements Sensor private CheckBox hwAcceleratedCb; private CheckBox showFpsCb; - //private SimpleXYSeries aprLevelsSeries = null; + private SimpleXYSeries aLvlSeries; private SimpleXYSeries pLvlSeries; private SimpleXYSeries rLvlSeries; @@ -67,7 +67,6 @@ public void onCreate(Bundle savedInstanceState) { // setup the APR Levels plot: aprLevelsPlot = (XYPlot) findViewById(R.id.aprLevelsPlot); aprLevelsPlot.setDomainBoundaries(-1, 1, BoundaryMode.FIXED); - aprLevelsPlot.getGraphWidget().getDomainTickLabelPaint().setColor(Color.TRANSPARENT); aLvlSeries = new SimpleXYSeries("A"); pLvlSeries = new SimpleXYSeries("P"); @@ -81,7 +80,7 @@ public void onCreate(Bundle savedInstanceState) { new BarFormatter(Color.rgb(0, 0, 200), Color.rgb(0, 80, 0))); aprLevelsPlot.setDomainStepValue(3); - aprLevelsPlot.setTicksPerRangeLabel(3); + aprLevelsPlot.setLinesPerRangeLabel(3); // per the android documentation, the minimum and maximum readings we can get from // any of the orientation sensors is -180 and 359 respectively so we will fix our plot's @@ -91,11 +90,11 @@ public void onCreate(Bundle savedInstanceState) { // update our domain and range axis labels: aprLevelsPlot.setDomainLabel(""); - aprLevelsPlot.getDomainLabelWidget().pack(); + aprLevelsPlot.getDomainTitle().pack(); aprLevelsPlot.setRangeLabel("Angle (Degs)"); - aprLevelsPlot.getRangeLabelWidget().pack(); - aprLevelsPlot.setGridPadding(15, 0, 15, 0); - aprLevelsPlot.setRangeValueFormat(new DecimalFormat("#")); + aprLevelsPlot.getRangeTitle().pack(); + aprLevelsPlot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT). + setFormat(new DecimalFormat("#")); // setup the APR History plot: aprHistoryPlot = (XYPlot) findViewById(R.id.aprHistoryPlot); @@ -118,16 +117,19 @@ public void onCreate(Bundle savedInstanceState) { aprHistoryPlot.addSeries(rollHistorySeries, new LineAndPointFormatter( Color.rgb(200, 100, 100), null, null, null)); - aprHistoryPlot.setDomainStepMode(XYStepMode.INCREMENT_BY_VAL); + aprHistoryPlot.setDomainStepMode(StepMode.INCREMENT_BY_VAL); aprHistoryPlot.setDomainStepValue(HISTORY_SIZE/10); - aprHistoryPlot.setTicksPerRangeLabel(3); + aprHistoryPlot.setLinesPerRangeLabel(3); aprHistoryPlot.setDomainLabel("Sample Index"); - aprHistoryPlot.getDomainLabelWidget().pack(); + aprHistoryPlot.getDomainTitle().pack(); aprHistoryPlot.setRangeLabel("Angle (Degs)"); - aprHistoryPlot.getRangeLabelWidget().pack(); + aprHistoryPlot.getRangeTitle().pack(); - aprHistoryPlot.setRangeValueFormat(new DecimalFormat("#")); - aprHistoryPlot.setDomainValueFormat(new DecimalFormat("#")); + aprHistoryPlot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT). + setFormat(new DecimalFormat("#")); + + aprHistoryPlot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM). + setFormat(new DecimalFormat("#")); // setup checkboxes: hwAcceleratedCb = (CheckBox) findViewById(R.id.hwAccelerationCb); @@ -159,10 +161,11 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { }); // get a ref to the BarRenderer so we can make some changes to it: - BarRenderer barRenderer = (BarRenderer) aprLevelsPlot.getRenderer(BarRenderer.class); + 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: @@ -241,7 +244,6 @@ public synchronized void onSensorChanged(SensorEvent sensorEvent) { rollHistorySeries.addLast(null, sensorEvent.values[2]); } - @Override public void onAccuracyChanged(Sensor sensor, int i) { // Not interested in this event 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/ScatterPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/ScatterPlotActivity.java index c8e0b347..def16c32 100644 --- a/demoapp/src/main/java/com/androidplot/demos/ScatterPlotActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/ScatterPlotActivity.java @@ -45,23 +45,18 @@ public void onCreate(Bundle savedInstanceState) // create formatters to use for drawing a series using LineAndPointRenderer // and configure them from xml: - LineAndPointFormatter series1Format = new LineAndPointFormatter(); - series1Format.configure(getApplicationContext(), - R.xml.point_formatter); + LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.point_formatter); - LineAndPointFormatter series2Format = new LineAndPointFormatter(); - series2Format.configure(getApplicationContext(), - R.xml.point_formatter_2); + LineAndPointFormatter series2Format = + new LineAndPointFormatter(this, R.xml.point_formatter_2); // add each series to the xyplot: plot.addSeries(series1, series1Format); plot.addSeries(series2, series2Format); // reduce the number of range labels - plot.setTicksPerRangeLabel(3); - - // rotate domain labels 45 degrees to make them more compact horizontally: - plot.getGraphWidget().setDomainLabelOrientation(-45); + plot.setLinesPerRangeLabel(3); } /** diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java index cfe37244..cf419083 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java @@ -16,17 +16,24 @@ 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; import com.androidplot.pie.PieRenderer; import com.androidplot.pie.Segment; import com.androidplot.pie.SegmentFormatter; +import com.androidplot.util.*; + +import java.util.*; /** * The simplest possible example of using AndroidPlot to plot some data. @@ -34,16 +41,19 @@ public class SimplePieChartActivity extends Activity { + public static final int SELECTED_SEGMENT_OFFSET = 50; + private TextView donutSizeTextView; private SeekBar donutSizeSeekBar; - private PieChart pie; + public PieChart pie; private Segment s1; private Segment s2; private Segment s3; private Segment s4; + @SuppressLint("ClickableViewAccessibility") @Override public void onCreate(Bundle savedInstanceState) { @@ -54,26 +64,52 @@ 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); + // detect segment clicks: pie.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { PointF click = new PointF(motionEvent.getX(), motionEvent.getY()); - if(pie.getPieWidget().containsPoint(click)) { + if(pie.getPie().containsPoint(click)) { Segment segment = pie.getRenderer(PieRenderer.class).getContainingSegment(click); + if(segment != null) { - // handle the segment click...for now, just print - // the clicked segment's title to the console: - System.out.println("Clicked Segment: " + segment.getTitle()); + final boolean isSelected = getFormatter(segment).getOffset() != 0; + deselectAll(); + setSelected(segment, !isSelected); + pie.redraw(); } } return false; } - }); + private SegmentFormatter getFormatter(Segment segment) { + return pie.getFormatter(segment, PieRenderer.class); + } - donutSizeSeekBar = (SeekBar) findViewById(R.id.donutSizeSeekBar); + private void deselectAll() { + List segments = pie.getRegistry().getSeriesList(); + for(Segment segment : segments) { + setSelected(segment, false); + } + } + + private void setSelected(Segment segment, boolean isSelected) { + SegmentFormatter f = getFormatter(segment); + if(isSelected) { + f.setOffset(SELECTED_SEGMENT_OFFSET); + } else { + f.setOffset(0); + } + } + }); + donutSizeSeekBar = (SeekBar) findViewById(R.id.donutSizeSeekBar); donutSizeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) {} @@ -93,44 +129,72 @@ public void onStopTrackingTouch(SeekBar seekBar) { donutSizeTextView = (TextView) findViewById(R.id.donutSizeTextView); updateDonutText(); - s1 = new Segment("s1", 10); + s1 = new Segment("s1", 3); s2 = new Segment("s2", 1); - s3 = new Segment("s3", 10); - s4 = new Segment("s4", 10); + s3 = new Segment("s3", 7); + s4 = new Segment("s4", 9); EmbossMaskFilter emf = new EmbossMaskFilter( new float[]{1, 1, 1}, 0.4f, 10, 8.2f); - SegmentFormatter sf1 = new SegmentFormatter(); - sf1.configure(getApplicationContext(), R.xml.pie_segment_formatter1); - + SegmentFormatter sf1 = new SegmentFormatter(this, R.xml.pie_segment_formatter1); + sf1.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf1.getFillPaint().setMaskFilter(emf); - SegmentFormatter sf2 = new SegmentFormatter(); - sf2.configure(getApplicationContext(), R.xml.pie_segment_formatter2); - + SegmentFormatter sf2 = new SegmentFormatter(this, R.xml.pie_segment_formatter2); + sf2.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf2.getFillPaint().setMaskFilter(emf); - SegmentFormatter sf3 = new SegmentFormatter(); - sf3.configure(getApplicationContext(), R.xml.pie_segment_formatter3); - + SegmentFormatter sf3 = new SegmentFormatter(this, R.xml.pie_segment_formatter3); + sf3.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf3.getFillPaint().setMaskFilter(emf); - SegmentFormatter sf4 = new SegmentFormatter(); - sf4.configure(getApplicationContext(), R.xml.pie_segment_formatter4); - + SegmentFormatter sf4 = new SegmentFormatter(this, R.xml.pie_segment_formatter4); + sf4.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf4.getFillPaint().setMaskFilter(emf); - pie.addSeries(s1, sf1); - pie.addSeries(s2, sf2); - pie.addSeries(s3, sf3); - pie.addSeries(s4, sf4); + pie.addSegment(s1, sf1); + pie.addSegment(s2, sf2); + pie.addSegment(s3, sf3); + pie.addSegment(s4, sf4); pie.getBorderPaint().setColor(Color.TRANSPARENT); 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 ee23f655..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,20 +15,29 @@ */ package com.androidplot.demos; + import android.app.Activity; 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.Arrays; /** * A simple XYPlot */ -public class SimpleXYPlotActivity extends Activity -{ +public class SimpleXYPlotActivity extends Activity { private XYPlot plot; @@ -39,43 +48,36 @@ public void onCreate(Bundle savedInstanceState) setContentView(R.layout.simple_xy_plot_example); // initialize our XYPlot reference: - plot = (XYPlot) findViewById(R.id.plot); - - plot.getLayoutManager().moveToBottom(plot.getTitleWidget()); + 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}; 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"); + 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: - LineAndPointFormatter series1Format = new LineAndPointFormatter(); - series1Format.setPointLabelFormatter(new PointLabelFormatter()); - series1Format.configure(getApplicationContext(), - R.xml.line_point_formatter_with_labels); + LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter_with_labels); - LineAndPointFormatter series2Format = new LineAndPointFormatter(); - series2Format.setPointLabelFormatter(new PointLabelFormatter()); - series2Format.configure(getApplicationContext(), - R.xml.line_point_formatter_with_labels_2); + LineAndPointFormatter series2Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter_with_labels_2); // add an "dash" effect to the series2 line: - series2Format.getLinePaint().setPathEffect( - new DashPathEffect(new float[] { + series2Format.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)); + // always use DP when specifying pixel sizes, to keep things consistent across devices: + 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)); @@ -87,10 +89,16 @@ public void onCreate(Bundle savedInstanceState) plot.addSeries(series1, series1Format); plot.addSeries(series2, series2Format); - // reduce the number of range labels - plot.setTicksPerRangeLabel(3); - - // rotate domain labels 45 degrees to make them more compact horizontally: - plot.getGraphWidget().setDomainLabelOrientation(-45); + 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; + } + }); } } diff --git a/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java index 5690cd1e..4b1fdc35 100644 --- a/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java @@ -18,11 +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; @@ -53,61 +62,59 @@ public void onCreate(Bundle savedInstanceState) SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Thread #1"); - // Create a getFormatter to use for drawing a series using LineAndPointRenderer: - LineAndPointFormatter series1Format = new LineAndPointFormatter( - Color.rgb(0, 100, 0), // line color - Color.rgb(0, 100, 0), // point color - Color.rgb(100, 200, 0), null); // fill color - + final int screenHeightPx = getWindowManager().getDefaultDisplay().getHeight(); // setup our line fill paint to be a slightly transparent gradient: Paint lineFill = new Paint(); lineFill.setAlpha(200); - lineFill.setShader(new LinearGradient(0, 0, 0, 250, Color.WHITE, Color.BLUE, Shader.TileMode.MIRROR)); + lineFill.setShader(new LinearGradient(0, 0, 0, screenHeightPx, Color.WHITE, Color.BLUE, Shader.TileMode.MIRROR)); - StepFormatter stepFormatter = new StepFormatter(Color.rgb(0, 0,0), Color.BLUE); - stepFormatter.getLinePaint().setStrokeWidth(1); + StepFormatter stepFormatter = new StepFormatter(Color.WHITE, Color.BLUE); + stepFormatter.setVertexPaint(null); // don't draw individual points + stepFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(3)); stepFormatter.getLinePaint().setAntiAlias(false); stepFormatter.setFillPaint(lineFill); mySimpleXYPlot.addSeries(series2, stepFormatter); - // adjust the domain/range ticks to make more sense; label per tick for range and label per 5 ticks domain: - mySimpleXYPlot.setRangeStep(XYStepMode.INCREMENT_BY_VAL, 1); - mySimpleXYPlot.setDomainStep(XYStepMode.INCREMENT_BY_VAL, 1); - mySimpleXYPlot.setTicksPerRangeLabel(1); - mySimpleXYPlot.setTicksPerDomainLabel(5); + // adjust the domain/range ticks to make more sense; label per line for range and label per 5 ticks domain: + mySimpleXYPlot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1); + mySimpleXYPlot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1); + mySimpleXYPlot.setLinesPerRangeLabel(1); + mySimpleXYPlot.setLinesPerDomainLabel(5); // get rid of decimal points in our domain labels: - mySimpleXYPlot.setDomainValueFormat(new DecimalFormat("0")); + mySimpleXYPlot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM). + setFormat(new DecimalFormat("0")); // create a custom getFormatter to draw our state names as range tick labels: - mySimpleXYPlot.setRangeValueFormat(new Format() { - @Override - public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { - Number num = (Number) obj; - switch(num.intValue()) { - case 1: - toAppendTo.append("Init"); - break; - case 2: - toAppendTo.append("Idle"); - break; - case 3: - toAppendTo.append("Recv"); - break; - case 4: - toAppendTo.append("Send"); - break; - default: - toAppendTo.append("Unknown"); - break; - } - return toAppendTo; - } + mySimpleXYPlot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).setFormat(new Format() { + @Override + public StringBuffer format(Object obj, @NonNull StringBuffer toAppendTo, + @NonNull FieldPosition pos) { + Number num = (Number) obj; + switch (num.intValue()) { + case 1: + toAppendTo.append("Init"); + break; + case 2: + toAppendTo.append("Idle"); + break; + case 3: + toAppendTo.append("Recv"); + break; + case 4: + toAppendTo.append("Send"); + break; + default: + toAppendTo.append("Unknown"); + break; + } + return 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 0a943083..12930bd7 100644 --- a/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java @@ -16,113 +16,152 @@ 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 com.androidplot.xy.SimpleXYSeries; -import com.androidplot.xy.XYSeries; -import com.androidplot.xy.*; +import androidx.annotation.NonNull; -import java.text.*; +import com.androidplot.util.PixelUtils; +import com.androidplot.xy.BoundaryMode; +import com.androidplot.xy.LineAndPointFormatter; +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.text.SimpleDateFormat; import java.util.Arrays; +import java.util.Calendar; import java.util.Date; +import java.util.GregorianCalendar; + +public class TimeSeriesActivity extends Activity { -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) - { + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.time_series_example); plot1 = (XYPlot) findViewById(R.id.plot1); - Number[] numSightings = {5, 8, 9, 2, 5}; - - // an array of years in milliseconds: - Number[] years = { - 978307200, // 2001 - 1009843200, // 2002 - 1041379200, // 2003 - 1072915200, // 2004 - 1104537600 // 2005 - }; - // create our series from our array of nums: - XYSeries series2 = new SimpleXYSeries( - Arrays.asList(years), - Arrays.asList(numSightings), - "Sightings in USA"); - - plot1.getGraphWidget().getGridBackgroundPaint().setColor(Color.WHITE); - plot1.getGraphWidget().getDomainGridLinePaint().setColor(Color.BLACK); - plot1.getGraphWidget().getDomainGridLinePaint(). - setPathEffect(new DashPathEffect(new float[]{1, 1}, 1)); - plot1.getGraphWidget().getRangeGridLinePaint().setColor(Color.BLACK); - plot1.getGraphWidget().getRangeGridLinePaint(). - setPathEffect(new DashPathEffect(new float[]{1, 1}, 1)); - plot1.getGraphWidget().getDomainOriginLinePaint().setColor(Color.BLACK); - plot1.getGraphWidget().getRangeOriginLinePaint().setColor(Color.BLACK); - // Create a getFormatter to use for drawing a series using LineAndPointRenderer: - LineAndPointFormatter series1Format = new LineAndPointFormatter( - Color.rgb(0, 100, 0), // line color - Color.rgb(0, 100, 0), // point color - Color.rgb(100, 200, 0), null); // fill color + // 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() + }; + addSeries(savedInstanceState); - // setup our line fill paint to be a slightly transparent gradient: - Paint lineFill = new Paint(); - lineFill.setAlpha(200); + plot1.setRangeBoundaries(0, 10, BoundaryMode.FIXED); - // ugly usage of LinearGradient. unfortunately there's no way to determine the actual size of - // a View from within onCreate. one alternative is to specify a dimension in resources - // and use that accordingly. at least then the values can be customized for the device type and orientation. - lineFill.setShader(new LinearGradient(0, 0, 200, 200, Color.WHITE, Color.GREEN, Shader.TileMode.CLAMP)); + plot1.getGraph().getGridBackgroundPaint().setColor(Color.WHITE); + plot1.getGraph().getDomainGridLinePaint().setColor(Color.BLACK); + plot1.getGraph().getDomainGridLinePaint(). + 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)); + plot1.getGraph().getDomainOriginLinePaint().setColor(Color.BLACK); + plot1.getGraph().getRangeOriginLinePaint().setColor(Color.BLACK); - LineAndPointFormatter formatter = - new LineAndPointFormatter(Color.rgb(0, 0,0), Color.BLUE, Color.RED, null); - formatter.setFillPaint(lineFill); - plot1.getGraphWidget().setPaddingRight(2); - plot1.addSeries(series2, formatter); + plot1.getGraph().setPaddingRight(2); // draw a domain tick for each year: - plot1.setDomainStep(XYStepMode.SUBDIVIDE, years.length); + plot1.setDomainStep(StepMode.SUBDIVIDE, years.length); // customize our domain/range labels plot1.setDomainLabel("Year"); plot1.setRangeLabel("# of Sightings"); // get rid of decimal points in our range labels: - plot1.setRangeValueFormat(new DecimalFormat("0")); + plot1.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT). + setFormat(new DecimalFormat("0.0")); + + plot1.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM). + setFormat(new Format() { + + // 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. + @SuppressLint("SimpleDateFormat") + private final SimpleDateFormat dateFormat = new SimpleDateFormat("MMM yyyy"); + + @Override + 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, @NonNull ParsePosition pos) { + return null; + + } + }); + } - plot1.setDomainValueFormat(new Format() { + /** + * 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 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("yyyy"); + // create our series from our array of nums: + series = new SimpleXYSeries(Arrays.asList(yVals), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, SERIES_TITLE); - @Override - public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + 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)); - // 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); - } + // setup our line fill paint to be a slightly transparent gradient: + Paint lineFill = new Paint(); + lineFill.setAlpha(200); - @Override - public Object parseObject(String source, ParsePosition pos) { - return null; + formatter.setFillPaint(lineFill); - } - }); + plot1.addSeries(series, formatter); + } - // by default, AndroidPlot displays developer guides to aid in laying out your plot. - // To get rid of them call disableAllMarkup(): - //plot1.disableAllMarkup(); + @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 9647bde9..dfae19d9 100644 --- a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java @@ -19,188 +19,168 @@ import java.text.DecimalFormat; import java.util.Random; -import android.app.Activity; +import android.app.*; import android.graphics.Color; -import android.graphics.PointF; -import android.os.Bundle; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnTouchListener; -import android.widget.Button; +import android.os.*; +import android.view.*; +import android.widget.*; import com.androidplot.Plot; -import com.androidplot.xy.BoundaryMode; -import com.androidplot.xy.LineAndPointFormatter; -import com.androidplot.xy.SimpleXYSeries; -import com.androidplot.xy.XYPlot; - -/*********************************** - * @author David Buezas (david.buezas at gmail.com) - * Feel free to copy, modify and use the source as it fits you. - * 09/27/2012 nfellows - updated for 0.5.1 and made a few simplifications - */ -public class TouchZoomExampleActivity extends Activity implements OnTouchListener { - private static final int SERIES_SIZE = 200; - private XYPlot mySimpleXYPlot; +import com.androidplot.xy.*; + +public class TouchZoomExampleActivity extends Activity { + 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; - private SimpleXYSeries[] series = null; - private PointF minXY; - private PointF maxXY; + private Spinner panSpinner; + private Spinner zoomSpinner; 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) { - minXY.x = series[0].getX(0).floatValue(); - maxXY.x = series[3].getX(series[3].size() - 1).floatValue(); - mySimpleXYPlot.setDomainBoundaries(minXY.x, maxXY.x, BoundaryMode.FIXED); - - // pre 0.5.1 users should use postRedraw() instead. - mySimpleXYPlot.redraw(); + reset(); } }); - mySimpleXYPlot = (XYPlot) findViewById(R.id.plot); - mySimpleXYPlot.setOnTouchListener(this); - mySimpleXYPlot.getGraphWidget().setTicksPerRangeLabel(2); - mySimpleXYPlot.getGraphWidget().setTicksPerDomainLabel(2); - mySimpleXYPlot.getGraphWidget().getBackgroundPaint().setColor(Color.TRANSPARENT); - mySimpleXYPlot.getGraphWidget().setRangeValueFormat( - new DecimalFormat("#####")); - mySimpleXYPlot.getGraphWidget().setDomainValueFormat( - new DecimalFormat("#####.#")); - mySimpleXYPlot.getGraphWidget().setRangeTickLabelWidth(25); - mySimpleXYPlot.setRangeLabel(""); - mySimpleXYPlot.setDomainLabel(""); - - mySimpleXYPlot.setBorderStyle(Plot.BorderStyle.NONE, null, null); - //mySimpleXYPlot.disableAllMarkup(); - series = new SimpleXYSeries[4]; - int scale = 1; - for (int i = 0; i < 4; i++, scale *= 5) { - series[i] = new SimpleXYSeries("S" + i); - populateSeries(series[i], scale); - } - mySimpleXYPlot.addSeries(series[3], - new LineAndPointFormatter(Color.rgb(50, 0, 0), null, - Color.rgb(100, 0, 0), null)); - mySimpleXYPlot.addSeries(series[2], - new LineAndPointFormatter(Color.rgb(50, 50, 0), null, - Color.rgb(100, 100, 0), null)); - mySimpleXYPlot.addSeries(series[1], - new LineAndPointFormatter(Color.rgb(0, 50, 0), null, - Color.rgb(0, 100, 0), null)); - mySimpleXYPlot.addSeries(series[0], - new LineAndPointFormatter(Color.rgb(0, 0, 0), null, - Color.rgb(0, 0, 150), null)); - mySimpleXYPlot.redraw(); - mySimpleXYPlot.calculateMinMaxVals(); - minXY = new PointF(mySimpleXYPlot.getCalculatedMinX().floatValue(), - mySimpleXYPlot.getCalculatedMinY().floatValue()); - maxXY = new PointF(mySimpleXYPlot.getCalculatedMaxX().floatValue(), - mySimpleXYPlot.getCalculatedMaxY().floatValue()); + 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); + + // 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); + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT). + setFormat(new DecimalFormat("#####")); + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM). + setFormat(new DecimalFormat("#####.#")); + + plot.setRangeLabel(""); + plot.setDomainLabel(""); + + plot.setBorderStyle(Plot.BorderStyle.NONE, null, null); + + 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()); + + generateSeriesData(); + reset(); } - private void populateSeries(SimpleXYSeries series, int max) { - Random r = new Random(); - for(int i = 0; i < SERIES_SIZE; i++) { - series.addLast(i, r.nextInt(max)); - } + private void reset() { + plot.setDomainBoundaries(0, 10000, BoundaryMode.FIXED); + plot.setRangeBoundaries(0, 1000, BoundaryMode.FIXED); + plot.redraw(); } - // Definition of the touch states - static final int NONE = 0; - static final int ONE_FINGER_DRAG = 1; - static final int TWO_FINGERS_DRAG = 2; - int mode = NONE; + private ProgressDialog progress; - PointF firstFinger; - float distBetweenFingers; - boolean stopThread = false; + private void generateSeriesData() { + progress = ProgressDialog.show(this, "Loading", "Please wait...", true); + new AsyncTask() { - @Override - public boolean onTouch(View arg0, MotionEvent event) { - switch (event.getAction() & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: // Start gesture - firstFinger = new PointF(event.getX(), event.getY()); - mode = ONE_FINGER_DRAG; - stopThread = true; - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - mode = NONE; - break; - case MotionEvent.ACTION_POINTER_DOWN: // second finger - distBetweenFingers = spacing(event); - // the distance check is done to avoid false alarms - if (distBetweenFingers > 5f) { - mode = TWO_FINGERS_DRAG; - } - break; - case MotionEvent.ACTION_MOVE: - if (mode == ONE_FINGER_DRAG) { - PointF oldFirstFinger = firstFinger; - firstFinger = new PointF(event.getX(), event.getY()); - scroll(oldFirstFinger.x - firstFinger.x); - mySimpleXYPlot.setDomainBoundaries(minXY.x, maxXY.x, - BoundaryMode.FIXED); - mySimpleXYPlot.redraw(); - - } else if (mode == TWO_FINGERS_DRAG) { - float oldDist = distBetweenFingers; - distBetweenFingers = spacing(event); - zoom(oldDist / distBetweenFingers); - mySimpleXYPlot.setDomainBoundaries(minXY.x, maxXY.x, - BoundaryMode.FIXED); - mySimpleXYPlot.redraw(); - } - break; + @Override + protected Object doInBackground(Object[] objects) { + generateAndAddSeries(625, new LineAndPointFormatter(Color.rgb(50, 0, 0), null, + Color.argb(SERIES_ALPHA, 100, 0, 0), null)); + generateAndAddSeries(125, new LineAndPointFormatter(Color.rgb(50, 50, 0), null, + Color.argb(SERIES_ALPHA, 100, 100, 0), null)); + generateAndAddSeries(25, new LineAndPointFormatter(Color.rgb(0, 50, 0), null, + Color.argb(SERIES_ALPHA, 0, 100, 0), null)); + generateAndAddSeries(5, new LineAndPointFormatter(Color.rgb(0, 0, 0), null, + Color.argb(SERIES_ALPHA, 0, 0, 150), null)); + return null; + } + + @Override + protected void onPostExecute(Object result) { + progress.dismiss(); + plot.redraw(); + } + }.execute(); + } + + private void generateAndAddSeries(int max, LineAndPointFormatter formatter) { + final FixedSizeEditableXYSeries series = new FixedSizeEditableXYSeries("s" + max, SERIES_SIZE); + Random r = new Random(); + for(int i = 0; i < SERIES_SIZE; i++) { + series.setX(i, i); + series.setY(r.nextInt(max), i); } - return true; + + // wrap our series in a SampledXYSeries with a threshold of 1000. + final SampledXYSeries sampledSeries = + new SampledXYSeries(series, OrderedXYSeries.XOrder.ASCENDING, 2,100); + plot.addSeries(sampledSeries, formatter); } - private void zoom(float scale) { - float domainSpan = maxXY.x - minXY.x; - float domainMidPoint = maxXY.x - domainSpan / 2.0f; - float offset = domainSpan * scale / 2.0f; + private void initSpinners() { + panSpinner.setAdapter( + new ArrayAdapter<>(this, R.layout.spinner_item, PanZoom.Pan.values())); + panSpinner.setSelection(panZoom.getPan().ordinal()); + panSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + panZoom.setPan(PanZoom.Pan.values()[position]); + } + + @Override + public void onNothingSelected(AdapterView parent) { + // nothing to do + } + }); - minXY.x = domainMidPoint - offset; - maxXY.x = domainMidPoint + offset; - minXY.x = Math.min(minXY.x, series[3].getX(series[3].size() - 3) - .floatValue()); - maxXY.x = Math.max(maxXY.x, series[0].getX(1).floatValue()); - clampToDomainBounds(domainSpan); - } + zoomSpinner.setAdapter( + new ArrayAdapter<>(this, R.layout.spinner_item, PanZoom.Zoom.values())); + zoomSpinner.setSelection(panZoom.getZoom().ordinal()); + zoomSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + panZoom.setZoom(PanZoom.Zoom.values()[position]); + } - private void scroll(float pan) { - float domainSpan = maxXY.x - minXY.x; - float step = domainSpan / mySimpleXYPlot.getWidth(); - float offset = pan * step; - minXY.x = minXY.x + offset; - maxXY.x = maxXY.x + offset; - clampToDomainBounds(domainSpan); + @Override + public void onNothingSelected(AdapterView parent) { + // nothing to do + } + }); } - private void clampToDomainBounds(float domainSpan) { - float leftBoundary = series[0].getX(0).floatValue(); - float rightBoundary = series[3].getX(series[3].size() - 1).floatValue(); - // enforce left scroll boundary: - if (minXY.x < leftBoundary) { - minXY.x = leftBoundary; - maxXY.x = leftBoundary + domainSpan; - } else if (maxXY.x > series[3].getX(series[3].size() - 1).floatValue()) { - maxXY.x = rightBoundary; - minXY.x = rightBoundary - domainSpan; - } + // (optional) save the current pan/zoom state + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putSerializable("pan-zoom-state", panZoom.getState()); } - private float spacing(MotionEvent event) { - float x = event.getX(0) - event.getX(1); - float y = event.getY(0) - event.getY(1); - return (float) Math.hypot(x, y); + // (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/Util.java b/demoapp/src/main/java/com/androidplot/demos/Util.java new file mode 100644 index 00000000..617f3976 --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/Util.java @@ -0,0 +1,17 @@ +package com.androidplot.demos; + +import android.app.*; +import android.content.*; + +/** + * Created by halfhp on 10/29/16. + */ +public class Util { + + + + public ProgressDialog showLoadingDialog(Context context) { + return ProgressDialog.show(context, "Loading", "Please wait...", true); + } + +} diff --git a/demoapp/src/main/java/com/androidplot/demos/XYPlotWithBgImgActivity.java b/demoapp/src/main/java/com/androidplot/demos/XYPlotWithBgImgActivity.java index 503cb372..e1dd432d 100644 --- a/demoapp/src/main/java/com/androidplot/demos/XYPlotWithBgImgActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/XYPlotWithBgImgActivity.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.view.View; import android.widget.ToggleButton; +import com.androidplot.util.PixelUtils; import com.androidplot.xy.*; import java.text.DecimalFormat; @@ -42,38 +43,37 @@ protected void onCreate(Bundle savedInstanceState) { plot = (XYPlot) findViewById(R.id.graph_metrics); - //For debugging. - //plot.setMarkupEnabled(true); - // Format Graph - plot.getGraphWidget().getBackgroundPaint().setColor(Color.TRANSPARENT); - plot.getGraphWidget().getGridBackgroundPaint().setShader(WHITE_SHADER); - plot.getGraphWidget().getDomainGridLinePaint().setColor(Color.BLACK); - plot.getGraphWidget().getDomainGridLinePaint().setPathEffect(new DashPathEffect(new float[]{3, 3}, 1)); - plot.getGraphWidget().getRangeGridLinePaint().setColor(Color.BLACK); - plot.getGraphWidget().getRangeGridLinePaint().setPathEffect(new DashPathEffect(new float[]{3, 3}, 1)); - plot.getGraphWidget().getDomainOriginLinePaint().setColor(Color.BLACK); - plot.getGraphWidget().getRangeOriginLinePaint().setColor(Color.BLACK); - //plot.getGraphWidget().setMarginTop(10); + plot.getGraph().getBackgroundPaint().setColor(Color.TRANSPARENT); + plot.getGraph().getGridBackgroundPaint().setShader(WHITE_SHADER); + plot.getGraph().getDomainGridLinePaint().setColor(Color.BLACK); + plot.getGraph().getDomainGridLinePaint().setPathEffect(new DashPathEffect(new float[]{3, 3}, 1)); + plot.getGraph().getRangeGridLinePaint().setColor(Color.BLACK); + plot.getGraph().getRangeGridLinePaint().setPathEffect(new DashPathEffect(new float[]{3, 3}, 1)); + plot.getGraph().getDomainOriginLinePaint().setColor(Color.BLACK); + plot.getGraph().getRangeOriginLinePaint().setColor(Color.BLACK); // Customize domain and range labels. plot.setDomainLabel("x-vals"); plot.setRangeLabel("y-vals"); - plot.setRangeValueFormat(new DecimalFormat("0")); + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT). + setFormat(new DecimalFormat("0")); // Make the domain and range step correctly plot.setRangeBoundaries(40, 160, BoundaryMode.FIXED); - plot.setRangeStep(XYStepMode.INCREMENT_BY_VAL, 20); - plot.setDomainStep(XYStepMode.INCREMENT_BY_VAL, 60); - plot.setTicksPerDomainLabel(2); + plot.setRangeStep(StepMode.INCREMENT_BY_VAL, 20); + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 60); + plot.setLinesPerDomainLabel(2); - series = (SimpleXYSeries) getSeries(); + series = getSeries(); LineAndPointFormatter lpFormat = new LineAndPointFormatter( Color.BLACK, - Color.BLACK, + Color.GRAY, null, // No fill new PointLabelFormatter(Color.TRANSPARENT) // Don't show text at points ); + lpFormat.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(3)); + lpFormat.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(6)); plot.addSeries(series, lpFormat); plot.redraw(); } @@ -98,30 +98,7 @@ private SimpleXYSeries getSeries() { public void onGraphStyleToggle(View v) { boolean styleOn = ((ToggleButton) v).isChecked(); - - /*RectF graphRect = plot.getGraphWidget().getGridRect(); - float segmentSize = 1.0f/6.0f; - LinearGradient lg = new LinearGradient( - 0, - graphRect.top, - 0, - graphRect.bottom, - new int[]{ - Color.RED, - Color.YELLOW, - Color.GREEN, - Color.WHITE}, - new float[]{ - 0, - segmentSize*2, - segmentSize*3, - segmentSize*5 - }, - Shader.TileMode.REPEAT - ); - plot.getGraphWidget().getGridBackgroundPaint().setShader(lg);*/ - - RectF rect = plot.getGraphWidget().getGridDimensions().marginatedRect; + RectF rect = plot.getGraph().getGridRect(); BitmapShader myShader = new BitmapShader( Bitmap.createScaledBitmap( BitmapFactory.decodeResource( @@ -136,10 +113,10 @@ public void onGraphStyleToggle(View v) { m.setTranslate(rect.left, rect.top); myShader.setLocalMatrix(m); if (styleOn) - plot.getGraphWidget().getGridBackgroundPaint().setShader( + plot.getGraph().getGridBackgroundPaint().setShader( myShader); else - plot.getGraphWidget().getGridBackgroundPaint().setShader(WHITE_SHADER); + plot.getGraph().getGridBackgroundPaint().setShader(WHITE_SHADER); 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 630a6f51..0f48c3c2 100644 --- a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java @@ -19,10 +19,10 @@ import android.app.Activity; import android.graphics.Color; import android.graphics.DashPathEffect; -import android.graphics.Paint; import android.os.Bundle; import android.widget.CheckBox; import android.widget.CompoundButton; + import com.androidplot.util.PixelUtils; import com.androidplot.xy.XYSeries; import com.androidplot.ui.*; @@ -40,37 +40,27 @@ public class XYRegionExampleActivity extends Activity { private static final float HOME_RUN_DIST = 325; - private static final int LINE_THICKNESS_DP = 2; - private static final int POINT_SIZE_DP = 6; + private static final int LINE_THICKNESS_DP = 4; + private static final int POINT_SIZE_DP = 7; private XYPlot plot; - private final Number[] timHits = {105, 252, 220, 350, 12, 250, 353}; - private final Number[] nickHits = {110, 191, 61, 371, 289, 101, 10}; - private final Number[] joeHits = {25, 375, 364, 128, 178, 289, 346}; - private final Number[] jamesHits = {250, 285, 295, 211, 311, 365, 241}; + private final Number[] timHits = {105, 252, 216, 10, 301, 320, 220, 350, 12, 250, 353}; + private final Number[] nickHits = {110, 191, 61, 300, 205, 40, 224, 371, 289, 101, 10}; private LineAndPointFormatter timFormatter; private LineAndPointFormatter nickFormatter; - private LineAndPointFormatter joeFormatter; - private LineAndPointFormatter jamesFormatter; private XYSeries timSeries; private XYSeries nickSeries; - private XYSeries joeSeries; - private XYSeries jamesSeries; private RectRegion shortRegion; private RectRegion warmupRegion; private RectRegion homeRunRegion; - //private XYRegionFormatter rf1; private XYRegionFormatter shortRegionFormatter; private XYRegionFormatter warmupRegionFormatter; private XYRegionFormatter homeRunRegionFormatter; - //private XYRegionFormatter rf5; private CheckBox timCB; private CheckBox nickCB; - private CheckBox joeCB; - private CheckBox jamesCB; private CheckBox r2CheckBox; private CheckBox r3CheckBox; @@ -79,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) { @@ -88,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) { @@ -96,25 +86,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { } }); - joeCB = (CheckBox) findViewById(R.id.s3CheckBox); - joeCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - onS3CheckBoxClicked(); - } - }); - - jamesCB = (CheckBox) findViewById(R.id.s4CheckBox); - jamesCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - onS4CheckBoxClicked(); - } - }); - - - - r2CheckBox = (CheckBox) findViewById(R.id.r2CheckBox); + r2CheckBox = findViewById(R.id.r2CheckBox); r2CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { @@ -122,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) { @@ -130,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) { @@ -146,7 +118,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { } private void onS1CheckBoxClicked() { - if(timCB.isChecked()) { + if (timCB.isChecked()) { plot.addSeries(timSeries, timFormatter); r2CheckBox.setEnabled(true); } else { @@ -157,7 +129,7 @@ private void onS1CheckBoxClicked() { } private void onS2CheckBoxClicked() { - if(nickCB.isChecked()) { + if (nickCB.isChecked()) { plot.addSeries(nickSeries, nickFormatter); r3CheckBox.setEnabled(true); r4CheckBox.setEnabled(true); @@ -169,34 +141,16 @@ private void onS2CheckBoxClicked() { plot.redraw(); } - private void onS3CheckBoxClicked() { - if(joeCB.isChecked()) { - plot.addSeries(joeSeries, joeFormatter); - } else { - plot.removeSeries(joeSeries); - } - plot.redraw(); - } - - private void onS4CheckBoxClicked() { - if(jamesCB.isChecked()) { - plot.addSeries(jamesSeries, jamesFormatter); - } else { - plot.removeSeries(jamesSeries); - } - plot.redraw(); - } - /** - * Processes a check box event + * Processes a run box event * @param cb The checkbox event origin * @param lpf LineAndPointFormatter with which rr and rf are to be added/removed * @param rf The XYRegionFormatter with which rr should be rendered * @param rr The RectRegion to add/remove */ private void onCheckBoxClicked(CheckBox cb, LineAndPointFormatter lpf, - XYRegionFormatter rf, RectRegion rr) { - if(cb.isChecked()) { + XYRegionFormatter rf, RectRegion rr) { + if (cb.isChecked()) { lpf.removeRegion(rr); } else { lpf.addRegion(rr, rf); @@ -208,68 +162,42 @@ private void onCheckBoxClicked(CheckBox cb, LineAndPointFormatter lpf, */ private void makePlotPretty() { // use a 2x5 grid with room for 10 items: - plot.getLegendWidget().setTableModel(new DynamicTableModel(4, 2)); - - // add a semi-transparent black background to the legend - // so it's easier to see overlaid on top of our plot: - Paint bgPaint = new Paint(); - bgPaint.setColor(Color.BLACK); - bgPaint.setStyle(Paint.Style.FILL); - bgPaint.setAlpha(40); - - plot.getLegendWidget().setBackgroundPaint(bgPaint); - - // adjust the padding of the legend widget to look a little nicer: - plot.getLegendWidget().setPadding(5, 5, 5, 5); - - plot.setRangeValueFormat(new NumberFormat() { - @Override - public StringBuffer format(double value, StringBuffer buffer, FieldPosition field) { - return new StringBuffer(value + "'"); - } - - @Override - public StringBuffer format(long value, StringBuffer buffer, FieldPosition field) { - throw new UnsupportedOperationException("Not yet implemented."); - } - - @Override - public Number parse(String string, ParsePosition position) { - throw new UnsupportedOperationException("Not yet implemented."); - } - }); - - plot.setDomainValueFormat(new DecimalFormat("#")); - - plot.getLegendWidget().setWidth(PixelUtils.dpToPix(100), SizeLayoutType.FILL); - - - // adjust the legend size so there is enough room - // to draw the new legend grid: - //plot.getLegendWidget().getHeight().setLayoutType(SizeLayoutType.ABSOLUTE); - //plot.getLegendWidget().getWidth().setLayoutType(SizeLayoutType.ABSOLUTE); - //plot.getLegendWidget().setSize( - // new SizeMetrics(70, SizeLayoutType.ABSOLUTE, 80, SizeLayoutType.ABSOLUTE)); + plot.getLegend().setTableModel(new DynamicTableModel(4, 2)); + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT) + .setFormat(new NumberFormat() { + @Override + public StringBuffer format(double value, StringBuffer buffer, + FieldPosition field) { + return new StringBuffer(value + "'"); + } + + @Override + public StringBuffer format(long value, StringBuffer buffer, + FieldPosition field) { + throw new UnsupportedOperationException("Not yet implemented."); + } + + @Override + public Number parse(String string, ParsePosition position) { + throw new UnsupportedOperationException("Not yet implemented."); + } + }); + + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM) + .setFormat(new DecimalFormat("#")); + + plot.getGraph().setDomainGridLinePaint(null); + + plot.getLegend().setWidth(PixelUtils.dpToPix(100), SizeMode.FILL); // reposition the grid so that it rests above the bottom-left // edge of the graph widget: - - plot.getLegendWidget().position( - 125, - XLayoutStyle.ABSOLUTE_FROM_LEFT, - 65, - YLayoutStyle.ABSOLUTE_FROM_TOP, - AnchorPosition.LEFT_TOP); - - plot.getGraphWidget().setRangeTickLabelHorizontalOffset(-1); - - // add enough space to ensure range value labels arent cut off on the left/right: - plot.getGraphWidget().setRangeTickLabelWidth(25); - - // add enough space to make sure domain value labels arent cut off on the bottom: - plot.getGraphWidget().setDomainTickLabelWidth(15); - - plot.getGraphWidget().setDomainTickLabelVerticalOffset(-6); + plot.getLegend().position( + 50, + HorizontalPositioning.ABSOLUTE_FROM_CENTER, + 200, + VerticalPositioning.ABSOLUTE_FROM_TOP, + Anchor.TOP_MIDDLE); plot.setRangeBoundaries(0, BoundaryMode.FIXED, 500, BoundaryMode.FIXED); } @@ -280,64 +208,32 @@ public Number parse(String string, ParsePosition position) { */ private void seriesSetup() { - // TIM - timFormatter = new LineAndPointFormatter( - Color.rgb(100, 25, 20), - Color.rgb(100, 25, 20), - null, null); + timFormatter = new LineAndPointFormatter(Color.YELLOW, Color.YELLOW, null, null); timFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(LINE_THICKNESS_DP)); timFormatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(POINT_SIZE_DP)); + timFormatter.setInterpolationParams(new CatmullRomInterpolator.Params(8, + CatmullRomInterpolator.Type.Centripetal)); timSeries = new SimpleXYSeries(Arrays.asList(timHits), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Tim"); + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Tim"); plot.addSeries(timSeries, timFormatter); // SERIES #2: - nickFormatter = new LineAndPointFormatter( - Color.rgb(100, 25, 200), - Color.rgb(100, 25, 200), - null, null); + nickFormatter = new LineAndPointFormatter(Color.BLUE, Color.BLUE, null, null); nickFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(LINE_THICKNESS_DP)); nickFormatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(POINT_SIZE_DP)); - - + nickFormatter.setInterpolationParams(new CatmullRomInterpolator.Params(8, + CatmullRomInterpolator.Type.Centripetal)); nickSeries = new SimpleXYSeries(Arrays.asList(nickHits), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Nick"); + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Nick"); plot.addSeries(nickSeries, nickFormatter); - // SERIES #3: - joeFormatter = new LineAndPointFormatter( - Color.rgb(200, 25, 200), - Color.rgb(200, 25, 200), - null, null); - joeFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(LINE_THICKNESS_DP)); - joeFormatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(POINT_SIZE_DP)); - - joeSeries = new SimpleXYSeries(Arrays.asList(joeHits), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Joe"); - - plot.addSeries(joeSeries, joeFormatter); - - // SERIES #4: - jamesFormatter = new LineAndPointFormatter( - Color.rgb(220, 25, 20), - Color.rgb(220, 25, 20), - null, null); - - jamesFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(LINE_THICKNESS_DP)); - jamesFormatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(POINT_SIZE_DP)); - - jamesSeries = new SimpleXYSeries(Arrays.asList(jamesHits), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY,"James"); - plot.addSeries(jamesSeries, jamesFormatter); - - plot.setRangeStep(XYStepMode.INCREMENT_BY_VAL, 100); - //plot.setTicksPerRangeLabel(1); - plot.setDomainStep(XYStepMode.INCREMENT_BY_VAL, 1); + plot.setRangeStep(StepMode.INCREMENT_BY_VAL, 100); + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1); } /** @@ -345,15 +241,16 @@ private void seriesSetup() { */ private void axisLabelSetup() { // DOMAIN - plot.getGraphWidget().addDomainAxisValueLabelRegion( - Double.NEGATIVE_INFINITY, 2, new AxisValueLabelFormatter(Color.GRAY)); - plot.getGraphWidget().addDomainAxisValueLabelRegion( - 2, Double.POSITIVE_INFINITY, new AxisValueLabelFormatter(Color.WHITE)); - // RANGE - plot.getGraphWidget().addRangeAxisValueLabelRegion( - Double.NEGATIVE_INFINITY, HOME_RUN_DIST, new AxisValueLabelFormatter(Color.RED)); - plot.getGraphWidget().addRangeAxisValueLabelRegion( - HOME_RUN_DIST, Double.POSITIVE_INFINITY, new AxisValueLabelFormatter(Color.GREEN)); + // TODO + // plot.getGraphWidget().addDomainLineLabelFormatter( + // Double.NEGATIVE_INFINITY, 2, new SimpleLineLabelFormatter(Color.GRAY)); + // plot.getGraphWidget().addDomainLineLabelFormatter( + // 2, Double.POSITIVE_INFINITY, new SimpleLineLabelFormatter(Color.WHITE)); + // // RANGE + // plot.getGraphWidget().addRangeLineLabelFormatter( + // Double.NEGATIVE_INFINITY, HOME_RUN_DIST, new SimpleLineLabelFormatter(Color.RED)); + // plot.getGraphWidget().addRangeLineLabelFormatter( + // HOME_RUN_DIST, Double.POSITIVE_INFINITY, new SimpleLineLabelFormatter(Color.GREEN)); } /** @@ -364,27 +261,28 @@ private void markerSetup() { YValueMarker fenwayLfMarker = new YValueMarker( 380, // y-val to mark "Fenway Park LF Wall", // marker label - new XPositionMetric( // object instance to set text positioning on the marker + new HorizontalPosition( + // object instance to set text positioning on the marker PixelUtils.dpToPix(5), // 5dp offset - XLayoutStyle.ABSOLUTE_FROM_RIGHT), // offset origin + HorizontalPositioning.ABSOLUTE_FROM_RIGHT), // offset origin Color.BLUE, // line paint color Color.BLUE); // text paint color YValueMarker attRfMarker = new YValueMarker( - 309, // y-val to mark - "ATT Park RF Wall", // marker label - new XPositionMetric( // object instance to set text positioning on the marker - PixelUtils.dpToPix(5), // 5dp offset - XLayoutStyle.ABSOLUTE_FROM_RIGHT), // offset origin - Color.CYAN, // line paint color - Color.CYAN); // text paint color - + 309, // y-val to mark + "ATT Park RF Wall", // marker label + new HorizontalPosition( + // object instance to set text positioning on the marker + PixelUtils.dpToPix(5), // 5dp offset + HorizontalPositioning.ABSOLUTE_FROM_RIGHT), // offset origin + Color.CYAN, // line paint color + Color.CYAN); // text paint color fenwayLfMarker.getTextPaint().setTextSize(PixelUtils.dpToPix(14)); attRfMarker.getTextPaint().setTextSize(PixelUtils.dpToPix(14)); DashPathEffect dpe = new DashPathEffect( - new float[]{PixelUtils.dpToPix(2), PixelUtils.dpToPix(2)}, 0); + new float[] {PixelUtils.dpToPix(2), PixelUtils.dpToPix(2)}, 0); fenwayLfMarker.getLinePaint().setPathEffect(dpe); attRfMarker.getLinePaint().setPathEffect(dpe); @@ -398,36 +296,33 @@ private void markerSetup() { */ private void regionSetup() { - // and another region: shortRegionFormatter = new XYRegionFormatter(Color.RED); - shortRegionFormatter.getPaint().setAlpha(75); - shortRegion = new RectRegion(2, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, HOME_RUN_DIST, "Short"); + shortRegionFormatter.getPaint().setAlpha(60); + shortRegion = new RectRegion( + 2, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, HOME_RUN_DIST, "Short"); + timFormatter.addRegion(shortRegion, shortRegionFormatter); nickFormatter.addRegion(shortRegion, shortRegionFormatter); - joeFormatter.addRegion(shortRegion, shortRegionFormatter); - jamesFormatter.addRegion(shortRegion, shortRegionFormatter); // the next three regions are horizontal regions with minY/maxY // set to negative and positive infinity respectively. - warmupRegionFormatter = new XYRegionFormatter(Color.WHITE); - warmupRegionFormatter.getPaint().setAlpha(75); + warmupRegionFormatter = new XYRegionFormatter(Color.LTGRAY); + warmupRegionFormatter.getPaint().setAlpha(60); + + warmupRegion = new RectRegion( + 0, 2, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, "Warmup"); - warmupRegion = new RectRegion(0, 2, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, "Warmup"); timFormatter.addRegion(warmupRegion, warmupRegionFormatter); nickFormatter.addRegion(warmupRegion, warmupRegionFormatter); - joeFormatter.addRegion(warmupRegion, warmupRegionFormatter); - jamesFormatter.addRegion(warmupRegion, warmupRegionFormatter); homeRunRegionFormatter = new XYRegionFormatter(Color.GREEN); - homeRunRegionFormatter.getPaint().setAlpha(75); + homeRunRegionFormatter.getPaint().setAlpha(60); - homeRunRegion = new RectRegion(2, Double.POSITIVE_INFINITY, HOME_RUN_DIST, Double.POSITIVE_INFINITY, "H. Run"); + homeRunRegion = new RectRegion( + 2, Double.POSITIVE_INFINITY, HOME_RUN_DIST, Double.POSITIVE_INFINITY, "H. Run"); timFormatter.addRegion(homeRunRegion, homeRunRegionFormatter); nickFormatter.addRegion(homeRunRegion, homeRunRegionFormatter); - joeFormatter.addRegion(homeRunRegion, homeRunRegionFormatter); - jamesFormatter.addRegion(homeRunRegion, homeRunRegionFormatter); - nickFormatter.setFillDirection(FillDirection.RANGE_ORIGIN); } } \ No newline at end of file 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 082035a7..592fbce3 100644 --- a/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java +++ b/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java @@ -26,6 +26,7 @@ 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; @@ -42,35 +43,28 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a final int h = (int) context.getResources().getDimension(R.dimen.sample_widget_height); final int w = (int) context.getResources().getDimension(R.dimen.sample_widget_width); - plot.getGraphWidget().setMargins(0, 0, 0 , 0); - plot.getGraphWidget().setPadding(0, 0, 0, 0); - plot.getGraphWidget().getGridBox().setPadding(20, 25, 0, 20); - plot.getGraphWidget().getDomainTickLabelPaint().setTextSize(PixelUtils.spToPix(10)); - plot.getGraphWidget().getRangeTickLabelPaint().setTextSize(PixelUtils.spToPix(10)); - plot.getGraphWidget().getDomainOriginTickLabelPaint().setTextSize(PixelUtils.spToPix(10)); - plot.getGraphWidget().getRangeOriginTickLabelPaint().setTextSize(PixelUtils.spToPix(10)); + plot.getGraph().setMargins(0, 0, 0 , 0); + plot.getGraph().setPadding(0, 0, 0, 0); - plot.getGraphWidget().position(0, XLayoutStyle.ABSOLUTE_FROM_LEFT, 0, - YLayoutStyle.ABSOLUTE_FROM_TOP, AnchorPosition.LEFT_TOP); + plot.getGraph().position(0, HorizontalPositioning.ABSOLUTE_FROM_LEFT, 0, + VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.LEFT_TOP); - plot.getGraphWidget().setSize(Size.FILL); + plot.getGraph().setSize(Size.FILL); - plot.getLayoutManager().moveToTop(plot.getTitleWidget()); + 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); - /*plot.getGraphWidget().setMarginBottom(PixelUtils.dpToPix(40)); - plot.getGraphWidget().setMarginLeft(PixelUtils.dpToPix(80)); - plot.getGraphWidget().setPaddingLeft(PixelUtils.dpToPix(80)); - plot.setPlotMargins(0, 0, 0, 0); - plot.getGraphWidget().position(0, XLayoutStyle.ABSOLUTE_FROM_LEFT, 0, - YLayoutStyle.ABSOLUTE_FROM_TOP, AnchorPosition.LEFT_TOP); - - plot.getGraphWidget().setSize(Size.FILL);*/ - // Create a couple arrays of y-values to plot: - //Number[] series1Numbers = {1, 8, 5, 2, 7, 4}; - //Number[] series2Numbers = {4, 6, 3, 8, 2, 10}; Number[] series1Numbers = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64}; Number[] series2Numbers = {5, 2, 10, 5, 20, 10, 40, 20, 80, 40}; @@ -100,11 +94,11 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a // reduce the number of range labels - plot.setTicksPerRangeLabel(3); - plot.setTicksPerDomainLabel(2); + plot.setLinesPerRangeLabel(3); + plot.setLinesPerDomainLabel(2); // hide the legend: - plot.getLegendWidget().setVisible(false); + plot.getLegend().setVisible(false); RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.demo_app_widget); diff --git a/demoapp/src/main/res/layout/bar_plot_example.xml b/demoapp/src/main/res/layout/bar_plot_example.xml index 77ef4a72..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"> + android:id="@+id/plot" + style="@style/APDefacto.Light" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_weight="1" + ap:gridClippingEnabled="false" + ap:gridInsetLeft="35dp" + ap:gridInsetRight="10dp" + ap:title="Growth" /> + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:orientation="horizontal"> + + android:id="@+id/spRenderStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + android:id="@+id/spSeriesSize" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:orientation="horizontal"> + + android:id="@+id/spWidthStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + android:id="@+id/sectionGraph" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + android:id="@+id/sbFixed" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:max="300" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:progress="10" /> + + android:id="@+id/sbVariable" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:max="50" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:progress="1" /> + + + - - + android:checked="true" + android:text="Series 1" /> + + \ No newline at end of file diff --git a/demoapp/src/main/res/layout/bubble_chart_example.xml b/demoapp/src/main/res/layout/bubble_chart_example.xml new file mode 100644 index 00000000..e18289aa --- /dev/null +++ b/demoapp/src/main/res/layout/bubble_chart_example.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/demoapp/src/main/res/layout/candlestick_example.xml b/demoapp/src/main/res/layout/candlestick_example.xml new file mode 100644 index 00000000..831f003b --- /dev/null +++ b/demoapp/src/main/res/layout/candlestick_example.xml @@ -0,0 +1,39 @@ + + + + + + + \ 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/dual_scale_xy_plot_example.xml b/demoapp/src/main/res/layout/dual_scale_xy_plot_example.xml deleted file mode 100644 index d2d7f3a9..00000000 --- a/demoapp/src/main/res/layout/dual_scale_xy_plot_example.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - -