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 ef59e858..9617a26e 100644 --- a/README.md +++ b/README.md @@ -1,51 +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 -* (NEW) Candlestick 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.8' -} -``` +# 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/Bounds.java b/androidplot-core/src/main/java/com/androidplot/Bounds.java deleted file mode 100644 index f91c0003..00000000 --- a/androidplot-core/src/main/java/com/androidplot/Bounds.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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; - -/** - * Defines simple min/max boundary. Differs from {@link LineRegion} in that it accepts null values. - * @since 0.9.7 - */ -public class Bounds { - - private Number min; - private Number max; - - public Bounds() { - this(null, null); - } - - public Bounds(Number min, Number max) { - this.min = min; - this.max = max; - } - - public Number getMin() { - return min; - } - - public void setMin(Number min) { - this.min = min; - } - - public Number getMax() { - return max; - } - - public void setMax(Number max) { - this.max = max; - } - - /** - * 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 - * @param input - */ - public void union(Bounds input) { - if(this.min == null || input.min != null && - input.min.doubleValue() < this.min.doubleValue()) { - this.min = input.min; - } - if(this.max == null || input.max != null && input.max.doubleValue() > - this.max.doubleValue()) { - this.max = input.max; - } - } - - /** - * Inverse of {@link #union(Bounds)}. - * @param input - */ - public void intersect(Bounds input) { - // TODO - throw new UnsupportedOperationException("Not yet implemented."); - } -} diff --git a/androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java b/androidplot-core/src/main/java/com/androidplot/LineLabelFormatter.java similarity index 64% rename from androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java rename to androidplot-core/src/main/java/com/androidplot/LineLabelFormatter.java index 62a6e6d9..ec92556b 100644 --- a/androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java +++ b/androidplot-core/src/main/java/com/androidplot/LineLabelFormatter.java @@ -1,23 +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.exception; - -public class PlotRenderException extends Exception { - public PlotRenderException(String message) { - super(message); - } -} +/* + * 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 b656785d..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 v1, Number v2) { - if (v1 != null && v2 != null && v1.doubleValue() < v2.doubleValue()) { - this.setMinVal(v1); - this.setMaxVal(v2); - } else { - this.setMinVal(v2); - this.setMaxVal(v1); - } - } - - /** - * - * @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 LineRegion(v1, v2).length(); - } - - public Number length() { - return maxVal == null || minVal == null ? null : 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) { - - // 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 8741a2fd..63d2e831 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -1,1031 +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 SeriesRegistry 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(); - - // 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 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 SeriesRegistry seriesRegistry; - private final ArrayList listeners; - - private Thread renderThread; - private boolean keepRunning = false; - private boolean isIdle = true; - - { - listeners = new ArrayList<>(); - seriesRegistry = new SeriesRegistry<>(); - 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 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 - } - - - 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); - } - - onAfterConfig(); - - layoutManager.onPostInit(); - if (renderMode == RenderMode.USE_BACKGROUND_THREAD) { - renderThread = new Thread(new Runnable() { - @Override - public void run() { - - keepRunning = true; - while (keepRunning) { - isIdle = false; - synchronized (pingPong) { - Canvas c = pingPong.getCanvas(); - renderOnCanvas(c); - pingPong.swap(); - } - synchronized (renderSynch) { - postInvalidate(); - // prevent this thread from becoming an orphan - // after the view is destroyed - if (keepRunning) { - try { - renderSynch.wait(); - } catch (InterruptedException e) { - keepRunning = false; - } - } - } - } - } - }); - } - } - - /** - * 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 - 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); - } - } - - /** - * 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) { - 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 : seriesRegistry) { - 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 : seriesRegistry) { - 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) { - this.displayDims = dims; - layoutManager.layout(displayDims); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - synchronized(renderSynch) { - keepRunning = false; - renderSynch.notify(); - } - } - - - @Override - protected synchronized void onSizeChanged (int w, int h, int oldw, int oldh) { - - // update pixel conversion values - PixelUtils.init(getContext()); - - // disable hardware acceleration if it's not explicitly supported - // by the current Plot implementation. this 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: - default: - canvas.drawRect(dims, paint); - break; - } - } - - /** - * - * @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 index 49d3348a..7a7b3023 100644 --- a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java +++ b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java @@ -17,8 +17,9 @@ package com.androidplot; import com.androidplot.ui.Formatter; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; +import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -27,14 +28,110 @@ * Manages a list of {@link Series} and their associated {@link Formatter} in the context of a {@link Plot}. * @since 0.9.7 */ -public class SeriesRegistry - extends ArrayList> { +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<>(); - for(SeriesAndFormatter sfPair : this) { + 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/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 6fbecd5a..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 : getSeriesAndFormatterList()) { + 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 = getSeriesAndFormatterList(); - 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 = getSeriesAndFormatterList(); + 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 c6c8f6a6..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; @@ -77,7 +82,7 @@ public void setMarkupEnabled(boolean enabled) { setDrawOutlineShadowsEnabled(enabled); } - public void draw(Canvas canvas) throws PlotRenderException { + public void draw(Canvas canvas) { if(isDrawMarginsEnabled()) { drawSpacing(canvas, displayDims.canvasRect, displayDims.marginatedRect, marginPaint); } @@ -85,13 +90,12 @@ public void draw(Canvas canvas) throws PlotRenderException { drawSpacing(canvas, displayDims.marginatedRect, displayDims.paddedRect, paddingPaint); } for (Widget widget : elements()) { - //int canvasState = canvas.save(Canvas.ALL_SAVE_FLAG); // preserve clipping etc try { - canvas.save(Canvas.ALL_SAVE_FLAG); + canvas.save(); PositionMetrics metrics = widget.getPositionMetrics(); float elementWidth = widget.getWidthPix(displayDims.paddedRect.width()); float elementHeight = widget.getHeightPix(displayDims.paddedRect.height()); - PointF coords = widget.getElementCoordinates(elementHeight, + PointF coords = Widget.calculateCoordinates(elementHeight, elementWidth, displayDims.paddedRect, metrics); DisplayDimensions dims = widget.getWidgetDimensions(); @@ -106,7 +110,7 @@ public void draw(Canvas canvas) throws PlotRenderException { if (widget.isClippingEnabled()) { canvas.clipRect(dims.canvasRect, Region.Op.INTERSECT); } - widget.draw(canvas, dims.canvasRect); + widget.draw(canvas); if (drawMarginsEnabled) { drawSpacing(canvas, dims.canvasRect, dims.marginatedRect, getMarginPaint()); @@ -133,9 +137,9 @@ public void draw(Canvas canvas) throws PlotRenderException { } } - private void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) { + private static void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) { try { - canvas.save(Canvas.ALL_SAVE_FLAG); + canvas.save(); canvas.clipRect(inner, Region.Op.DIFFERENCE); canvas.drawRect(outer, paint); } finally { diff --git a/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java b/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java index 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 79% 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 db9c9f26..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; } 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 ce2896da..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> getSeriesAndFormatterList() { - List> results = new ArrayList<>(); - ArrayList sfList = getPlot().getSeriesRegistry(); - - for(SeriesAndFormatter thisPair : sfList) { + public List> getSeriesAndFormatterList() { + List> results = new ArrayList<>(); + List sfList = getPlot().getRegistry().getSeriesAndFormatterList(); + for(SeriesBundle thisPair : sfList) { if(thisPair.rendersWith(this)) { results.add(thisPair); } @@ -113,9 +109,9 @@ public void drawSeriesLegendIcon(Canvas canvas, RectF rect, SeriesFormatterType */ public List getSeriesList() { List results = new ArrayList<>(); - ArrayList sfList = getPlot().getSeriesRegistry(); + List sfList = getPlot().getRegistry().getSeriesAndFormatterList(); - for(SeriesAndFormatter thisPair : sfList) { + for(SeriesBundle thisPair : sfList) { if(thisPair.rendersWith(this)) { results.add(thisPair.getSeries()); } 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 1731c9ef..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; { @@ -39,16 +35,16 @@ public class TextLabelWidget extends Widget { } 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; } @@ -67,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(); @@ -103,16 +95,13 @@ public void doOnDraw(Canvas canvas, RectF widgetRect) { if(text == null || text.length() == 0) { return; } - //FontUtils.getStringDimensions(text, labelPaint); + float vOffset = labelPaint.getFontMetrics().descent; PointF start = getAnchorCoordinates(widgetRect, - 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: @@ -129,11 +118,8 @@ public void doOnDraw(Canvas canvas, RectF widgetRect) { } canvas.drawText(text, 0, vOffset, labelPaint); } finally { - //canvas.restoreToCount(canvasState); canvas.restore(); } - - // END ROTATION CALCULATION } public Paint getLabelPaint() { @@ -143,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(); @@ -173,7 +159,6 @@ public void setAutoPackEnabled(boolean autoPackEnabled) { } public void setText(String text) { - //Log.d(TAG, "Setting textLabel to: " + text); this.text = text; if(autoPackEnabled) { pack(); diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java index 403f085f..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; /** @@ -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 2849970b..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,8 +16,10 @@ package com.androidplot.util; -import com.androidplot.Bounds; -import com.androidplot.xy.XYBounds; +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; @@ -28,22 +30,41 @@ */ public class SeriesUtils { - public static XYBounds minMax(List seriesList) { + public static RectRegion minMax(List seriesList) { return minMax(null, seriesList); } - public static XYBounds minMax(XYSeries... 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 XYBounds minMax(XYConstraints constraints, List seriesList) { - // TODO: this is inefficient...clean it up! + public static RectRegion minMax(XYConstraints constraints, List seriesList) { return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()])); } @@ -53,44 +74,35 @@ public static XYBounds minMax(XYConstraints constraints, List seriesLi * @return * @since 0.9.7 */ - public static XYBounds minMax(XYConstraints constraints, XYSeries... seriesArray) { + public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArray) { - final XYBounds bounds = new XYBounds(); + 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 (series.size() > 0) { - for (int i = 0; i < series.size(); i++) { - final Number xi = series.getX(i); - final Number yi = series.getY(i); - - // if constraints have been set, make sure this xy coordinate exists within them: - if (constraints == null || constraints.contains(xi, yi)) { - if (xi != null) { - if (bounds.getMinX() == null || - xi.doubleValue() < bounds.getMinX().doubleValue()) { - bounds.setMinX(xi); - } - if (bounds.getMaxX() == null || - xi.doubleValue() > bounds.getMaxX().doubleValue()) { - bounds.setMaxX(xi); - } - } - if (yi != null) { - - if (bounds.getMinY() == null || - yi.doubleValue() < bounds.getMinY().doubleValue()) { - bounds.setMinY(yi); - } - if (bounds.getMaxY() == null || - yi.doubleValue() > bounds.getMaxY().doubleValue()) { - bounds.setMaxY(yi); - } - } - } + + // 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); } } } @@ -99,26 +111,161 @@ public static XYBounds minMax(XYConstraints constraints, XYSeries... seriesArray } /** - * @param lists + * + * @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 + * @param visibleBounds The visible constraints of the plot * @return - * @since 0.9.7 */ - public static Bounds minMax(List... lists) { - Number min = null; - Number max = null; - for (List list : lists) { - for (Number i : list) { - if (i != null) { - double di = i.doubleValue(); - if (min == null || di < min.doubleValue()) { - min = i; + 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; + } } - if (max == null || di > max.doubleValue()) { - max = i; + } + } + } + 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 new Bounds(min, max); + 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 79da8541..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,90 +20,102 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Map.Entry; -import java.util.TreeMap; import android.graphics.Canvas; import android.graphics.RectF; import com.androidplot.ui.RenderStack; -import com.androidplot.ui.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 GroupRenderer { - 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(); + private BarOrientation barOrientation = BarOrientation.OVERLAID; // default Render Style + private BarGroupWidthMode barGroupWidthMode = BarGroupWidthMode.FIXED_WIDTH; // default Width Style + + /** + * Represents the size in pixels of either bar width or bar gap width, depending on the current + * value of barWidthMode. + */ + private float width = PixelUtils.dpToPix(3); + + /** + * How bars should be laid out when in a group of 2 or more series. + */ + public enum BarOrientation { + + /** + * Bars are drawn overlapping one another, in the order their respective series + * was added to the plot. + */ + IN_ORDER, - 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()); } @@ -119,271 +131,255 @@ public FormatterType getFormatter(int index, XYSeries series) { return null; } + @Override + public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { - @Override - public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { - - TreeMap axisMap = new TreeMap(); + List barGroups = new ArrayList<>(); /* * Build the axisMap (yVal,BarGroup)... a TreeMap of BarGroups * BarGroups represent a point on the X axis where a single or group of bars need to be drawn. */ - for(SeriesAndFormatter thisPair : sfList) { - BarGroup barGroup; - - // For each value in the series - for(int i = 0; i < seriesSize; i++) { - - if (thisPair.getSeries().getX(i) != null) { - - // get a new bar object - Bar bar = new Bar(thisPair.getSeries(), thisPair.getFormatter(),i,plotArea); - - // Find or create the barGroup - if (axisMap.containsKey(bar.intX)) { - barGroup = axisMap.get(bar.intX); - } else { - barGroup = new BarGroup(bar.intX,plotArea); - axisMap.put(bar.intX, barGroup); - } - barGroup.addBar(bar); - } + for(int i = 0; i < seriesSize; i++) { + final BarGroup group = new BarGroup(i, 0, plotArea); + int seriesOrder = 0; + for(SeriesBundle bundle : sfList) { + // TODO: is this null check really necessary? + if(bundle.getSeries().getX(i) != null) { + Bar bar = new Bar(getPlot(), bundle.getSeries(), + bundle.getFormatter(), seriesOrder, i, plotArea); + group.addBar(bar); + group.centerPix = bar.xPix; + } + seriesOrder++; } + barGroups.add(group); } - // Loop through the axisMap linking up prev pointers - BarGroup prev, current; - prev = null; - for(Entry mapEntry : axisMap.entrySet()) { - current = mapEntry.getValue(); - current.prev = prev; - prev = current; - } - - // The default gap between each bar section - int gap = (int) barGap; - - // Determine roughly how wide (rough_width) this bar should be. This is then used as a default width - // when there are gaps in the data or for the first/last bars. - float f_rough_width = ((plotArea.width() - ((axisMap.size() - 1) * gap)) / (axisMap.size() - 1)); - int rough_width = (int) f_rough_width; - if (rough_width < 0) rough_width = 0; - if (gap > rough_width) { - gap = rough_width / 2; - } - /* - * Calculate the dimensions of each barGroup and then draw each bar within it according to + * Calculate the dimensions of each barGroup and then draw each bar within it according to * the Render Style and Width Style. */ - for(Number key : axisMap.keySet()) { - - BarGroup barGroup = axisMap.get(key); - - // Determine the exact left and right X for the Bar Group - switch (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/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 index b3af9d79..84fce88f 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.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; @@ -45,8 +46,8 @@ public class CandlestickFormatter extends XYSeriesFormatter { private BodyStyle bodyStyle; public enum BodyStyle { - Square, - Triangle + SQUARE, + TRIANGULAR } protected static Paint getDefaultFillPaint(int color) { @@ -65,6 +66,11 @@ protected static Paint getDefaultStrokePaint(int color) { return p; } + public CandlestickFormatter(Context context, int xmlCfgId) { + this(); + configure(context, xmlCfgId); + } + public CandlestickFormatter() { this(getDefaultStrokePaint(Color.YELLOW), getDefaultFillPaint(Color.GREEN), @@ -73,7 +79,7 @@ public CandlestickFormatter() { getDefaultStrokePaint(Color.RED), getDefaultStrokePaint(Color.YELLOW), getDefaultStrokePaint(Color.YELLOW), - BodyStyle.Square); + BodyStyle.SQUARE); } public CandlestickFormatter(Paint wickPaint, Paint risingBodyFillPaint, Paint fallingBodyFillPaint, @@ -95,7 +101,7 @@ public Class getRendererClass() { } @Override - public SeriesRenderer getRendererInstance(XYPlot plot) { + public SeriesRenderer doGetRendererInstance(XYPlot plot) { return new CandlestickRenderer(plot); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java index 49f559ef..81c4363c 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java @@ -18,8 +18,7 @@ import android.graphics.*; import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesAndFormatter; -import com.androidplot.util.ValPixConverter; +import com.androidplot.ui.SeriesBundle; import java.util.List; @@ -39,10 +38,10 @@ */ public class CandlestickRenderer extends GroupRenderer { - private static final int HIGH_INDEX = 0; - private static final int LOW_INDEX = 1; - private static final int OPEN_INDEX = 2; - private static final int CLOSE_INDEX = 3; + 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); @@ -50,60 +49,53 @@ public CandlestickRenderer(XYPlot plot) { @Override - public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { + 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++) { - // x-val for all series should be identical so just grab x from the first series: - Number x = sfList.get(HIGH_INDEX).getSeries().getX(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(); - Number high = sfList.get(HIGH_INDEX).getSeries().getY(i); - Number low = sfList.get(LOW_INDEX).getSeries().getY(i); - Number open = sfList.get(OPEN_INDEX).getSeries().getY(i); - Number close = sfList.get(CLOSE_INDEX).getSeries().getY(i); - drawValue(canvas, plotArea, sfList.get(0).getFormatter(), x, high, low, open, close); + // 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 drawValue(Canvas canvas, RectF plotArea, FormatterType formatter, - Number x, Number high, Number low, Number open, Number close) { - final PointF highPix = ValPixConverter.valToPix( - x, high, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - - final PointF lowPix = ValPixConverter.valToPix( - x, low, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - - final PointF openPix = ValPixConverter.valToPix( - x, open, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - - final PointF closePix = ValPixConverter.valToPix( - x, close, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - - drawWick(canvas, highPix, lowPix, formatter); - drawBody(canvas, openPix, closePix, formatter); - drawUpperCap(canvas, highPix, formatter); - drawLowerCap(canvas, lowPix, formatter); + 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) { @@ -121,11 +113,11 @@ protected void drawBody(Canvas canvas, PointF open, PointF close, FormatterType formatter.getRisingBodyStrokePaint() : formatter.getFallingBodyStrokePaint(); switch(formatter.getBodyStyle()) { - case Square: + case SQUARE: canvas.drawRect(rect, bodyFillPaint); canvas.drawRect(rect, bodyStrokePaint); break; - case Triangle: + case TRIANGULAR: drawTriangle(canvas, rect, bodyFillPaint, bodyStrokePaint); } } 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 index 118af5bf..503d624d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java @@ -19,15 +19,15 @@ import android.graphics.Canvas; import android.graphics.RectF; import android.util.Log; -import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import java.util.List; /** - * Renders data to an XYPlot that potentially contains more than a single yVal per index. - * Examples of such data are bar plot groups, candlestick charts, etc. + * 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> @@ -41,11 +41,11 @@ public GroupRenderer(XYPlot plot) { @Override protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, - FormatterType formatter, RenderStack stack) throws PlotRenderException { + FormatterType formatter, RenderStack stack) { // get all the data associated with this renderer: - List> sfList = getSeriesAndFormatterList(); + List> sfList = getSeriesAndFormatterList(); // no data to render so exit: if(sfList == null) { @@ -79,6 +79,6 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, * @param sfList * @param stack */ - public abstract void onRender(Canvas canvas, RectF plotArea, List> sfList, int size, RenderStack 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 02b033f5..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,12 +16,21 @@ 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 @@ -32,12 +41,28 @@ public class LineAndPointRenderer e 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 c406d9e2..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,49 +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 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 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 @@ -86,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); } } @@ -138,7 +226,6 @@ public static List regionsWithin(List regions, Number mi } /** - * * @return Width of this region, in native units */ public Number getWidth() { @@ -146,7 +233,6 @@ public Number getWidth() { } /** - * * @return Height of this region, in native units */ public Number getHeight() { @@ -159,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() { @@ -202,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 97f1a81b..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,41 +17,34 @@ package com.androidplot.xy; import android.graphics.Canvas; + import com.androidplot.Plot; import com.androidplot.PlotListener; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * A convenience class used to create instances of XYPlot generated from Lists of Numbers. */ -public class SimpleXYSeries implements XYSeries, PlotListener { - - private static final String TAG = SimpleXYSeries.class.getName(); - - @Override - public void onBeforeDraw(Plot source, Canvas canvas) { - lock.readLock().lock(); - } +public class SimpleXYSeries implements EditableXYSeries, OrderedXYSeries, PlotListener { + private volatile LinkedList xVals = new LinkedList<>(); + private volatile LinkedList yVals = new LinkedList<>(); + private volatile String title = null; - @Override - public void onAfterDraw(Plot source, Canvas canvas) { - lock.readLock().unlock(); - } + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + private XOrder xOrder = XOrder.NONE; public enum ArrayFormat { Y_VALS_ONLY, XY_VALS_INTERLEAVED } - private volatile LinkedList xVals = new LinkedList(); - private volatile LinkedList yVals = new LinkedList(); - private volatile String title = null; - private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); - - public SimpleXYSeries(String title) { this.title = title; } @@ -60,11 +53,39 @@ public SimpleXYSeries(ArrayFormat format, String title, Number... model) { this(asNumberList(model), format, title); } + /** + * Retrieve the current x-ordering specified for this series. Default is + * {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE}. + * @return + */ + @Override + public XOrder getXOrder() { + return xOrder; + } + + /** + * If XVals are in strict ascending order, use this method to set + * {@link com.androidplot.xy.OrderedXYSeries.XOrder#ASCENDING} to provide an optimization + * hint to the renderer. + * @param xOrder + */ + public void setXOrder(XOrder xOrder) { + this.xOrder = xOrder; + } + + @Override + public void onBeforeDraw(Plot source, Canvas canvas) { + lock.readLock().lock(); + } + + @Override + public void onAfterDraw(Plot source, Canvas canvas) { + lock.readLock().unlock(); + } + protected static List asNumberList(Number... model) { - List numbers = new ArrayList<>(); - for(Number n : model) { - numbers.add(n); - } + List numbers = new ArrayList<>(model.length); + Collections.addAll(numbers, model); return numbers; } @@ -120,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: @@ -132,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."); @@ -188,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 @@ -216,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(); } @@ -244,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(); } @@ -284,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/XYBounds.java b/androidplot-core/src/main/java/com/androidplot/xy/XYBounds.java deleted file mode 100644 index 1d503ad6..00000000 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYBounds.java +++ /dev/null @@ -1,98 +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 com.androidplot.Bounds; - -/** - * Defines a rectangle using xy min/max values. XYBounds differs from {@link RectRegion} in that - * it accepts null values. - * @since 0.9.7 - */ -public class XYBounds { - - private Bounds xBounds; - private Bounds yBounds; - - public XYBounds() { - this(null, null, null, null); - } - - public XYBounds(Number minX, Number maxX, Number minY, Number maxY) { - xBounds = new Bounds(minX, maxX); - yBounds = new Bounds(minY, maxY); - } - - /** - * 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 - * @param input - */ - public void union(XYBounds input) { - xBounds.union(input.getXBounds()); - yBounds.union(input.getYBounds()); - } - - public void setXBounds(Bounds xBounds) { - this.xBounds = xBounds; - } - - public void setYBounds(Bounds yBounds) { - this.yBounds = yBounds; - } - - public Bounds getXBounds() { - return xBounds; - } - - public Bounds getYBounds() { - return yBounds; - } - - public Number getMinX() { - return xBounds.getMin(); - } - - public void setMinX(Number minX) { - xBounds.setMin(minX); - } - - public Number getMaxX() { - return xBounds.getMax(); - } - - public void setMaxX(Number maxX) { - xBounds.setMax(maxX); - } - - public Number getMinY() { - return yBounds.getMin(); - } - - public void setMinY(Number minY) { - yBounds.setMin(minY); - } - - public Number getMaxY() { - return yBounds.getMax(); - } - - public void setMaxY(Number maxY) { - yBounds.setMax(maxY); - } -} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java index 398d0379..c4452d33 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java @@ -16,16 +16,20 @@ 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. 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. + // 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; @@ -44,113 +48,148 @@ public XYConstraints() { this(null, null, null, null); } - public XYConstraints(Number minX, Number maxX, Number minY, Number maxY) { + public XYConstraints(@Nullable Number minX, @Nullable Number maxX, @Nullable Number minY, @Nullable Number maxY) { this.minX = minX; this.minY = minY; this.maxX = maxX; this.maxY = maxY; } + public boolean contains(@NonNull RectRegion rectRegion) { + return contains(rectRegion.getMinY(), rectRegion.getMinY()) + && contains(rectRegion.getMaxX(), rectRegion.getMaxY()); + } + public boolean contains(Number x, Number y) { - if(x == null || y == null) { + if (x == null || y == null) { // this is essentially an invisible point: return false; - } else { - final double dx = x.doubleValue(); - - if(minX != null && dx < minX.doubleValue()) { - return false; - } else if(maxX != null && dx > maxX.doubleValue()) { - return false; - } else { - final double dy = y.doubleValue(); - if(minY != null && dy < minY.doubleValue()) { - return false; - } else if(maxY != null && dy > maxY.doubleValue()) { - return false; - } - } + } + + if (minX == null && maxX == null && minY == null && maxY == null) { + //there are no constraints return true; } + + final double dx = x.doubleValue(); + if (minX != null && dx < minX.doubleValue()) { + return false; + } else if (maxX != null && dx > maxX.doubleValue()) { + return false; + } + + final double dy = y.doubleValue(); + if (minY != null && dy < minY.doubleValue()) { + return false; + } else if (maxY != null && dy > maxY.doubleValue()) { + return false; + } + + return true; } + @Nullable public Number getMinX() { return minX; } + @Nullable public Number getMaxX() { return maxX; } + @Nullable public Number getMinY() { return minY; } + @Nullable public Number getMaxY() { return maxY; } + public void setMinX(@Nullable Number minX) { + this.minX = minX; + } + + public void setMaxX(@Nullable Number maxX) { + this.maxX = maxX; + } + + public void setMinY(@Nullable Number minY) { + this.minY = minY; + } + + public void setMaxY(@Nullable Number maxY) { + this.maxY = maxY; + } + + @NonNull public XYFramingModel getDomainFramingModel() { return domainFramingModel; } - public void setDomainFramingModel(XYFramingModel domainFramingModel) { + public void setDomainFramingModel(@NonNull XYFramingModel domainFramingModel) { this.domainFramingModel = domainFramingModel; } + @NonNull public XYFramingModel getRangeFramingModel() { return rangeFramingModel; } - public void setRangeFramingModel(XYFramingModel rangeFramingModel) { + public void setRangeFramingModel(@NonNull XYFramingModel rangeFramingModel) { this.rangeFramingModel = rangeFramingModel; } + @NonNull public BoundaryMode getDomainUpperBoundaryMode() { return domainUpperBoundaryMode; } - public void setDomainUpperBoundaryMode(BoundaryMode domainUpperBoundaryMode) { + public void setDomainUpperBoundaryMode(@NonNull BoundaryMode domainUpperBoundaryMode) { this.domainUpperBoundaryMode = domainUpperBoundaryMode; } + @NonNull public BoundaryMode getDomainLowerBoundaryMode() { return domainLowerBoundaryMode; } - public void setDomainLowerBoundaryMode(BoundaryMode domainLowerBoundaryMode) { + public void setDomainLowerBoundaryMode(@NonNull BoundaryMode domainLowerBoundaryMode) { this.domainLowerBoundaryMode = domainLowerBoundaryMode; } + @NonNull public BoundaryMode getRangeUpperBoundaryMode() { return rangeUpperBoundaryMode; } - public void setRangeUpperBoundaryMode(BoundaryMode rangeUpperBoundaryMode) { + public void setRangeUpperBoundaryMode(@NonNull BoundaryMode rangeUpperBoundaryMode) { this.rangeUpperBoundaryMode = rangeUpperBoundaryMode; } + @NonNull public BoundaryMode getRangeLowerBoundaryMode() { return rangeLowerBoundaryMode; } - public void setRangeLowerBoundaryMode(BoundaryMode rangeLowerBoundaryMode) { + public void setRangeLowerBoundaryMode(@NonNull BoundaryMode rangeLowerBoundaryMode) { this.rangeLowerBoundaryMode = rangeLowerBoundaryMode; } - public void setMinX(Number minX) { - this.minX = minX; - } - - public void setMaxX(Number maxX) { - this.maxX = maxX; - } - - public void setMinY(Number minY) { - this.minY = minY; - } - - public void setMaxY(Number maxY) { - this.maxY = maxY; + @Override + public String toString() { + return "XYConstraints{" + "domainFramingModel=" + domainFramingModel + + ", rangeFramingModel=" + rangeFramingModel + + ", domainUpperBoundaryMode=" + domainUpperBoundaryMode + + ", domainLowerBoundaryMode=" + domainLowerBoundaryMode + + ", rangeUpperBoundaryMode=" + rangeUpperBoundaryMode + + ", rangeLowerBoundaryMode=" + rangeLowerBoundaryMode + + ", minX=" + minX + + ", maxX=" + maxX + + ", minY=" + minY + + ", maxY=" + maxY + + '}'; } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/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 f373d594..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,180 +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. - * The inner area of the graph upon which grid ticks, lines and points are rendered is called the "grid" area. + * Displays graphical data (lines, points, etc.) annotated with domain and range tick markers. The + * inner area of the graph upon which grid lines and points are rendered is called the "grid" area. */ public class XYGraphWidget extends Widget { 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); + + /** + * Line interval per range label + */ + private int linesPerRangeLabel = ONE; - // max size of domain tick labels in pixels - private float domainTickLabelWidth; - private float rangeTickLabelWidth; - 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 boolean showDomainLabels; - private boolean showRangeLabels; + /** + * how far lines extend on labeled "tick" lines + */ + private float lineExtensionTop; + private float lineExtensionBottom; + private float lineExtensionLeft; + private float lineExtensionRight; - private float domainTickExtension; - private float rangeTickExtension; - private float domainLabelSubTickExtension = ZERO; - private float rangeLabelSubTickExtension = ZERO; + /** + * 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; + } } { @@ -197,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); @@ -244,16 +265,9 @@ 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); } @@ -263,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); @@ -434,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(shouldDrawLabel) { + // labels + if (isLineLabelEnabled(Edge.LEFT)) { + drawLineLabel(canvas, Edge.LEFT, yVal, labelRect.left, yPix, isOrigin); } - 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); - } - } 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()); - + Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect); - // 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, !showDomainLabels); - } + // Draw Domain Lines: - // 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, !showDomainLabels); - } else { - drawDomainTick(canvas, xPix, xVal, dlp, domainSubGridLinePaint, true); - } - } - i++; - } + final double domainStepPix = domainStep.getStepPix(); + final double iMin = (gridRect.left - domainOriginPix - FUDGE) / domainStepPix; + final double iMax = (gridRect.right - domainOriginPix + FUDGE) / domainStepPix; - // 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) { - - if (i % getTicksPerDomainLabel() == ZERO) { - drawDomainTick(canvas, xPix, xVal, dlp, domainGridLinePaint, !showDomainLabels); - } 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, !showRangeLabels); + // if no range origin is set, use the bottom-most value visible on the grid: + rangeOriginPix = gridRect.bottom; + rangeOrigin = plot.getBounds().getMinY(); } - // 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, !showRangeLabels); - } else { - drawRangeTick(canvas, yPix, yVal, rlp, - rangeSubGridLinePaint, true); - } - } - i++; - } - - // 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, !showRangeLabels); - } 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(); + } } } @@ -911,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; } @@ -967,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. */ @@ -992,6 +838,7 @@ public Paint getDomainGridLinePaint() { /** * Set the paint used to draw the domain grid line. + * * @param gridLinePaint */ public void setDomainGridLinePaint(Paint gridLinePaint) { @@ -1011,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) { @@ -1022,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) { @@ -1037,337 +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 float getDomainTickExtension() { - return domainTickExtension; + public Paint getDomainOriginLinePaint() { + return domainOriginLinePaint; } - public void setDomainTickExtension(float domainTickExtension) { - this.domainTickExtension = domainTickExtension; + public void setDomainOriginLinePaint(Paint domainOriginLinePaint) { + this.domainOriginLinePaint = domainOriginLinePaint; } - public float getRangeTickExtension() { - return rangeTickExtension; + public Paint getRangeOriginLinePaint() { + return rangeOriginLinePaint; } - public float getDomainLabelSubTickExtension() { - return domainLabelSubTickExtension; + public void setRangeOriginLinePaint(Paint rangeOriginLinePaint) { + this.rangeOriginLinePaint = rangeOriginLinePaint; } - public void setDomainLabelSubTickExtension(float domainLabelSubTickExtension) { - this.domainLabelSubTickExtension = 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 setRangeTickExtension(float rangeTickExtension) { - this.rangeTickExtension = rangeTickExtension; + /** + * Set domain and range cursor position using screen coordinates + * + * @param point + */ + public void setCursorPosition(PointF point) { + setCursorPosition(point.x, point.y); } - public float getRangeLabelSubTickExtension() { - return rangeLabelSubTickExtension; + public Float getDomainCursorPosition() { + return domainCursorPosition; } - public void setRangeLabelSubTickExtension(float rangeLabelSubTickExtension) { - this.rangeLabelSubTickExtension = rangeLabelSubTickExtension; + public Number getDomainCursorVal() { + return screenToSeriesX(getDomainCursorPosition()); } - public int getTicksPerRangeLabel() { - return ticksPerRangeLabel; + /** + * Set domain cursor position using screen coordinates + * + * @param domainCursorPosition + */ + public void setDomainCursorPosition(Float domainCursorPosition) { + this.domainCursorPosition = domainCursorPosition; } - public void setTicksPerRangeLabel(int ticksPerRangeLabel) { - this.ticksPerRangeLabel = ticksPerRangeLabel; + public Float getRangeCursorPosition() { + return rangeCursorPosition; } - public int getTicksPerDomainLabel() { - return ticksPerDomainLabel; + public Number getRangeCursorVal() { + return screenToSeriesY(getRangeCursorPosition()); } - public void setTicksPerDomainLabel(int ticksPerDomainLabel) { - this.ticksPerDomainLabel = ticksPerDomainLabel; + /** + * Set range cursor position using screen coordinates + * + * @param rangeCursorPosition + */ + public void setRangeCursorPosition(Float rangeCursorPosition) { + this.rangeCursorPosition = rangeCursorPosition; } - public Paint getDomainOriginLinePaint() { - return domainOriginLinePaint; + public boolean isDrawGridOnTop() { + return drawGridOnTop; } - public void setDomainOriginLinePaint(Paint domainOriginLinePaint) { - this.domainOriginLinePaint = domainOriginLinePaint; + public void setDrawGridOnTop(boolean drawGridOnTop) { + this.drawGridOnTop = drawGridOnTop; } - public Paint getRangeOriginLinePaint() { - return rangeOriginLinePaint; + public boolean isDrawMarkersEnabled() { + return drawMarkersEnabled; } - public void setRangeOriginLinePaint(Paint rangeOriginLinePaint) { - this.rangeOriginLinePaint = rangeOriginLinePaint; + public void setDrawMarkersEnabled(boolean drawMarkersEnabled) { + this.drawMarkersEnabled = drawMarkersEnabled; } - public Paint getDomainOriginTickLabelPaint() { - return domainOriginTickLabelPaint; + public Paint getDomainCursorPaint() { + return domainCursorPaint; } - public void setDomainOriginTickLabelPaint(Paint domainOriginTickLabelPaint) { - this.domainOriginTickLabelPaint = 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 Paint getRangeOriginTickLabelPaint() { - return rangeOriginTickLabelPaint; + public Paint getRangeCursorPaint() { + return rangeCursorPaint; } - public void setRangeOriginTickLabelPaint(Paint rangeOriginTickLabelPaint) { - this.rangeOriginTickLabelPaint = 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 setCursorPosition(float x, float y) { - setDomainCursorPosition(x); - setRangeCursorPosition(y); + public float getLineExtensionTop() { + return lineExtensionTop; } - public void setCursorPosition(PointF point) { - setCursorPosition(point.x, point.y); + public void setLineExtensionTop(float lineExtensionTop) { + this.lineExtensionTop = lineExtensionTop; } - public float getDomainCursorPosition() { - return domainCursorPosition; + public float getLineExtensionBottom() { + return lineExtensionBottom; } - public Double getDomainCursorVal() { - return getXVal(getDomainCursorPosition()); + public void setLineExtensionBottom(float lineExtensionBottom) { + this.lineExtensionBottom = lineExtensionBottom; } - public void setDomainCursorPosition(float domainCursorPosition) { - this.domainCursorPosition = domainCursorPosition; + public float getLineExtensionLeft() { + return lineExtensionLeft; } - public float getRangeCursorPosition() { - return rangeCursorPosition; + public void setLineExtensionLeft(float lineExtensionLeft) { + this.lineExtensionLeft = lineExtensionLeft; } - public Double getRangeCursorVal() { - return getYVal(getRangeCursorPosition()); + public float getLineExtensionRight() { + return lineExtensionRight; } - public void setRangeCursorPosition(float rangeCursorPosition) { - this.rangeCursorPosition = rangeCursorPosition; + public void setLineExtensionRight(float lineExtensionRight) { + this.lineExtensionRight = lineExtensionRight; } - public Paint getCursorLabelPaint() { - return cursorLabelPaint; + 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 void setCursorLabelPaint(Paint cursorLabelPaint) { - this.cursorLabelPaint = 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 Paint getCursorLabelBackgroundPaint() { - return cursorLabelBackgroundPaint; + public LineLabelRenderer getLineLabelRenderer(Edge edge) { + return lineLabelRenderers.get(edge); } - public void setCursorLabelBackgroundPaint(Paint cursorLabelBackgroundPaint) { - this.cursorLabelBackgroundPaint = cursorLabelBackgroundPaint; + public void setLineLabelRenderer(Edge edge, LineLabelRenderer renderer) { + lineLabelRenderers.put(edge, renderer); } - public boolean isDrawMarkersEnabled() { - return drawMarkersEnabled; + public LineLabelStyle getLineLabelStyle(Edge edge) { + return lineLabelStyles.get(edge); } - public void setDrawMarkersEnabled(boolean drawMarkersEnabled) { - this.drawMarkersEnabled = drawMarkersEnabled; + public void setLineLabelStyle(Edge edge, LineLabelStyle style) { + lineLabelStyles.put(edge, style); } - public boolean isRangeAxisLeft() { - return rangeAxisLeft; + public CursorLabelFormatter getCursorLabelFormatter() { + return cursorLabelFormatter; } - public void setRangeAxisLeft(boolean rangeAxisLeft) { - this.rangeAxisLeft = rangeAxisLeft; + public void setCursorLabelFormatter( + CursorLabelFormatter cursorLabelFormatter) { + this.cursorLabelFormatter = cursorLabelFormatter; } - public boolean isDomainAxisBottom() { - return domainAxisBottom; + /** + * Grid insets + */ + public Insets getGridInsets() { + return gridInsets; } - public void setDomainAxisBottom(boolean domainAxisBottom) { - this.domainAxisBottom = domainAxisBottom; - } - - public boolean isRangeTick() { - return rangeTick; + public void setGridInsets(Insets gridInsets) { + this.gridInsets = gridInsets; + recalculateSizes(null); } - public void setRangeTick(boolean rangeTick) { - this.rangeTick = rangeTick; + /** + * Domain / Range label insets + */ + public Insets getLineLabelInsets() { + return lineLabelInsets; } - public boolean isRangeSubTick() { - return rangeSubTick; + public void setLineLabelInsets(Insets lineLabelInsets) { + this.lineLabelInsets = lineLabelInsets; + recalculateSizes(null); } - public void setRangeSubTick(boolean rangeSubTick) { - this.rangeSubTick = rangeSubTick; + public RectF getGridRect() { + return gridRect; } - public boolean isDomainTick() { - return domainTick; + public void setGridRect(RectF gridRect) { + this.gridRect = gridRect; } - public void setDomainTick(boolean domainTick) { - this.domainTick = domainTick; + public RectF getLabelRect() { + return labelRect; } - public boolean isDomainSubTick() { - return domainSubTick; + public void setLabelRect(RectF labelRect) { + this.labelRect = labelRect; } - public void setDomainSubTick(boolean domainSubTick) { - this.domainSubTick = domainSubTick; + public boolean isGridClippingEnabled() { + return isGridClippingEnabled; } - /* - * 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)); - } - } - } - - /* - * 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 setGridClippingEnabled(boolean gridClippingEnabled) { + isGridClippingEnabled = gridClippingEnabled; } - public DisplayDimensions getGridDimensions() { - return gridDimensions; + public boolean isLineLabelEnabled(Edge position) { + return lineLabelEdges.contains(position); } - /** - * - * @return - * @since 0.9.8 - */ - public boolean isShowDomainLabels() { - return showDomainLabels; + public void setLineLabelEdges(Edge... positions) { + EnumSet positionSet = EnumSet.noneOf(Edge.class); + if (positions != null) { + Collections.addAll(positionSet, positions); + } + this.lineLabelEdges = positionSet; } - /** - * - * @param showDomainLabels - * @since 0.9.8 - */ - public void setShowDomainLabels(boolean showDomainLabels) { - this.showDomainLabels = showDomainLabels; + public void setLineLabelEdges(Collection positions) { + this.lineLabelEdges = EnumSet.copyOf(positions); } - /** - * - * @return - * @since 0.9.8 - */ - public boolean isShowRangeLabels() { - return showRangeLabels; + protected void setLineLabelEdges(int bitfield) { + for (Edge tp : Edge.values()) { + if ((tp.value & bitfield) == tp.value) { + lineLabelEdges.add(tp); + } + } } /** + * Checks whether the point exists within the visible grid space. * - * @param showRangeLabels - * @since 0.9.8 + * @param x + * @param y + * @return + * @since 1.0 */ - public void setShowRangeLabels(boolean showRangeLabels) { - this.showRangeLabels = showRangeLabels; + 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 837db7ca..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,17 +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 com.androidplot.util.SeriesUtils; -import java.text.Format; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -40,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; @@ -54,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; @@ -65,36 +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_DOMAIN_TICK_EXTENSION_DP = 2; - private static final int DEFAULT_RANGE_TICK_EXTENSION_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 static final int DEFAULT_DOMAIN_TICK_LABEL_WIDTH = 15; - private static final int DEFAULT_RANGE_TICK_LABEL_WIDTH = 41; - private BoundaryMode domainOriginBoundaryMode; private BoundaryMode rangeOriginBoundaryMode; // widgets - private XYLegendWidget legendWidget; - private XYGraphWidget graphWidget; - private TextLabelWidget domainLabelWidget; - private TextLabelWidget rangeLabelWidget; + private XYLegendWidget legend; + private XYGraphWidget graph; + private TextLabelWidget domainTitle; + private TextLabelWidget rangeTitle; - private XYStepModel domainStepModel; - private XYStepModel rangeStepModel; + private StepModel domainStepModel; + private StepModel rangeStepModel; private XYConstraints constraints = new XYConstraints(); - // these are the final min/max used for dispplaying data - private Number calculatedMinX; - private Number calculatedMaxX; - private Number calculatedMinY; - private Number calculatedMaxY; + // 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. @@ -103,37 +100,31 @@ 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; + /** + * 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 boolean drawDomainOriginEnabled = true; - private boolean drawRangeOriginEnabled = true; - private ArrayList yValueMarkers; private ArrayList xValueMarkers; - private RectRegion defaultBounds; - private PreviewMode previewMode; + public enum PreviewMode { LineAndPoint, Candlestick, @@ -159,84 +150,84 @@ 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); + VerticalPositioning.ABSOLUTE_FROM_CENTER, + Anchor.LEFT_MIDDLE); - getLayoutManager().moveToTop(getTitleWidget()); - getLayoutManager().moveToTop(getLegendWidget()); + getLayoutManager().moveToTop(getTitle()); + getLayoutManager().moveToTop(getLegend()); - getDomainLabelWidget().pack(); - getRangeLabelWidget().pack(); + 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)); @@ -245,10 +236,8 @@ protected void onPreInit() { xValueMarkers = new ArrayList<>(); yValueMarkers = new ArrayList<>(); - setDefaultBounds(new RectRegion(-1, 1, -1, 1)); - - domainStepModel = new XYStepModel(XYStepMode.SUBDIVIDE, 10); - rangeStepModel = new XYStepModel(XYStepMode.SUBDIVIDE, 10); + domainStepModel = new StepModel(StepMode.SUBDIVIDE, 10); + rangeStepModel = new StepModel(StepMode.SUBDIVIDE, 10); } @Override @@ -258,11 +247,14 @@ protected void onAfterConfig() { switch (previewMode) { case LineAndPoint: { - addSeries(new SimpleXYSeries(Arrays.asList(1, 2, 3, 3, 4), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Red"), + 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"), + 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"), + addSeries(new SimpleXYSeries(Arrays.asList(3, 3, 2, 3, 3), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Blue"), new LineAndPointFormatter(Color.BLUE, null, null, null)); } break; @@ -291,156 +283,61 @@ protected void processAttrs(TypedArray attrs) { this. previewMode = PreviewMode.values()[attrs.getInt( R.styleable.xy_XYPlot_previewMode, PreviewMode.LineAndPoint.ordinal())]; - // graph size & position - AttrUtils.configureWidget(attrs, getGraphWidget(), - R.styleable.xy_XYPlot_graphHeightSizeLayoutType, R.styleable.xy_XYPlot_graphHeight, - R.styleable.xy_XYPlot_graphWidthSizeLayoutType, R.styleable.xy_XYPlot_graphWidth, - R.styleable.xy_XYPlot_graphLayoutStyleX, R.styleable.xy_XYPlot_graphPositionX, - R.styleable.xy_XYPlot_graphLayoutStyleY, R.styleable.xy_XYPlot_graphPositionY, - R.styleable.xy_XYPlot_graphAnchorPosition, R.styleable.xy_XYPlot_graphVisible); - - String domainLabelAttr = attrs.getString(R.styleable.xy_XYPlot_domainLabel); + 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); } - graphWidget.setDomainTickLabelWidth( - attrs.getDimension(R.styleable.xy_XYPlot_domainTickLabelWidth, DEFAULT_DOMAIN_TICK_LABEL_WIDTH)); - - graphWidget.setRangeTickLabelWidth( - attrs.getDimension(R.styleable.xy_XYPlot_rangeTickLabelWidth, DEFAULT_RANGE_TICK_LABEL_WIDTH)); - AttrUtils.configureStep(attrs, getDomainStepModel(), R.styleable.xy_XYPlot_domainStepMode, R.styleable.xy_XYPlot_domainStep); 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); - - graphWidget.setDomainTickExtension(attrs.getDimension(R.styleable.xy_XYPlot_domainTickExtension, - PixelUtils.dpToPix(DEFAULT_DOMAIN_TICK_EXTENSION_DP))); - - graphWidget.setRangeTickExtension(attrs.getDimension(R.styleable.xy_XYPlot_rangeTickExtension, - PixelUtils.dpToPix(DEFAULT_RANGE_TICK_EXTENSION_DP))); - - graphWidget.setShowDomainLabels(attrs.getBoolean(R.styleable.xy_XYPlot_showDomainLabels, true)); - graphWidget.setShowRangeLabels(attrs.getBoolean(R.styleable.xy_XYPlot_showRangeLabels, true)); - - // 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); - - // domainOriginLinePaint - AttrUtils.configureLinePaint(attrs, getGraphWidget().getDomainOriginLinePaint(), - R.styleable.xy_XYPlot_domainOriginLineColor, - R.styleable.xy_XYPlot_domainOriginLineThickness); - - // rangeOriginLinePaint - AttrUtils.configureLinePaint(attrs, getGraphWidget().getRangeOriginLinePaint(), - R.styleable.xy_XYPlot_rangeOriginLineColor, - R.styleable.xy_XYPlot_rangeOriginLineThickness); + 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.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); - 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())); - - 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!!! } /** @@ -451,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); } /** ` @@ -468,41 +362,129 @@ 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); + } + + public Number screenToSeriesX(float x) { + return getGraph().screenToSeriesX(x); + } + + 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); + } + + /** + * 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; + prevMinX = bounds.isMinXSet() ? bounds.getMinX() : null; + prevMaxX = bounds.isMaxXSet() ? bounds.getMaxX() : null; + prevMinY = bounds.isMinYSet() ? bounds.getMinY() : null; + prevMaxY = bounds.isMaxYSet() ? bounds.getMaxY() : null; - calculatedMinX = constraints.getMinX(); - calculatedMaxX = constraints.getMaxX(); - calculatedMinY = constraints.getMinY(); - calculatedMaxY = constraints.getMaxY(); + bounds.setMinX(constraints.getMinX()); + bounds.setMaxX(constraints.getMaxX()); + bounds.setMinY(constraints.getMinY()); + bounds.setMaxY(constraints.getMaxY()); // only calculate if we must: - if(calculatedMinX == null || calculatedMaxX == null || calculatedMinY == null || calculatedMaxY == null) { + if(!bounds.isFullyDefined()) { + + RectRegion b = SeriesUtils.minMax(constraints, getRegistry().getSeriesList()); - XYBounds bounds = SeriesUtils.minMax(constraints, getSeriesRegistry().getSeriesList()); + if(!bounds.isMinXSet()) { + bounds.setMinX(b.getMinX()); + } + if(!bounds.isMaxXSet()) { + bounds.setMaxX(b.getMaxX()); + } - if(calculatedMinX == null) calculatedMinX = bounds.getMinX(); - if(calculatedMaxX == null) calculatedMaxX = bounds.getMaxX(); - if(calculatedMinY == null) calculatedMinY = bounds.getMinY(); - if(calculatedMaxY == null) calculatedMaxY = bounds.getMaxY(); + 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 @@ -513,14 +495,13 @@ public void calculateMinMaxVals() { updateDomainMinMaxForOriginModel(); break; case EDGE: - calculatedMaxX = getCalculatedUpperBoundary( - constraints.getDomainUpperBoundaryMode(), prevMaxX, calculatedMaxX); - calculatedMinX = getCalculatedLowerBoundary( - constraints.getDomainLowerBoundaryMode(), 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( @@ -532,13 +513,13 @@ public void calculateMinMaxVals() { updateRangeMinMaxForOriginModel(); break; case EDGE: - if (getSeriesRegistry().size() > 0) { - calculatedMaxY = getCalculatedUpperBoundary( - constraints.getRangeUpperBoundaryMode(), prevMaxY, calculatedMaxY); - calculatedMinY = getCalculatedLowerBoundary( - constraints.getRangeLowerBoundaryMode(), 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: @@ -546,11 +527,12 @@ public void calculateMinMaxVals() { "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) { @@ -605,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); @@ -620,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); } @@ -632,9 +614,9 @@ public void centerOnDomainOrigin(Number origin) { * @param extent * @param mode */ - public void centerOnDomainOrigin(Number origin, Number extent, BoundaryMode mode) { + public void centerOnDomainOrigin(@NonNull Number origin, Number extent, BoundaryMode mode) { if (origin == null) { - throw new NullPointerException("Origin param cannot be null."); + throw new IllegalArgumentException("Origin param cannot be null."); } constraints.setDomainFramingModel(XYFramingModel.ORIGIN); setUserDomainOrigin(origin); @@ -651,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); } @@ -664,9 +646,9 @@ public void centerOnRangeOrigin(Number origin) { * @param mode */ @SuppressWarnings("SameParameterValue") - public void centerOnRangeOrigin(Number origin, Number extent, BoundaryMode mode) { + public void centerOnRangeOrigin(@NonNull Number origin, Number extent, BoundaryMode mode) { if (origin == null) { - throw new NullPointerException("Origin param cannot be null."); + throw new IllegalArgumentException("Origin param cannot be null."); } constraints.setRangeFramingModel(XYFramingModel.ORIGIN); setUserRangeOrigin(origin); @@ -690,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}; } @@ -703,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 { @@ -713,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. @@ -729,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 SHRINK: - 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; default: @@ -764,14 +745,14 @@ 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: @@ -784,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); } @@ -839,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); } @@ -860,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 XYLegendWidget getLegend() { + return legend; } - public void setLegendWidget(XYLegendWidget legendWidget) { - this.legendWidget = legendWidget; + public void setLegend(XYLegendWidget legend) { + this.legend = legend; } - public XYGraphWidget getGraphWidget() { - return graphWidget; + public XYGraphWidget getGraph() { + return graph; } - public void setGraphWidget(XYGraphWidget graphWidget) { - this.graphWidget = graphWidget; + public void setGraph(XYGraphWidget graph) { + this.graph = graph; } - public TextLabelWidget getDomainLabelWidget() { - return domainLabelWidget; + public TextLabelWidget getDomainTitle() { + return domainTitle; } - public void setDomainLabelWidget(TextLabelWidget domainLabelWidget) { - this.domainLabelWidget = domainLabelWidget; + public void setDomainTitle(TextLabelWidget domainTitle) { + this.domainTitle = domainTitle; } - public TextLabelWidget getRangeLabelWidget() { - return rangeLabelWidget; + public void setDomainLabel(String domainLabel) { + getDomainTitle().setText(domainLabel); } - public void setRangeLabelWidget(TextLabelWidget rangeLabelWidget) { - this.rangeLabelWidget = rangeLabelWidget; + public TextLabelWidget getRangeTitle() { + return rangeTitle; } - /** - * Convenience method - wraps XYGraphWidget.getRangeValueFormat(). - * - * @return - */ - public Format getRangeValueFormat() { - return graphWidget.getRangeValueFormat(); + public void setRangeTitle(TextLabelWidget rangeTitle) { + this.rangeTitle = rangeTitle; } - /** - * Convenience method - wraps XYGraphWidget.setRangeValueFormat(). - * - * @param rangeValueFormat - */ - public void setRangeValueFormat(Format rangeValueFormat) { - graphWidget.setRangeValueFormat(rangeValueFormat); - } - - /** - * Convenience method - wraps XYGraphWidget.getDomainValueFormat(). - * - * @return - */ - public Format getDomainValueFormat() { - return graphWidget.getDomainValueFormat(); - } - - /** - * Convenience method - wraps XYGraphWidget.setDomainValueFormat(). - * - * @param domainValueFormat - */ - public void setDomainValueFormat(Format domainValueFormat) { - graphWidget.setDomainValueFormat(domainValueFormat); + public void setRangeLabel(String rangeLabel) { + getRangeTitle().setText(rangeLabel); } /** @@ -1079,12 +1016,16 @@ public synchronized void setRangeLowerBoundary(Number boundary, BoundaryMode mod setRangeFramingModel(XYFramingModel.EDGE); } + public XYCoords getOrigin() { + return calculatedOrigin; + } + public Number getDomainOrigin() { - return calculatedDomainOrigin; + return calculatedOrigin.x; } public Number getRangeOrigin() { - return calculatedRangeOrigin; + return calculatedOrigin.y; } public synchronized void setUserDomainOrigin(Number origin) { @@ -1102,65 +1043,22 @@ public synchronized void setUserRangeOrigin(Number origin) { } @SuppressWarnings("SameParameterValue") - protected void setDomainFramingModel(XYFramingModel model) { + protected void setDomainFramingModel(@NonNull XYFramingModel model) { constraints.setDomainFramingModel(model); } @SuppressWarnings("SameParameterValue") - protected void setRangeFramingModel(XYFramingModel model) { + protected void setRangeFramingModel(@NonNull XYFramingModel model) { constraints.setRangeFramingModel(model); } /** - * 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(); - } - - /** - * 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; } /** @@ -1198,9 +1096,7 @@ public YValueMarker removeMarker(YValueMarker marker) { * @return */ public int removeMarkers() { - int removed = removeXMarkers(); - removed += removeYMarkers(); - return removed; + return removeXMarkers() + removeYMarkers(); } /** @@ -1258,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; + public RectRegion getInnerLimits() { + return innerLimits; } - /** - * @return the rangeTopMax - */ - public Number getRangeTopMax() { - return rangeTopMax; - } - - /** - * @param rangeTopMax the rangeTopMax to set - */ - public synchronized void setRangeTopMax(Number rangeTopMax) { - this.rangeTopMax = rangeTopMax; + public RectRegion getOuterLimits() { + return outerLimits; } - /** - * @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; - } - - /** - * @return the domainRightMax - */ - public Number getDomainRightMax() { - return domainRightMax; - } - - /** - * @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 ae2bc93d..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,7 +18,8 @@ import android.content.Context; import android.graphics.Paint; -import com.androidplot.util.Configurator; + +import com.halfhp.fig.*; /** * Base class of all XYRegionFormatters. @@ -42,7 +43,11 @@ public class XYRegionFormatter { public XYRegionFormatter(Context ctx, int xmlCfgId) { // prevent configuration of classes derived from this one: if (getClass().equals(XYRegionFormatter.class)) { - Configurator.configure(ctx, this, xmlCfgId); + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java index 8eb2d226..5afb8963 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java @@ -47,5 +47,4 @@ public interface XYSeries extends Series { * @return The y-value. */ 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 6674ddc6..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; @@ -40,8 +40,8 @@ public XYSeriesRenderer(XYPlot plot) { public Hashtable getUniqueRegionFormatters() { Hashtable found = new Hashtable<>(); - for(SeriesAndFormatter sfPair : getSeriesAndFormatterList()) { - ZIndexable regionIndexer = sfPair.getFormatter().getRegions(); + 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 207283d9..5c714912 100644 --- a/androidplot-core/src/main/res/values/attrs.xml +++ b/androidplot-core/src/main/res/values/attrs.xml @@ -1,5 +1,5 @@ - + + - + - - - + + + @@ -40,9 +52,44 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -54,7 +101,7 @@ - + @@ -66,7 +113,7 @@ - + @@ -78,7 +125,7 @@ - + @@ -90,67 +137,76 @@ - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + @@ -159,7 +215,7 @@ - + @@ -168,7 +224,7 @@ - + @@ -177,7 +233,7 @@ - + @@ -186,7 +242,7 @@ - + @@ -195,7 +251,7 @@ - + @@ -204,7 +260,7 @@ - + @@ -213,7 +269,7 @@ - + @@ -222,147 +278,857 @@ - + + + + + + + + + + + - - - + + + + + + + + + - + - + - + + - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +`VerticalPositioning` component of the `VerticalPosition` of the `TextLabelWidget` +that displays the domain title. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. +--> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + - - - - - - - +Text alignment of line labels drawn on top edge of the plot. This alignment is applied relative +to the line label insets defined by `lineLabelInsetTop`. +--> + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + - - + + + + + + + - - - - - - + + + + - - + + + \ 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 index 0cf8c29e..5c861e55 100644 --- a/androidplot-core/src/main/res/values/style.xml +++ b/androidplot-core/src/main/res/values/style.xml @@ -18,29 +18,44 @@ @@ -67,71 +82,60 @@ @color/off_white @color/off_white @color/ap_white - @color/ap_gray - @color/ap_gray - @color/ap_charcoal - @color/ap_charcoal - @color/ap_charcoal - @color/ap_charcoal + @color/ap_gray + @color/ap_gray + @color/ap_charcoal + @color/ap_charcoal + @color/ap_charcoal + @color/ap_charcoal @color/ap_charcoal @color/ap_charcoal - @color/ap_black - @color/ap_black - @color/ap_black + @color/ap_black + @color/ap_black + @color/ap_black @color/ap_black - - - - - - \ No newline at end of file diff --git a/demoapp/src/main/res/values-ldpi/dimens.xml b/demoapp/src/main/res/xml/bubble_formatter1.xml similarity index 90% rename from demoapp/src/main/res/values-ldpi/dimens.xml rename to demoapp/src/main/res/xml/bubble_formatter1.xml index a58e6c64..795c43ce 100644 --- a/demoapp/src/main/res/values-ldpi/dimens.xml +++ b/demoapp/src/main/res/xml/bubble_formatter1.xml @@ -15,6 +15,6 @@ ~ limitations under the License. --> - - - \ No newline at end of file + \ No newline at end of file diff --git a/demoapp/src/main/res/values/attrs.xml b/demoapp/src/main/res/xml/bubble_formatter2.xml similarity index 82% rename from demoapp/src/main/res/values/attrs.xml rename to demoapp/src/main/res/xml/bubble_formatter2.xml index 6eb58380..549f153f 100644 --- a/demoapp/src/main/res/values/attrs.xml +++ b/demoapp/src/main/res/xml/bubble_formatter2.xml @@ -15,8 +15,6 @@ ~ limitations under the License. --> - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/demoapp/src/main/res/xml/bubble_formatter3.xml b/demoapp/src/main/res/xml/bubble_formatter3.xml new file mode 100644 index 00000000..ae13e260 --- /dev/null +++ b/demoapp/src/main/res/xml/bubble_formatter3.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/demoapp/src/main/res/xml/candlestick_formatter.xml b/demoapp/src/main/res/xml/candlestick_formatter.xml new file mode 100644 index 00000000..a72b5602 --- /dev/null +++ b/demoapp/src/main/res/xml/candlestick_formatter.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/demoapp/src/main/res/xml/line_point_formatter.xml b/demoapp/src/main/res/xml/line_point_formatter.xml new file mode 100644 index 00000000..6996d4c0 --- /dev/null +++ b/demoapp/src/main/res/xml/line_point_formatter.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/docs/advanced_xy_plot.md b/docs/advanced_xy_plot.md new file mode 100644 index 00000000..76b1552f --- /dev/null +++ b/docs/advanced_xy_plot.md @@ -0,0 +1,154 @@ +# Advanced XY Series Types +Androidplot offers several specialized implementations of XYSeries providing various performance +or usability enhancements. + +## EditableXYSeries +Enhances the standard XYSeries interface with edit methods. + +## FixedSizeEditableXYSeries +An implementation of EditableXYSeries that supports modifying x/y values and has been optimized +for speed. FixedSizeEditableXYSeries is optimized for data whose samples may frequently change but +whose absolute size doesn't change, such as an ECG (ring buffer) or an animated plot. + +## FastXYSeries +By default, Androidplot iterates over every element in each series every render cycle to +determine it's current min/max values. This is necessary in order to support dynamic plots where +data can change at any moment and invalidate the previously calculated min/max vals. + +FastXYSeries allows the developer to provide a more efficient algorithm to obtain min/max XVals +and avoid the high overhead of this iteration. For example, if you know your XVals will always +be in strict ascending order then the first and last XVal of the series will always contain the +min/max XVals respectively. + +XYSeries implementation supports a min/max algorithm implementation that is more efficient than doing +a comparison on each point of the series via iteration. It's a good idea to implement this interface +if your series will contain more than about 500 points + +## OrderedXYSeries +If the XVals of your series are in ascending order, implementing this interface provides a hint to +the series renderer that allows it to avoid iterating over points that are outside the screen's +visible domain. For larger data sets, implementing this interface can mean the difference between +smooth animations and freezing. + +## ScalingXYSeries +Wraps any other instance of XYSeries and provides a simple interface for dynamically +scaling x and/or y values. A popular use case for dynamic scaling is to create an animated intro +for your XYSeries where yVals increase (or decrease in the case of negative values) from 0 towards +their original value. The [AnimatedXYPlotActivity](../demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java) +in the DemoApp is one example on how this can be accomplished. + +## SampledXYSeries +SampledXYSeries is meant for use with extremely large datasets. Given a series, multiple sampled +series instances at stepped resolutions are generated for faster rendering. + +Basic usage: + +```java +XYSeries series = ... // instantiate an XYSeries however you want here + +// wrap our series in a SampledXYSeries with a threshold of 1000: +SampledXYSeries sampledSeries = + new SampledXYSeries(series, OrderedXYSeries.XOrder.ASCENDING, 2,100); + +// add the SampledXYSeries instance to the plot: +plot.addSeries(sampledSeries, formatter); +``` + +SampledXYSeries is meant to be used in conjunction with ZoomEstimator, which enables a plot to +automatically render using the resolution appropriate for the current screen boundaries, allowing +pan / zoom operations to perform with little or no degradation as series size increases. + +To enable ZoomEstimator: + +```java +// enable autoselect of sampling level based on visible boundaries: +plot.getRegistry().setEstimator(new ZoomEstimator()); +``` + +[The Touch Zoom Example source code](../demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java) provides a functional reference implementation. + +If you want to take advantage of the performance benefits of sampling but don't need pan/zoom support +check out the Sampling section below. + +## NormedXYSeries +A convenience wrapper to simplify normalizing XYSeries data in the range of 0 to 1. Usage is straightforward: + +```java +XYSeries normedSeries = new NormedXYSeries(series); +``` + +which is equivalent to: + +```java +XYSeries normedSeries = new NormedXYSeries(series, null, new Norm(null, 0, false)); +``` + +The first null argument pertains to the normalization being applied to the x axis values. While normalization +can certainly be applied on this axis, it's typically unused, which is why the single argument constructor +defaults to a null value here. + +Both examples auto normalize the passed in series, maximizing the resolution of the +output result. Sometimes however, it's desirable to control the output resolution for the purpose +of visually shifting the result up or down in the graph. This can be done by using the `NormedXYSeries.Norm` +constructor's second and third arguments: `offset` and `useOffsetCompression`. + +##### offset +This value is added to the original normalized value to effectively shift the series data up or down +within the normalized range. If the offset is large enough and offset compression is not used, this +can cause normalized values to exceed the norm range of 0 to 1. + +##### useOffsetCompression +If you want to shift a normalized series up or down in the graph but do not want the shift to potentially +move values offscreen, you can set `useOffsetComression` to true. This tells `NormedXYSeries` to shrink +the scale of the associated series relative to the offset being used to ensure that normed values +stay within the range of 0 to 1. If set to true and you supply an offset <0 or >1, an +`IllegalArgumentException` will be thrown. + +We've not talked about the first argument to the `NormedXYSeries.Norm` constructor: `minMax`. This is an +optional optimization value that reflects min/max values in the series data being passed in. If they +are known, you can pass these in to speed up normalization, otherwise just pass in null and they will +be auto calculated. + +IMPORTANT: If your series data is dynamic and it's min/max values change at runtime, you'll need to invoke +`NormedXYSeries.normalize(Norm, Norm)` immediately following each change in order to maintain accuracy. + +There's a [reference implementation](../demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java) +of a dual scale plot that demonstrates NormedXYSeries usage in the DemoApp. + +# Sampling +Series size is generally the biggest factor when it comes to rendering performance. If you're plotting +very large datasets, its generally a good idea to sample the data to improve performance. +For example, if you have an XYSeries that consists of 10,000 points, we can sample that data into +the 200 points that most accurately represent the profile of the original series: + +```java +// An instance of any implementation of XYSeries. Assume it's size is 10,000 +XYSeries originalSeries = ...; + +// Sampled series of size 200: +EditableXYSeries sampledSeries = new FixedSizeEditableXYSeries( + originalSeries.getTitle(), 200); + +// an instance of an implementation of Sampler: +Sampler sampler = ... + +// do the actual sampling: +new LTTBSampler().run(originalSeries, sampledSeries); +``` + +Currently LTTBSampler is the only available implementation. + +# Storing series data in onSaveInstanceState +If your series data requires a non trivial amount of preprocessing (subsampling etc.) or your data comes +from a dynamic source, you'll likely want to persist your series data when your Activity saves its +instance state. There are a few caveats to be aware of: + +* You can only persist about 1mb worth of data at a time so if your series data is much larger than that +you'll need to find a creative solution to the problem +* Due to [quirks in the way Android persists data](http://stackoverflow.com/questions/12300886/linkedlist-put-into-intent-extra-gets-recast-to-arraylist-when-retrieving-in-nex) +`XYSeries` implementations such as `SimpleXYSeries` that use `LinkedList` instances to store data cannot be serialized directly. +* Formatters generally cannot be persisted as they typically contain instances of `Paint` that cannot be serialized directly.. + +Due to these limitations we suggest storing `XYSeries` data into an array or `ArrayList` and using that to +instantiate your `XYSeries`. The DemoApp's [Time Series Example](../demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java) +contains a full source example. \ No newline at end of file diff --git a/docs/attrs.md b/docs/attrs.md new file mode 100644 index 00000000..afda917c --- /dev/null +++ b/docs/attrs.md @@ -0,0 +1,598 @@ +_This documentation is auto generated from [attrs.xml](../androidplot-core/src/main/res/values/attrs.xml)._ + +# Androidplot XML Attributes +Attributes are broken down by element followed by either their type or list of accepted values. +Supported Elements: + +* [Plot](#Plot) +* [XYPlot](#XYPlot) +* [PieChart](#PieChart) + +## Plot +Plot's attrs are available in all Plot types. + +### markupEnabled +__boolean__ + +### renderMode +* use_background_thread +* use_main_thread + +### marginTop +__dimension__ + +### marginBottom +__dimension__ + +### marginLeft +__dimension__ + +### marginRight +__dimension__ + +### paddingTop +__dimension__ + +### paddingBottom +__dimension__ + +### paddingLeft +__dimension__ + +### paddingRight +__dimension__ + +### title +__dimension__ + +### titleTextSize +__string__ + +### titleTextColor +__dimension__ + +### backgroundColor +__color__ + +### borderColor +__color__ + +### borderThickness +__color__ + +## XYPlot +XML attributes for the [XYPlot](xyplot.md) class. + +### previewMode +TODO + +### domainStepMode +* subdivide +* increment_by_val +* increment_by_pixels + +### domainStep +__dimension|float|integer__ + +### rangeStepMode +* subdivide +* increment_by_val +* increment_by_pixels + +### rangeStep +__dimension|float|integer__ + +### domainTitle +__string__ + +### domainTitleTextColor +__color__ + +### domainTitleTextSize +__dimension__ + +### domainTitleHeightMode +* absolute +* relative +* fill + +### domainTitleWidthMode +* absolute +* relative +* fill + +### domainTitleHeight +__dimension|float|integer__ + +### domainTitleWidth +__dimension|float|integer__ + +### domainTitleHorizontalPositioning +* absolute_from_left +* absolute_from_right +* absolute_from_center +* relative_from_left +* relative_from_right +* relative_from_center + +`HorizontalPositioning` component of the `HorizontalPosition` of the `TextLabelWidget` +that displays the domain title. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### domainTitleVerticalPositioning +* absolute_from_top +* absolute_from_bottom +* absolute_from_center +* relative_from_top +* relative_from_bottom +* relative_from_center + +`VerticalPositioning` component of the `VerticalPosition` of the `TextLabelWidget` +that displays the domain title. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### domainTitleHorizontalPosition +__dimension|float|integer__ + +`float` value component of the `HorizontalPosition` of the `TextLabelWidget` +that displays the domain title. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### domainTitleVerticalPosition +__dimension|float|integer__ + +`float` value component of the `VerticalPosition` of the `TextLabelWidget` +that displays the domain title. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### domainTitleAnchor +* top_middle +* left_top +* left_middle +* left_bottom +* right_top +* right_middle +* right_bottom +* bottom_middle +* center + +### domainTitleVisible +__boolean__ + +### rangeTitle +__string__ + +### rangeTitleTextColor +__color__ + +### rangeTitleTextSize +__dimension__ + +### rangeTitleHeightMode +* absolute +* relative +* fill + +### rangeTitleWidthMode +* absolute +* relative +* fill + +### rangeTitleHeight +__dimension|float|integer__ + +### rangeTitleWidth +__dimension|float|integer__ + +### rangeTitleHorizontalPositioning +* absolute_from_left +* absolute_from_right +* absolute_from_center +* relative_from_left +* relative_from_right +* relative_from_center + +### rangeTitleVerticalPositioning +* absolute_from_top +* absolute_from_bottom +* absolute_from_center +* relative_from_top +* relative_from_bottom +* relative_from_center + +### rangeTitleHorizontalPosition +__dimension|float|integer__ + +### rangeTitleVerticalPosition +__dimension|float|integer__ + +### rangeTitleAnchor +* top_middle +* left_top +* left_middle +* left_bottom +* right_top +* right_middle +* right_bottom +* bottom_middle +* center + +### rangeTitleVisible +__boolean__ + +### drawGridOnTop +__boolean__ +
+(default is false) When set to true, grid lines are drawn on top of rendered series data +instead of underneath. + +### graphHeightMode +* absolute +* relative +* fill + +### graphWidthMode +* absolute +* relative +* fill + +### graphHeight +__dimension|float|integer__ + +### graphWidth +__dimension|float|integer__ + +### graphRotation +* none +* ninety_degrees +* negative_ninety_degrees +* one_hundred_eighty_degrees + +### graphHorizontalPositioning +* absolute_from_left +* absolute_from_right +* absolute_from_center +* relative_from_left +* relative_from_right +* relative_from_center + +`HorizontalPositioning` component of the `HorizontalPosition` of the `XYGraphWidget`. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### graphVerticalPositioning +* absolute_from_top +* absolute_from_bottom +* absolute_from_center +* relative_from_top +* relative_from_bottom +* relative_from_center + +`VerticalPositioning` component of the `VerticalPosition` of the `XYGraphWidget`. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### graphHorizontalPosition +__dimension|float|integer__ + +`float` value component of the `HorizontalPosition` of the `XYGraphWidget`. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### graphVerticalPosition +__dimension|float|integer__ + +`float` value component of the `VerticalPosition` of the `XYGraphWidget`. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### graphAnchor +* top_middle +* left_top +* left_middle +* left_bottom +* right_top +* right_middle +* right_bottom +* bottom_middle +* center + +### graphVisible +__boolean__ + +### graphMarginTop +__dimension__ + +### graphMarginBottom +__dimension__ + +### graphMarginLeft +__dimension__ + +### graphMarginRight +__dimension__ + +### graphPaddingTop +__dimension__ + +### graphPaddingBottom +__dimension__ + +### graphPaddingLeft +__dimension__ + +### graphPaddinRight +__dimension__ + +### gridClippingEnabled +__boolean__ + +### gridInsetTop +__dimension__ + +### gridInsetBottom +__dimension__ + +### gridInsetLeft +__dimension__ + +### gridInsetRight +__dimension__ + +### lineLabelInsetTop +__dimension__ +
+Top edge of a rectangle relative to the XYGraphWidget +border upon which line labels will be anchored. + +### lineLabelInsetBottom +__dimension__ +
+Bottom edge of a rectangle relative to the XYGraphWidget +border upon which line labels will be anchored. + +### lineLabelInsetLeft +__dimension__ +
+Left edge of a rectangle relative to the XYGraphWidget +border upon which line labels will be anchored. + +### lineLabelInsetRight +__dimension__ +
+Right edge of a rectangle relative to the XYGraphWidget +border upon which line labels will be anchored. + +### lineLabels +* top +* bottom +* left +* right + +Enable line labels on one or more edge of the graph. For example, to enable labels on the left +and bottom edges: +``` +ap:lineLabels="left|bottom" +``` + +### lineLabelAlignTop +* left +* center +* right + +Text alignment of line labels drawn on top edge of the plot. This alignment is applied relative +to the line label insets defined by `lineLabelInsetTop`. + +### lineLabelAlignBottom +* left +* center +* right + +Text alignment of line labels drawn on bottom edge of the plot. This alignment is applied relative +to the line label insets defined by `lineLabelInsetBottom`. + +### lineLabelAlignLeft +* left +* center +* right + +Text alignment of line labels drawn on left edge of the plot. This alignment is applied relative +to the line label insets defined by `lineLabelInsetLeft`. + +### lineLabelAlignRight +* left +* center +* right + +Text alignment of line labels drawn on right edge of the plot. This alignment is applied relative +to the line label insets defined by `lineLabelInsetRight`. + +### lineLabelRotationTop +__float__ +
+Angle at which line labels on the plot's top edge are drawn. + +### lineLabelRotationBottom +__float__ +
+Angle at which line labels on the plot's bottom edge are drawn. + +### lineLabelRotationLet +__float__ +
+Angle at which line labels on the plot's left edge are drawn. + +### lineLabelRotationRight +__float__ +
+Angle at which line labels on the plot's right edge are drawn. + +### domainLineThickness +__dimension__ + +### rangeLineThickness +__dimension__ + +### domainLineColor +__color__ + +### rangeLineColor +__color__ + +### domainOriginLineThickness +__dimension__ + +### rangeOriginLineThickness +__dimension__ + +### domainOriginLineColor +__color__ + +### rangeOriginLineColor +__color__ + +### lineLabelTextSizeTop +__dimension__ + +### lineLabelTextSizeBottom +__dimension__ + +### lineLabelTextSizeLeft +__dimension__ + +### lineLabelTextSizeRight +__dimension__ + +### lineLabelTextColorTop +__color__ + +### lineLabelTextColorBottom +__color__ + +### lineLabelTextColorLeft +__color__ + +### lineLabelTextColorRight +__color__ + +### lineExtensionTop +__dimension__ + +### lineExtensionBottom +__dimension__ + +### lineExtensionLeft +__dimension__ + +### lineExtensionRight +__dimension__ + +### gridBackgroundColor +__color__ +
+background color of the grid portion of the XYGraphWidget + +### graphBackgroundColor +__color__ +
+background color of the XYGraphWidget + +### legendHeightMode +* absolute +* relative +* fill + +### legendWidthMode +* absolute +* relative +* fill + +### legendHeight +__dimension|float|integer__ + +### legendWidth +__dimension|float|integer__ + +### legendHorizontalPositioning +`HorizontalPositioning` component of the `HorizontalPosition` of the `XYLegendWidget`. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. +* absolute_from_left +* absolute_from_right +* absolute_from_center +* relative_from_left +* relative_from_right +* relative_from_center + +### legendVerticalPositioning +`VerticalPositioning` component of the `VerticalPosition` of the `XYLegendWidget`. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. +* absolute_from_top +* absolute_from_bottom +* absolute_from_center +* relative_from_top +* relative_from_bottom +* relative_from_center + +### legendHorizontalPosition +__dimension|float|integer__ + +`float` value component of the `HorizontalPosition` of the `XYLegendWidget`. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### legendVerticalPosition +__dimension|float|integer__ + +`float` value component of the `VerticalPosition` of the `XYLegendWidget`. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. + +### legendAnchor +* top_middle +* left_top +* left_middle +* left_bottom +* right_top +* right_middle +* right_bottom +* bottom_middle +* center + +### legendTextSize +__dimension__ + +### legendTextColor +__color__ + +### legendIconHeightMode +* absolute +* relative +* fill + +### legendIconWidthMode +* absolute +* relative +* fill + +### legendIconHeight +__dimension|float|integer__ + +### legendIconWidth +__dimension|float|integer__ + +### legendVisible +__boolean__ + +### pieBorderThickness +__dimension__ +
+Determines how far beyond the graph's edge each domain grid line will extend. + +### pieBorderThickness +__dimension__ +
+Determines how far beyond the graph's edge each range grid line will extend. + +## PieChart +TODO + +### pieBorderColor +__color__ + +### pieBorderThickness +__dimension__ + diff --git a/docs/barchart.md b/docs/barchart.md new file mode 100644 index 00000000..5b384250 --- /dev/null +++ b/docs/barchart.md @@ -0,0 +1,132 @@ +# Bar Charts +Bar Charts are drawn with `BarRenderer` by converting xy values of an `XYSeries` into +vertical bars. The height of the vertical bar is calculated from the yVal and the base +of the bar is calculated from the plot's range origin value. + +![image](images/screens/bar_horiz.png) + +# Basic Usage +Rendering an `XYSeries` as a bar chart is as simple as adding the series to an `XYPlot` with an +instance of `BarFormatter`: + +```java +// create a bar formatter with a red fill color and a white outline: +BarFormatter bf = new BarFormatter(Color.RED, Color.WHITE); +plot.addSeries(series, bf); +``` + +# BarRenderer +Unlike most other renderers, much of the visual configuration of a bar chart is configured through it's `BarRenderer`. +`BarRenderer` provides methods for setting how the width of each bar should be calculated, the space between each `Bar` +and what style of visual grouping to use. + + +##### Getting the BarRenderer Instance +Many of the topics below require access to the `BarRenderer` instance to be set. Each instance of +`XYPlot` contains it's own unique `Renderer` instances. To retrieve the `BarRenderer` +instance from an `XYPlot`: + +```java +BarRenderer renderer = plot.getRenderer(BarRenderer.class); +``` + +# BarGroup +A `BarGroup` is automatically generated to group values from one or more series by their index values. +All `Bar` instances belong to a `BarGroup`, even if there is only a single `XYSeries` that exists. + +A common use case of bar charts is to represent a group of two or more values for a given interval as +individual bars, for example the number of wins and losses for a baseball team for every month over the course +of a year. + +To model this in Androidplot, create two instances of `XYSeries`; one for wins and one for losses, each +with exactly 12 elements (one for each day of the month): + +```java +XYSeries wins = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "wins", 3, 4, 5, 3, 2, 3, 5, 6, 2, 1, 3, 1); +XYSeries losses = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "losses", 0, 1, 1, 0, 1, 0, 0, 0, 2, 1, 0, 1); +``` + +Each series is then added to the plot with it's own formatter: + +```java +// draw wins bars with a green fill: +BarFormatter winsFormatter = new BarFormatter(Color.GREEN, Color.BLACK); +plot.addSeries(wins, winsFormatter); + +// draw losses bars with a red fill: +BarFormatter lossesFormatter = new BarFormatter(Color.RED, Color.BLACK); +plot.addSeries(losses, lossesFormatter); +``` + +The pairs of wins/losses bars are then drawn side-by-side for each of the 12 indexes. Androidplot +knows to do this because `BarRenderer` (the renderer used to draw series associated with a `BarFormatter`) +extends [GroupRenderer](grouprenderer.md). + +# BarRenderer Styles +`BarRenderer` provides three grouping styles that may be used when rendering two or more XYSeries: +`OVERLAID`, `STACKED`, `SIDE_BY_SIDE` + +##### OVERLAID (default) +Bars in the same grouping are overlaid on each-other, the bars being drawn by yVal in descending order. + +![image](images/bargroup_overlaid.png) + + +To use: +```java +barRenderer.setBarOrientation(BarRenderer.BarOrientation.OVERLAID); +``` + +##### STACKED +Bars in the same group are stacked on top of each-other. Limitations: +* Range Origin must be set to 0. +* All `XYSeries` must contain no negative values. + +![image](images/bargroup_stacked.png) + +To use: +```java +barRenderer.setBarOrientation(BarRenderer.BarOrientation.STACKED); +``` + +##### SIDE_BY_SIDE +Bars in the same group are drawn next to one another. + +![image](images/bargroup_side_by_side.png) + +To use: +```java +barRenderer.setBarOrientation(BarRenderer.BarOrientation.SIDE_BY_SIDE); +``` + +By default there is no spacing between bars in the same `BarGroup` in this mode. You can add spacing +by setting a left and right margin on your BarFormatter instances: + +```java +barFormatter.setMarginLeft(PixelUtils.dpToPix(1)); +barFormatter.setMarginRight(PixelUtils.dpToPix(1)); +``` + +# BarGroup Widths & Spacing +When configuring BarGroup widths and spacing, there are two mutually exclusive methods that can be used; +`FIXED_WIDTH` and `FIXED_GAP`. + +![image](images/bargroup_spacing.png) + +##### FIXED_WIDTH +The exact size of the `BarGroup` is specified in pixels and the space between each `BarGroup` +is dynamically calculated based on that size. + +```java +barRenderer.setBarGroupWidth(BarRenderer.BarGroupWidthMode.FIXED_WIDTH, PixelUtils.dpToPix(25)); +``` +##### FIXED_GAP +The exact size of the "gap" between each `BarGroup` is specified in pixels and the size of each `BarGroup` +is dynamically calculated based on that spacing. + +```java +barRenderer.setBarGroupWidth(BarRenderer.BarGroupWidthMode.FIXED_GAP, PixelUtils.dpToPix(5)); +``` + +# Example +See the DemoApp's [bar chart example source](../demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java). \ No newline at end of file diff --git a/docs/bubblechart.md b/docs/bubblechart.md new file mode 100644 index 00000000..bef52e45 --- /dev/null +++ b/docs/bubblechart.md @@ -0,0 +1,53 @@ +# Bubble Charts +A `BubbleChart` is a two dimensional representation of three dimensional data +on an `XYPlot`, where the xy values are drawn as a circle with a radius representing the zVal. + +![image](images/screens/bubble_horiz.png) + +# Basic Usage +The first step in creating a `BubbleChart` is to define the data to be plotted. We'll start +by creating an instance of `BubbleSeries`: + +Using the implicit iVal for x: + +```java +BubbleSeries series1 = new BubbleSeries( + Arrays.asList(new Number[]{3, 5, 2, 3, 6}), // xCoordinate + Arrays.asList(new Number[]{1, 5, 2, 2, 3}), "s1"); // yCoordinate +``` + +Or it's equivalent four argument counterpart: + +```java +BubbleSeries bubbleSeries = new BubbleSeries( + Arrays.asList(new Number[]{0, 1, 2, 3, 4}), // xCoordinate + Arrays.asList(new Number[]{3, 5, 2, 3, 6}), // yCoordinate + Arrays.asList(new Number[]{1, 5, 2, 2, 3}), // zVal (corresponds to radius) + "s1"); +``` + +Next, create a `Formatter` defining the fill and outline colors of the bubbles: + + +```java +// draw bubbles with a green fill and white outline: +BubbleFormatter formatter = new BubbleFormatter(Color.GREEN, Color.WHITE) +``` + +Finally, add the `BubbleSeries` to our plot as you would any other `XYSeries` instance: + +```java +plot.addSeries(bubbleSeries, formatter); +``` + +# BubbleScaleMode +By default, `BubbleRenderer` scales each rendered bubble radius using the square root of it's corresponding +zVal, preventing apparent size differences in each bubble radius from being visually misleading. See: https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly + +If you'd prefer to use a linear scale: + +```java +plot.getRenderer(BubbleRenderer.class).setBubbleScaleMode(BubbleRenderer.BubbleScaleMode.LINEAR); +``` +# Example +Check out the [bubble chart example source](../demoapp/src/main/java/com/androidplot/demos/BubbleChartActivity.java) for a full source example of a bubble chart. diff --git a/docs/candlestick.md b/docs/candlestick.md new file mode 100644 index 00000000..778117e9 --- /dev/null +++ b/docs/candlestick.md @@ -0,0 +1,77 @@ +# Candlestick Charts +Candlestick charts are a special kind of `XYPlot` usually used for rendering financial information. +Each "candlestick" is composed of 4 values: open, close, high and low. + +![image](images/screens/candlestick_horiz.png) + +Androidplot's implementation of candlestick charts uses four separate `XYSeries` instances to represent +the candlestick values; each series corresponds to one of the four values that composes each "candlestick". + +For example: + +```java +Number[] highVals = new Number[] {12, 10, 15, 8, 7}; +Number[] lowVals = new Number[] {3, 1, 5, 0, 2}; +Number[] openVals = new Number[] {5, 2, 7, 5, 3}; +Number[] closeVals = new Number[] {7, 9, 6, 0, 4}; +``` + +Using the values above, the first candlestick model would be `{high=12, low=3, open=5, close=7}`, +the second would be `{high=10, low=1, open=2, close=7}` and so on. + +There are a few constraints that must be satisfied before we can create XYSeries from the data above and add them to Androidplot: + +* Each series must be of the same size. +* Each series must have the same x(i); getX(i) must return the same value for each series. +* There must be exactly four series, added in the precise order: high, low, open, close. +* The series data must make sense; each candlestick's high must be equal to or greater than all other +values in the candlestick, and each candlestick's low must be less than or equal to all other values. +* Each candlestick series belonging to the same candlestick chart must use the same `CandlestickFormatter` +instance when being added to the XYPlot. + +While it's possible to satisfy these constraints by creating each of the four `XYSeries` required by a +candlestick chart, adding them in order using the same formatter, many projects will find it simpler +to interact with the data as though it were a single series of candlestick values. To makr this easier, +two convenience classes: `CandlestickSeries` and `CandlestickMaker`. Here's how they are used together to add a candlestick chart to an `XYPlot`: + +```java +CandlestickSeries candlestickSeries = new CandlestickSeries( + new CandlestickSeries.Item(1, 10, 2, 9), + new CandlestickSeries.Item(4, 18, 6, 5), + new CandlestickSeries.Item(3, 11, 5, 10), + new CandlestickSeries.Item(2, 17, 2, 15), + new CandlestickSeries.Item(6, 11, 11, 7), + new CandlestickSeries.Item(8, 16, 10, 15)); + +CandlestickFormatter formatter = new CandlestickFormatter(); + +// add the candlestick series data to the plot: +CandlestickMaker.make(plot, formatter, candlestickSeries); +``` + +It's necessary to use `CandlestickMaker` to add the `CandlestickSeries` (via "make") to the +`XYPlot` as `CandlestickSeries` does not and cannot implement the `XYSeries` interface. + + +[A full source example is available here](../demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java). + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..cad3ef5f --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,56 @@ +# Contributing Soure Code +Contributions are always welcome! If you've got an idea for a specific feature and would like feedback +on implementation or if you want to contribute but don't know where to start, feel free to [ +start a topic in the Forums](https://groups.google.com/forum/#!forum/androidplot). If you already have +a feature or bugfix to submit, take a look at the code style notes below and then submit a Pull Request. + +## Code Style +For familiarity and simplicity, Androidplot uses [Android's code style guide](https://source.android.com/source/code-style.html). +In particular: + +* Indentations are 4 spaces each - no tab characters. +* Braces do not go on their own line; they go on the same line as the code before them. +* Line length should be a max of 100 characters. +* Don't catch generic Exception and don't ignore caught exceptions. +* Classes and methods should be documented with standard Javadoc comments. + +## Pull Requests +If you have code changes you'd like to share with the project, issue a [Pull Request](https://help.github.com/articles/about-pull-requests/) against the master +branch of Androidplot. Please make sure that: + +* Difficult to follow logical statements, etc. are reasonably documented. +* Commented out lines, unused methods / dead code blocks have been removed. +* Unit Tests have have been refactored appropriately and all pass. +* A clear description of the change is included in the Pull Request. + +Adding Unit Tests for new methods / functionality is highly encouraged. + +# Git Newcomers +If you're new to Git, this section will show you the basics of checking out a local copy of the project and building it. + +## Clone +Androidplot is hosted on Github. If you've got a git client installed, you can either +[fork](https://help.github.com/articles/fork-a-repo/) the project into your own +github account, or you can clone the project to your workstation using this command: + +``` +git clone https://github.com/halfhp/androidplot.git +``` + +You can also [download an ZIP](https://github.com/halfhp/androidplot/archive/master.zip) of the master branch of Androidplot. + +## Build +Androidplot is a Gradle project, which makes building incredibly easy. To perform a full build (runs tests, +generates javadocs and builds the library and demo app) from the command line: + +``` +./gradlew test assemble +``` +You can also of course use the IDE of your choice instead. [Android Studio](https://developer.android.com/studio/index.html) is highly recommended. + +## Useful Links +If you're new to Git these links will get you started: + +* [Setting up Git](https://help.github.com/articles/set-up-git/) Start here if you don't have a Git client installed. +* [Pro Git (free eBook)](https://git-scm.com/book/en/v2) - _The_ book to read when it comes to learning Git +* [Git Reference](https://git-scm.com/docs) - Good set of links for general reference \ No newline at end of file diff --git a/docs/custom_renderer.md b/docs/custom_renderer.md new file mode 100644 index 00000000..73cac354 --- /dev/null +++ b/docs/custom_renderer.md @@ -0,0 +1,101 @@ +# Custom Renderers +Androidplot renderers can be extended with extra functionality in the form of custom renderers. +For the example below we'll look at creating a custom implementation of BarRenderer that will +draw bars with rounded edges. While this example demonstrates an `XYPlot`, the steps apply to +`PieChart` etc. as well. + +## Create the Formatter +The first step to creating a custom renderer is defining it's `Formatter`. Aside from providing +the visual configuration used by the `Renderer` to draw series data, it's also used by Androidplot +to map a series to a specific renderer type and if necessary, obtain a new instance of that renderer. +Here's the most basic implementation: + +```java +class RoundedBarFormatter extends BarFormatter { + + { + // for now we'll hardcode some formatting values. + // a real implementation would probably provide a constructor instead + getBorderPaint().setColor(Color.WHITE); + getFillPaint().setColor(Color.RED); + } + + @Override + public Class getRendererClass() { + return RoundedBarRenderer.class; + } + + @Override + public RoundedBarRenderer doGetRendererInstance(XYPlot xyPlot) { + return new RoundedBarRenderer(xyPlot); + } +} +``` + +## Create the Renderer +All we want to do here is tweak the way `BarRenderer` behaves a little so we'll use it as our base class: + +```java +class RoundedBarRenderer extends BarRenderer { + + public RoundedBarRenderer(XYPlot plot) { + super(plot); + } + + @Override + protected void drawBar(Canvas canvas, Bar bar, RectF rect) { + // TODO this is where we'll add our custom behavior + } +} +``` + +In the case of our RoundedBarFormatter, the behavior we need to change all exits within the +`drawBar(Canvas, Bar, RectF)` method. The existing implementation uses `Canvas.drawRect(...)` to +draw bars. We could use a stroke with rounded edges, but this would cause the edges on both the top +and the bottom to be rounded, which won't look right. We'll draw using a path instead: + +```java +@Override +protected void drawBar(Canvas canvas, Bar bar, RectF rect) { + + // skip nulls: + if(bar.getY() == null) { + return; + } + + RoundedBarFormatter formatter = getFormatter(bar.i, bar.series); + if(formatter == null) { + formatter = bar.formatter; + } + + // don't need to draw if the bar lacks height or width: + if (rect.height() > 0 && rect.width() > 0) { + final Path path = new Path(); + final float arcHeightPx = 20; + final float adjustedTop = rect.top + arcHeightPx; + final RectF cap = new RectF(rect.left, rect.top, rect.right,rect.top + 2 * arcHeightPx); + + // start at bottom-left and move clock-wise around the shape: + path.moveTo(rect.left, rect.bottom); + path.lineTo(rect.left, adjustedTop); + path.arcTo(cap, 180, 180, false); + path.lineTo(rect.right, rect.bottom); + path.close(); + + canvas.drawPath(path, formatter.getFillPaint()); + canvas.drawPath(path, formatter.getBorderPaint()); + } +} +``` + +## Use It +Using the new custom renderer is as easy as adding a series to a plot using `RoundedBarFormatter`: + +```java +plot.addSeries(series1, new RoundedBarFormatter(Color.RED)); +plot.addSeries(series2, new RoundedBarFormatter(Color.BLUE)); +``` + +Here's what this would look like used in the demo app's `SimpleXYPlotActivity`: + +![image](images/screens/rounded_bar_renderer.png) diff --git a/docs/dynamicdata.md b/docs/dynamicdata.md new file mode 100644 index 00000000..9aad4679 --- /dev/null +++ b/docs/dynamicdata.md @@ -0,0 +1,88 @@ +# Plotting Dynamic Data +One of Androidplot's core focuses is on plotting dynamically changing data in real-time. Special care has +been put into the design of the library to provide solutions for various scenarios and synchronization concerns. + +# Background Rendering +By default, Androidplot does all of it's rendering on the UI thread. While this is fine for most applications, +it can cause performance issues when rendering larger datasets or when continuously redrawing dynamic data. +To resolve this issue, Androidplot provides a background rendering mode that can be enabled programmatically in +your Java source or via XML: + +**Programmatically:** +```java +plot.setRenderMode(Plot.RenderMode.USE_BACKGROUND_THREAD); +``` + +**XML:** +```xml +ap:renderMode="use_background_thread" +``` + +In general, if your plot is continuously redrawing the plot, you should use background rendering. + +#Rendering Dynamic Data +There are two general approaches to dynamically rendering data: event driven and render loops. Each has +pros and cons and often times, the application might force you to use one approach over the other, however +90% of the time using a render loop is the better approach. + +In both cases the redraw is triggered by invoking `Plot.redraw().` + +## Render Loops +A render loop is basically a continuous loop of calls to a render routine. This loop might +include logic for updating the data being plotted, or it might simply focus on maintaining a stable refresh rate. +Androidplot provides a convenience utility, Redrawer which provides a basic implementation of a render loop +running on a fixed frequency. Check out the [ECG demo source](../demoapp/src/main/java/com/androidplot/demos/ECGExample.java) for a working example. + +## Event Driven Redraws +Sometimes it's more efficient to only redraw the plot as a result of an event such as a GPS update, button click, etc. +Event driven updates are as simple as invoking `Plot.redraw()` from the callback handling the event of interest, +after the data being plotted has been updated. While it's possible for the event triggering the redraw +to fire at a faster rate than the plot is capable of redrawing, no special care needs to be taken as Androidplot +ignores subsequent invocations of redraw() when a previous invocation is already active. + +# Synchronization +A major challenge of plotting dynamic data is the need to render an accurate representation of the data +as it existed at a specific point in time. This is not a trivial problem and the right solution depends +greatly on the specific details of the project. Presented here are a list of items to keep in mind +along with general best practices and approaches to solving more common problems. + +The most common scenario is when your data model is being dynamically updated by a background thread. +Regardless of whether or not you use background rendering here, invoking plot.redraw() is asynchronous +and your background thread can easily loop around and start altering values while your plot is half way through +rendering the previous set of values. The result of a race condition is indeterminate; if the size of your data +changed the app could crash from an IndexOutOfBoundsException, or a bogus representation of your data might +be rendered to the display. + +To prevent the race condition, changes to the data model must be synchronized with Androidplot's +rendering loop. A PlotListener can be used to detect when a Plot begins and ends a redraw and a read-write +can be used to protect the model (typically any series data being drawn): + +```java +plot.addListener(new PlotListener() { + @Override + public void onBeforeDraw(Plot source, Canvas canvas) { + // write-lock each active series for writes + } + + @Override + public void onAfterDraw(Plot source, Canvas canvas) { + // unlock any locked series + } +}); +``` +Note that if your custom series implements the PlotListener interface, it's onBeforeDraw and onAfterDraw +methods will be automatically called when the series is rendered, without the need for adding it +as a listener to the plot. SimpleXYSeries provides a reference implementation of this synchronization +technique. + +In-depth usage of thread synchronization and read-write locks is beyond scope for this doc but you can +check out the source code of the SimpleXYSeries class for a functional example of ReentrantReadWriteLock +being used to synchronize an XYSeries implementation. + +# Examples +Androidplot provides a few full-source examples of how to dynamically plot data as demonstrated +in the [demo app](https://play.google.com/store/apps/details?id=com.androidplot.demos&hl=en). + +* [Plotting Realtime Orientation Sensor Data](../demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java) +* [An ECG Example](../demoapp/src/main/java/com/androidplot/demos/ECGExample.java) +* [A simple dynamic XYPlot](../demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java) \ No newline at end of file diff --git a/docs/grouprenderer.md b/docs/grouprenderer.md new file mode 100644 index 00000000..8a8ce9d1 --- /dev/null +++ b/docs/grouprenderer.md @@ -0,0 +1,18 @@ +# GroupRenderer +`GroupRenderer` is a special implementation of `XYSeriesRenderer` that combines multiple instances +of `XYSeries` into a single virtual series of s higher dimension. Examples of `GroupRenderer` are: + +* BarRenderer +* CandlestickRenderer + +Let's take a quick look at `CandlestickRenderer`. A candlestick chart is basically a two two dimensional +representation of a complex value; for every value of x a single candlestick is drawn, but there are +four values (dimensions) that make up each candlestick: + +* open +* close +* high +* low + +When a series is added to the plot with a formatter associated with a renderer that extends `GroupRenderer`, +all series added with formatters of the same type are automatically "grouped" together during rendering. \ No newline at end of file diff --git a/docs/images/aplogo.png b/docs/images/aplogo.png new file mode 100644 index 00000000..1d1f8adc Binary files /dev/null and b/docs/images/aplogo.png differ diff --git a/docs/images/aplogo_small.png b/docs/images/aplogo_small.png new file mode 100644 index 00000000..1dbda884 Binary files /dev/null and b/docs/images/aplogo_small.png differ diff --git a/docs/images/bargroup_overlaid.png b/docs/images/bargroup_overlaid.png new file mode 100644 index 00000000..39ed7a41 Binary files /dev/null and b/docs/images/bargroup_overlaid.png differ diff --git a/docs/images/bargroup_side_by_side.png b/docs/images/bargroup_side_by_side.png new file mode 100644 index 00000000..938c346a Binary files /dev/null and b/docs/images/bargroup_side_by_side.png differ diff --git a/docs/images/bargroup_spacing.png b/docs/images/bargroup_spacing.png new file mode 100644 index 00000000..463cca62 Binary files /dev/null and b/docs/images/bargroup_spacing.png differ diff --git a/docs/images/bargroup_stacked.png b/docs/images/bargroup_stacked.png new file mode 100644 index 00000000..e2c1256e Binary files /dev/null and b/docs/images/bargroup_stacked.png differ diff --git a/docs/images/bubble_chart.png b/docs/images/bubble_chart.png new file mode 100644 index 00000000..b6bdac8d Binary files /dev/null and b/docs/images/bubble_chart.png differ diff --git a/docs/images/markup_mode.jpg b/docs/images/markup_mode.jpg new file mode 100644 index 00000000..e3ceb175 Binary files /dev/null and b/docs/images/markup_mode.jpg differ diff --git a/docs/images/pie_donut_size.png b/docs/images/pie_donut_size.png new file mode 100644 index 00000000..2fb20380 Binary files /dev/null and b/docs/images/pie_donut_size.png differ diff --git a/docs/images/pie_extent.png b/docs/images/pie_extent.png new file mode 100644 index 00000000..919e19ff Binary files /dev/null and b/docs/images/pie_extent.png differ diff --git a/docs/images/pie_inner_inset.png b/docs/images/pie_inner_inset.png new file mode 100644 index 00000000..0fc8172c Binary files /dev/null and b/docs/images/pie_inner_inset.png differ diff --git a/docs/images/pie_offset.png b/docs/images/pie_offset.png new file mode 100644 index 00000000..cf204470 Binary files /dev/null and b/docs/images/pie_offset.png differ diff --git a/docs/images/pie_orientation.png b/docs/images/pie_orientation.png new file mode 100644 index 00000000..1ee46acc Binary files /dev/null and b/docs/images/pie_orientation.png differ diff --git a/docs/images/pie_outer_inset.png b/docs/images/pie_outer_inset.png new file mode 100644 index 00000000..d0ce79f3 Binary files /dev/null and b/docs/images/pie_outer_inset.png differ diff --git a/docs/images/pie_radial_inset.png b/docs/images/pie_radial_inset.png new file mode 100644 index 00000000..32ff6aa2 Binary files /dev/null and b/docs/images/pie_radial_inset.png differ diff --git a/docs/images/plot_anatomy.png b/docs/images/plot_anatomy.png new file mode 100644 index 00000000..25c5b374 Binary files /dev/null and b/docs/images/plot_anatomy.png differ diff --git a/docs/images/positioning/bottom-right-absolute-with-offset.png b/docs/images/positioning/bottom-right-absolute-with-offset.png new file mode 100644 index 00000000..fa575feb Binary files /dev/null and b/docs/images/positioning/bottom-right-absolute-with-offset.png differ diff --git a/docs/images/positioning/bottom-right-absolute.png b/docs/images/positioning/bottom-right-absolute.png new file mode 100644 index 00000000..9d1a4908 Binary files /dev/null and b/docs/images/positioning/bottom-right-absolute.png differ diff --git a/docs/images/positioning/center-absolute.png b/docs/images/positioning/center-absolute.png new file mode 100644 index 00000000..b7e803cf Binary files /dev/null and b/docs/images/positioning/center-absolute.png differ diff --git a/docs/images/positioning/top-left-absolute.png b/docs/images/positioning/top-left-absolute.png new file mode 100644 index 00000000..d7452a35 Binary files /dev/null and b/docs/images/positioning/top-left-absolute.png differ diff --git a/docs/images/preview.png b/docs/images/preview.png new file mode 100644 index 00000000..57e4d20c Binary files /dev/null and b/docs/images/preview.png differ diff --git a/docs/images/screens/about_screens.md b/docs/images/screens/about_screens.md new file mode 100644 index 00000000..63ffdbf3 --- /dev/null +++ b/docs/images/screens/about_screens.md @@ -0,0 +1,7 @@ +# About Androidplot Screenshots +Screenshots should be added to this directory for each new plot type. These screens should +conform to the following guidelines: + +* Screenshots framed in Nexus5x, no glare, no shadows +* Horizontal screen dimensions: 950w * 484h +* Vertical screen dimensions 484w * 950h \ No newline at end of file diff --git a/docs/images/screens/bar_horiz.png b/docs/images/screens/bar_horiz.png new file mode 100644 index 00000000..8210d010 Binary files /dev/null and b/docs/images/screens/bar_horiz.png differ diff --git a/docs/images/screens/bar_vert.png b/docs/images/screens/bar_vert.png new file mode 100644 index 00000000..e6cf56b4 Binary files /dev/null and b/docs/images/screens/bar_vert.png differ diff --git a/docs/images/screens/bubble_horiz.png b/docs/images/screens/bubble_horiz.png new file mode 100644 index 00000000..36e63660 Binary files /dev/null and b/docs/images/screens/bubble_horiz.png differ diff --git a/docs/images/screens/bubble_vert.png b/docs/images/screens/bubble_vert.png new file mode 100644 index 00000000..99dbbb81 Binary files /dev/null and b/docs/images/screens/bubble_vert.png differ diff --git a/docs/images/screens/candlestick_horiz.png b/docs/images/screens/candlestick_horiz.png new file mode 100644 index 00000000..9052353c Binary files /dev/null and b/docs/images/screens/candlestick_horiz.png differ diff --git a/docs/images/screens/candlestick_vert.png b/docs/images/screens/candlestick_vert.png new file mode 100644 index 00000000..eafb0e0b Binary files /dev/null and b/docs/images/screens/candlestick_vert.png differ diff --git a/docs/images/screens/fx_horiz.png b/docs/images/screens/fx_horiz.png new file mode 100644 index 00000000..6b5be442 Binary files /dev/null and b/docs/images/screens/fx_horiz.png differ diff --git a/docs/images/screens/fx_vert.png b/docs/images/screens/fx_vert.png new file mode 100644 index 00000000..47d41d0d Binary files /dev/null and b/docs/images/screens/fx_vert.png differ diff --git a/docs/images/screens/pie_horiz.png b/docs/images/screens/pie_horiz.png new file mode 100644 index 00000000..4c0b21af Binary files /dev/null and b/docs/images/screens/pie_horiz.png differ diff --git a/docs/images/screens/pie_vert.png b/docs/images/screens/pie_vert.png new file mode 100644 index 00000000..5c77ed4a Binary files /dev/null and b/docs/images/screens/pie_vert.png differ diff --git a/docs/images/screens/rounded_bar_renderer.png b/docs/images/screens/rounded_bar_renderer.png new file mode 100644 index 00000000..4afd37d4 Binary files /dev/null and b/docs/images/screens/rounded_bar_renderer.png differ diff --git a/docs/images/screens/scatter_horiz.png b/docs/images/screens/scatter_horiz.png new file mode 100644 index 00000000..b81bf673 Binary files /dev/null and b/docs/images/screens/scatter_horiz.png differ diff --git a/docs/images/screens/scatter_vert.png b/docs/images/screens/scatter_vert.png new file mode 100644 index 00000000..abf31778 Binary files /dev/null and b/docs/images/screens/scatter_vert.png differ diff --git a/docs/images/screens/step_horiz.png b/docs/images/screens/step_horiz.png new file mode 100644 index 00000000..9db59c98 Binary files /dev/null and b/docs/images/screens/step_horiz.png differ diff --git a/docs/images/screens/step_vert.png b/docs/images/screens/step_vert.png new file mode 100644 index 00000000..7b5bde46 Binary files /dev/null and b/docs/images/screens/step_vert.png differ diff --git a/docs/images/simple_xy_plot.png b/docs/images/simple_xy_plot.png new file mode 100644 index 00000000..fde56e19 Binary files /dev/null and b/docs/images/simple_xy_plot.png differ diff --git a/docs/images/sizing/abs100x-abs100y.png b/docs/images/sizing/abs100x-abs100y.png new file mode 100644 index 00000000..03176840 Binary files /dev/null and b/docs/images/sizing/abs100x-abs100y.png differ diff --git a/docs/images/sizing/abs100x-abs150y.png b/docs/images/sizing/abs100x-abs150y.png new file mode 100644 index 00000000..c1c8159d Binary files /dev/null and b/docs/images/sizing/abs100x-abs150y.png differ diff --git a/docs/images/sizing/abs100x-rel1y.png b/docs/images/sizing/abs100x-rel1y.png new file mode 100644 index 00000000..b62cf708 Binary files /dev/null and b/docs/images/sizing/abs100x-rel1y.png differ diff --git a/docs/images/sizing/fil50x-fil50y.png b/docs/images/sizing/fil50x-fil50y.png new file mode 100644 index 00000000..6dd24012 Binary files /dev/null and b/docs/images/sizing/fil50x-fil50y.png differ diff --git a/docs/images/sizing/rel075x-abs100y.png b/docs/images/sizing/rel075x-abs100y.png new file mode 100644 index 00000000..d0328550 Binary files /dev/null and b/docs/images/sizing/rel075x-abs100y.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..90a8a021 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,54 @@ +# Androidplot Documentation + +When working with Androidplot, your best resource +is the [DemoApp's example source code](../demoapp) as it's kept up to date for every release. + +If you can't find an answer feel free to ask a question on Stack Overflow using the +[Androidplot tag](http://stackoverflow.com/questions/tagged/androidplot). + +# Tutorials +These tutorials are roughly in the order that they should be read. Quickstart will get your app +up and running with a basic xy plot. Plot Composition explains the common anatomy of all plots +in the Androidplot library and lays the foundation for future topics. The tutorials discuss +specific plot types explaining styling and other advanced topics. + +* [Quickstart](quickstart.md) :star: +* [Quickstart (YouTube Video)](https://www.youtube.com/watch?v=wEFkzQY_wWI) :movie_camera: +* [Plot Composition](plot_composition.md) +* [The Legend](legend.md) +* [XY Plots](xyplot.md) +* [Bar Charts](barchart.md) +* [Candlestick Charts](candlestick.md) +* [Pie Charts](piechart.md) +* [Bubble Charts](bubblechart.md) +* [Dynamic Plots](dynamicdata.md) +* [Advanced XY Plot Topics](advanced_xy_plot.md) +* [Custom Renderers](custom_renderer.md) + +# Examples +Source code examples of the various plot types. + +* [An XYPlot](../demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java) +* [A dynamic XYPlot](../demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java) +* [Panning & Zooming](../demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java) +* [Step Chart](../demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java) +* [Candlestick Chart](../demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java) +* [Scatter Plot](../demoapp/src/main/java/com/androidplot/demos/ScatterPlotActivity.java) +* [Bar Chart](../demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java) +* [Pie Chart](../demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java) +* [An ECG Example](../demoapp/src/main/java/com/androidplot/demos/ECGExample.java) +* [f(x) Plot](../demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java) + +# XML Attributes +A complete list of XML attributes is [available here](attrs.md). +# Javadoc +Javadocs for versions through 1.5.7 are [available here](https://javadoc.io/doc/com.androidplot/androidplot-core). Due to a build issue +producing Javadocs for the latest releases has been paused. There have been very +few changes to the API since 1.5.7 so the Javadocs should still be useful in the mean time. + +# Release Notes +Full release notes are [available here](release_notes.md) + +# Contributing +_If you see something that isn't right or want to contribute, please [make a pull-request](https://help.github.com/articles/creating-a-pull-request/) - these docs +live the main repo in the top-level `/docs` directory. For more info, see the [Contributing Source Code](docs/contributing.md) doc. \ No newline at end of file diff --git a/docs/legend.md b/docs/legend.md new file mode 100644 index 00000000..c3ea90e4 --- /dev/null +++ b/docs/legend.md @@ -0,0 +1,63 @@ +# The Legend +For `Plot` types that support it, the legend displays a list of elements in the plot along with +a color coded icon. The color coded icon is automatically generated using the colors and line styles +used to render the associated item. In the case of a `Series`, this is the `Formatter` you associated +with the `Series` when you added it to your `Plot`. + +# Showing / Hiding the Legend +Depending on the `Plot` type(s) you are using, the legend may or may not be visible by default. To +can enable / disable the legend: + +```java +plot.getLegend().setVisible(true|false); +``` + +# Hiding Series Items +You can tell Androidplot not to generate a legend item for a `Series` by configuring it's associated +`Formatter`: + +```java +formatter.setLegendIconEnabled(false); +``` + +## The TableModel +The `TableModel` controls how and where each item in the legend is drawn. Androidplot provides two +default implementations; `DynamicTableModel` and `FixedTableModel` (detailed below). All `TableModel` implementations +organize elements into a grid. This grid is populated with items based on the order which it's corresponding +series was added to the plot. This ordering can be further controlled by setting the `TableModel`'s +`TableOrder` param to either [ROW_MAJOR](https://en.wikipedia.org/wiki/Row-major_order) (items are added left-to-right, top-down) +or `COLUMN_MAJOR` (items are added top-down, left-to-right). + +### DynamicTableModel +The `DynamicTableModel` takes a desired of numbered rows and columns and evenly subdivides the `LegendWidget`'s +visible space into cells. For example, A 2x2 legend using `ROW_MAJOR` ordering: + +```java +plot.getLegend().setTableModel(new DynamicTableModel(2, 2, TableOrder.ROW_MAJOR)); +``` + +### FixedTableModel +The `FixedTableModel` takes a desired size of each cell in pixels and adds cells using the specified `TableOrder`. +It automatically wraps to the next row or column (based on `TableOrder`) when the cell being added +exceeds the legend's available space on a given axis. For example, A `FixedTableModel` using 300w*100h cells and +a TableOrder of `COLUMN_MAJOR`: + +```java +plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300), + PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR)); +``` + +# Sorting Legend Entries +You can control the order of Legend entries by setting a custom `Comparator` on the legend: + +```java +Comparator<...> myComparator = ... +plot.getLegend().setLegendItemComparator(myComparator); +``` + +Using a custom `Comparator` in conjunction with `ROW_MAJOR` and `COLUMN_MAJOR` properties on the `TableModel` +(show above) gives you full control over the display ordering of your legend entries. + +# Positioning and Resizing +The legend is just an implementation of a Widget and is positioned and resized in the same ways +that all Widget instances are positioned. See the [Plot Composition](plot_composition.md) doc for details. \ No newline at end of file diff --git a/docs/piechart.md b/docs/piechart.md new file mode 100644 index 00000000..8bbfbfbb --- /dev/null +++ b/docs/piechart.md @@ -0,0 +1,104 @@ +# Pie Charts +Like all other primary chart and graph classes in Androidplot, `PieChart` is simply a composition +of widgets along with convenience methods. The real work is actually done by `PieWidget`. + +![image](images/screens/pie_horiz.png) + +# Basic Usage +Pie charts are composed of one or more values called Segments which combines a vaue and a label: + +```java +Segment segment = new Segment("my segment", 10); +``` + +Displaying a `Segment` to a pie chart also requires a `SegmentFormatter`. `SegmentFormatter` is what defines +the visual characteristics of the associated `Segment`; color, border thickness, +segment offset, label text style (if any) etc. The below code instantiates a new `SegmentFormatter` +with a segment color of red: + +```java +SegmentFormatter formatter = new SegmentFormatter(Color.RED); +``` + +Finally, the `Segment` must be added to the pie chart along with it's `SegmentFormatter`: + +```java +pie.addSegment(segment, formatter); +``` + +# Donut Size +By default, `PieChart` draws it's self as a ring. The amount of empty space in the middle of +the pie is called the `donutSize`. Setting a `donutSize` of 0 causes the `PieChart` to draw it's +self as a full circle. + +![image](images/pie_donut_size.png) + +Setting `donutSize` programmatically: + +```java +// draw pie with an empty inner space radius equal to 50% of the pie's radius. +pieRenderer.setDonutSize(0.5, PieRenderer.DonutMode.PERCENT); + +// donut size may also be expressed in pixels: +pieRenderer.setDonutSize(PixelUtils.dpToPix(30), PieRenderer.DonutMode.PIXELS); +``` + +# Segment Orientation +Each `Segment` is drawn in the order it was added to the `PieChart` clockwise from `startDegs` +which by default is 0. + +![image](images/pie_orientation.png) + +Using the image above, to move Segment0 to the Northeast quadrant +of the pie (currently occupied by Segment3) `startDeg` should be set to 90: + +```java +pie.getRenderer(PieRenderer.class).setStartDegs(90); +``` +# Extent +The `extentDegs` value determines the number of degrees running clockwise from `startDegs` that +represent 100% of the pie chart. The formal description is a little obtuse but is easy to understand +through example. You could for instance use `extentDegs` in conjunction with `startDegs` to represent +the full pie as a gauge: + +```java +// start the first segment at 5 degrees; +pie.getRenderer(PieRenderer.class).setStartDegs(165); +pie.getRenderer(PieRenderer.class).setExtentDegs(150); +``` +Which produces a pie chart profile like this: + +![image](images/pie_extent.png) + +# SegmentFormatter +As mentioned above, `SegmentFormatter` defines how a `Segment` is visually represented in a `PieChart`. +In addition to basic color / border line styling params, there are several parameters available +to visually distinguish a segment: + +## Offset +Value in pixels used to "shift" the position of the segment outward relative to the center of the `PieChart`. +The visual effect is equivalent to cutting up a pie and dragging one piece out and away from the rest of the pie: + +![image](images/pie_offset.png) + +## RadialInset +Value in degrees used to shrink the radial edges of the segment inward. The visual effect is equivalent +to shaving the edges off of a slice of pie, resulting in a narrower slice of pie: + +![image](images/pie_radial_inset.png) + +## InnerInset +Value in pixels used to shrink the inner section of the pie. The visual effect is equivalent to +cutting the tip off of a slice of pie: + +![image](images/pie_inner_inset.png) + +## OuterInset +Value in pixels used to shrink the outer edge of the pie. The visual effect is equivalent to evenly +shaving the crust off of a slice of pie: + +![image](images/pie_outer_inset.png) + +See the DemoApp's +[pie chart example source](../demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java) for a comprehensive usage example. + diff --git a/docs/plot_composition.md b/docs/plot_composition.md new file mode 100644 index 00000000..2c51e349 --- /dev/null +++ b/docs/plot_composition.md @@ -0,0 +1,289 @@ +# Plot Composition +All plots in Androidplot inherit from the abstract base class `Plot` which provides common behaviors +for all `Plot` implementations. + +# Widgets +Plots are composed of one or more Widgets. A `Widget` is an abstraction of a visual +component that may be positioned and scaled within the visible area of a Plot. For example, +an `XYPlot` is typically composed of these 5 `Widgets`: + +* Title +* Graph +* Domain Label +* Range Label +* [Legend](legend.md) + +All implementations of `Plot` will contain at least one default `Widget` providing the core +behavior encapsulated by that `Plot`. In addition to moving and scaling a `Widget`, developers may +also extend them and replace the `Plot` instance's default instance with the derived implementation in order to +get custom behavior. + +# The LayoutManager +The `LayoutManager` provides the logic for visually positioning and scaling Widgets; all `Plot` implementations +contain an instance of `LayoutManager` that can be retrieved via `Plot.getLayoutManager()`. + +## Z-Indexing +Z-indexing is a 2D drawing concept which associates each drawable entity with a value that determines +which elements get drawn onto the screen first, producing the visual effect that certain elements appear +on top of others. + +While Androidplot uses the term "z-index" it's implemented internally as a linked list to prevent the possibility +of duplicate index values and therefore ensuring that `Widget` drawing order is always explicit. +The [Layerable](../androidplot-core/src/main/java/com/androidplot/util/Layerable.java) interface +defines methods used for manipulating the z-index of a `Widget`. + +## Adding & Removing Widgets +New `Widget` instances can be added either to the front or back of the z-index using these methods: + +* `LayoutManager.addToTop(Widget)` +* `LayoutManager.addToBottom(Widget)` + +## Positioning Widgets +Once a Widget has been added to the LayoutManager, it's position within the Plot can be adjusted via +`Widget.position(...)`. This method takes a layout style and value for the x and y dimension and an +optional anchor position from which the layout modes will be applied. + +### HorizontalPosition & VerticalPosition +These define the part of the Plot from which Widget will be positioned using relative or absolute units. + +* XPosition - Supports positioning in relation to the Plot's left edge, right edge or horizontal center. +* YPosition - Supports positioning in relation to the Plot's top edge, bottom edge or vertical center. + + +#### Absolute Positioning +Absolute positioning means that positions are expressed as an absolute pixel offset from the specified +edge or center point. + +#### Relative Positioning +Relative positions are expressed as a ratio of the total size of the Plot along the given axis. This +ration must fall within the range of -1 to 1. + +**Example #1: A relative XPosition of 1 and YPosition of 0.5 in a 100Hx200W pixel Plot** + +x = 1 * 200 = 200 + +y = 0.5 * 100 = 50 + +**Example #2: An absolute XPosition of 50 and an absolute YPosition of 25 in a 100Hx200W pixel Plot** + +x = 50 + +y = 25 + +#### Anchors +The Anchor param specifies the point on the Widget from which the XPosition and YPosition calculations will be applied. +Using example #1 above, an Anchor value of `Anchor.LEFT_TOP` means that the top left corner of the Widget +would be positioned at the screen coordinate [200, 50]. + +#### Examples +The examples below illustrate positioning an `XYGraphWidget` of an `XYPlot`. + +xml: +```xml +ap:graphAnchor="right_bottom" +ap:graphHorizontalPositioning="absolute_from_right" +ap:graphHorizontalPosition="0dp" +ap:graphVerticalPositioning="relative_from_bottom" +ap:graphVerticalPosition="0dp" +``` +java: +```java +plot.getGraph().position( + 0, HorizontalPositioning.ABSOLUTE_FROM_RIGHT, + 0, VerticalPositioning.RELATIVE_TO_BOTTOM); +``` +![alt text](images/positioning/bottom-right-absolute.png "bottom-right-absolute") +*** + +xml: +```xml +ap:graphAnchor="right_bottom" +ap:graphHorizontalPositioning="absolute_from_right" +ap:graphHorizontalPosition="10dp" +ap:graphVerticalPositioning="relative_from_bottom" +ap:graphVerticalPosition="10dp" +``` +java: +```java +plot.getGraph().position( + PixelUtils.dpToPix(10), HorizontalPositioning.ABSOLUTE_FROM_RIGHT, + PixelUtils.dpToPix(10), VerticalPositioning.RELATIVE_TO_BOTTOM); +``` +![alt text](images/positioning/bottom-right-absolute-with-offset.png "bottom-right-absolute-with-offset") +*** + +xml: +```xml +ap:graphAnchor="right_bottom" +ap:graphHorizontalPositioning="absolute_from_left" +ap:graphHorizontalPosition="0dp" +ap:graphVerticalPositioning="relative_from_top" +ap:graphVerticalPosition="0dp" +``` +java: +```java +plot.getGraph().position( + 0, HorizontalPositioning.ABSOLUTE_FROM_LEFT, + 0, VerticalPositioning.RELATIVE_TO_TOP); +``` +![alt text](image``s/positioning/top-left-absolute.png "top-left-absolute") +*** + +xml: +```xml +ap:graphAnchor="center" +ap:graphHorizontalPositioning="absolute_from_center" +ap:graphHorizontalPosition="0dp" +ap:graphVerticalPositioning="absolute_from_center" +ap:graphVerticalPosition="0dp" +``` +java: +```java +plot.getGraph().position( + 0, HorizontalPositioning.ABSOLUTE_FROM_CENTER, + 0, VerticalPositioning.ABSOLUTE_FROM_CENTER); +``` +![alt text](images/positioning/center-absolute.png "center-absolute") + + +## Sizing Widgets +The size and shape of a `Widget` is controlled by it's `setSize(Size)` method. + +### Size +The `Size` parameter of `Widget.setSize(Size)` defines the height and width of the associated Widget. +It is composed of two `SizeMetric` instances; one for height and one for width. +`Size` provides two constructors: `Size(SizeMetric, SizeMetric)` or +`Size(int, SizeMode, int, SizeMode)`. Constructor params represent height and width respectively. + +### SizeMetric +A `SizeMetric` is composed of a `SizeMode` and a float value. There are three kinds of `SizeMode`: + +* ABSOLUTE - float value defines the size metric as an absolute value in pixels. +* RELATIVE - float value defines the size of the metric relative to the size of the containing Plot along +the associated axis, in the range of 0.0 to 1.0. +* FILL - float value defines an absolute value in pixels to subtract from the size of the containing +Plot along the associated axis and the SizeMetric "fills" the difference. + +#### Examples +The examples below illustrate positioning an `XYGraphWidget` of an `XYPlot` and +assume centered positioning is applied as described in the [Positioning Widgets](#positioning-widgets) section above. + +xml: +```xml +ap:graphHeightMode="absolute" +ap:graphHeight="100dp" +ap:graphWidthMode="absolute" +ap:graphWidth="100dp" +``` +java: +```java +plot.getGraph().setSize(new Size( + PixelUtils.dpToPix(100), SizeMode.ABSOLUTE, + PixelUtils.dpToPix(100), SizeMode.ABSOLUTE)); +``` +![alt text](images/sizing/abs100x-abs100y.png "x=100|ABSOLUTE, y=100|ABSOLUTE") +*** +xml: +```xml +ap:graphHeightMode="absolute" +ap:graphHeight="150dp" +ap:graphWidthMode="absolute" +ap:graphWidth="100dp" +``` +java: +```java +plot.getGraph().setSize(new Size( + PixelUtils.dpToPix(150), SizeMode.ABSOLUTE, + PixelUtils.dpToPix(100), SizeMode.ABSOLUTE)); +``` +![alt text](images/sizing/abs100x-abs150y.png "x=100|ABSOLUTE, y=150|ABSOLUTE") +*** +xml: +```xml +ap:graphHeightMode="relative" +ap:graphHeight="1.0" +ap:graphWidthMode="absolute" +ap:graphWidth="100dp" +``` +java: +```java +plot.getGraph().setSize(new Size( + 1.0f, SizeMode.RELATIVE, + PixelUtils.dpToPix(100), SizeMode.ABSOLUTE)); +``` +![alt text](images/sizing/abs100x-rel1y.png "x=100|ABSOLUTE, y=1.0|RELATIVE") +*** +xml: +```xml +ap:graphHeightMode="absolute" +ap:graphHeight="100dp" +ap:graphWidthMode="relative" +ap:graphWidth="0.75" +``` +java: +```java +plot.getGraph().setSize(new Size( + PixelUtils.dpToPix(100), SizeMode.ABSOLUTE, + 0.75f, SizeMode.RELATIVE)); +``` +![alt text](images/sizing/rel075x-abs100y.png "x=0.75|RELATIVE, y=100|ABSOLUTE") +*** +xml: +```xml +ap:graphHeightMode="fill" +ap:graphHeight="50dp" +ap:graphWidthMode="fill" +ap:graphWidth="50dp" +``` +java: +```java +plot.getGraph().setSize(new Size( + PixelUtils.dpToPix(50), SizeMode.FILL, + PixelUtils.dpToPix(50), SizeMode.FILL)); +``` +![alt text](images/sizing/fil50x-fil50y.png "x=50|FILL, y=50|FILL") + +## Margins and Padding +Every widget has a margin, padding and an optional border that can be drawn around it. These params behave +very similarly to those defined in the [CSS Box Model](http://www.w3schools.com/css/css_boxmodel.asp). + +## Markup Mode +If you're having trouble visualizing the effects of tweaking margins and padding, you can enable +markup mode which will highlight these spaces on each widget, as well as draw a green line around it's +absolute border. + +To turn markup mode on for a plot programmatically: + +```java +plot.setMarkupEnabled(true); +``` + +Or via XML: + +```xml +ap:markupEnabled="true" +``` + +This is what it looks like: + +![alt text](images/markup_mode.jpg "markup enabled") + +# Formatters, Renderers and Series Data +Each Plot specifies the type of Series it supports; XYPlots support XYSeries, PieCharts support Segment, etc. +In all cases, the Series encapsulates the numeric model of the data being represented by the Plot. + +## Formatters +The Plot keeps a mapping between Series +data and the Formatter instance provided by the user that is to be used to render that data. It is the +Formatter that tells Androiplot which Renderer to use to draw the Series along with which colors, line thicknesses, +text style, etc. to apply while drawing. + +## Renderers +The Renderer is what renders Series data onto a Plot. Users can provide their own custom rendering behavior +by writing their own Renderer implementation along with a custom Formatter telling Androidplot about the +Renderer via the `Formatter.getRendererClass()` method. See [Custom Renderer](custom_rnderer.md) documentation. + +# XML Styling +Androidplot supports an increasing number of XML attributes. The two best resources for learning about +these attributes is the [demo app source code](../demoapp) and [attrs.xml](../androidplot-core/src/main/res/values/attrs.xml) file which +contains the exhaustive list of available attributes. \ No newline at end of file diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..3bd87c3d --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,205 @@ +# Quickstart + +![image](images/simple_xy_plot.png) + +This tutorial will walk you through Adding Androidplot as a dependency and displaying an XYPlot with +two data series. You can view this sample on your own device or emulator by installing the +[Demo App](https://play.google.com/store/apps/details?id=com.androidplot.demos) on your device and +clicking on the Simple XY Plot Example. Full source [available here](../demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java). + +You can also watch this tutorial in video form on [Youtube](https://youtu.be/wEFkzQY_wWI). + +# Add the Dependency +To use the library in your gradle project add the following to your build.gradle: + +```groovy +dependencies { + implementation "com.androidplot:androidplot-core:1.5.11" +} +``` + +*NOTE: As of version 1.5.8 Androidplot has migrated over from the Android Support Libraries to androidx. +If you have a very old project and experience issues, may need to stay on version 1.5.7* + +If you’re using Proguard obfuscation (Projects created by Android Studio do by default) you’ll also +want add this to your proguard-rules.pro file: + +`-keep class com.androidplot.** { *; }` + +# Create your Activity XML Layout +Once you’ve got an Android project skeleton created, create **res/layout/simple_xy_plot_example.xml** +and add an XYPlot view: +```xml + + + + +``` +This example uses a default style to decorate the plot. The full list of XML styleable attributes is +[available here](attrs.md). While new attributes are added regularly, +not all configurable properties are yet available. + +If something you need is missing, use [Fig Syntax](https://github.com/halfhp/fig) +directly within your Plot's XML, prefixing each property with "androidPlot". Example: + +```xml +androidPlot.title="My Plot" +``` + +Add these files to your **/res/xml** directory: + +#### /res/xml/line_point_formatter_with_labels.xml +```xml + + +``` + +#### /res/xml/line_point_formatter_with_labels_2.xml +```xml + + +``` + +# Create an Activity +Now let's create an Activity to display the XYPlot we just defined in `simple_xy_plot_example.xml`. +The basic steps are: + +1. Create an instance of Series and populate it with data to be displayed. +2. Register one or more series with the plot instance along with a Formatter to describing how the series should look when drawn. +3. Draw the Plot + +Since we're working with XY data, we’ll use XYPlot, SimpleXYSeries (which is an +implementation of the XYSeries interface) and LineAndPointFormatter: + +```java +import android.app.Activity; +import android.graphics.*; +import android.os.Bundle; + +import com.androidplot.util.PixelUtils; +import com.androidplot.xy.SimpleXYSeries; +import com.androidplot.xy.XYSeries; +import com.androidplot.xy.*; + +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.util.*; + +/** + * A simple XYPlot + */ +public class SimpleXYPlotActivity 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: + LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter_with_labels); + + 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[] { + + // 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: + // 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)); + + // add a new series' to the xyplot: + plot.addSeries(series1, series1Format); + plot.addSeries(series2, series2Format); + + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() { + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + int i = Math.round(((Number) obj).floatValue()); + return toAppendTo.append(domainLabels[i]); + } + @Override + public Object parseObject(String source, ParsePosition pos) { + return null; + } + }); + } +} +``` + +One potentially confusing section of the code above are the initializations of LineAndPointFormatter +You probably noticed that they take a mysterious reference to an xml resource file. This is actually +using [Fig](https://github.com/halfhp/fig) to configure the instance properties from XML. + +If you'd prefer to avoid the XML and keep everything in Java, just replace the code: + +```java +LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter_with_labels); +``` + +with: + +```java +LineAndPointFormatter series1Format = new LineAndPointFormatter(Color.RED, Color.GREEN, Color.BLUE, null); +``` + +In general XML configuration should be used over programmatic configuration when possible as it produces +more flexibility in terms of defining properties by screen density etc.. For more details on how to +programmatically configure Formatters etc. consult the latest Javadocs. + +# Whats Next? +Learn about [Plot Composition](plot_composition.md) or continue with [XYPlots](xyplot.md). \ No newline at end of file diff --git a/docs/release_notes.md b/docs/release_notes.md new file mode 100644 index 00000000..50d52c11 --- /dev/null +++ b/docs/release_notes.md @@ -0,0 +1,177 @@ +# Androidplot Versioning +For details on what to expect in general when updating to a new version of Androiplot, check out the +[versioning doc](versioning.md). + +# 1.5.11 +* Update project to latest gradle / build tools +* Fix issue with jetifier flagging an outdated dependency + +# 1.5.10 +* Update project to use latest gradle / build tools +* (#114) fix `setLinesPerRangeLabel` & `setLinesPerDomainLabel` + +# 1.5.9 +* (#107) Fix ambiguous ordinal for render mode attributes +* (#104) Fix issue with background rendering in RecyclerView. +* Adds a RecyclerView example to demo app. + +# 1.5.8 +* Maintenance release - update dependences, get off jcenter, etc. + +# 1.5.7 +* (#94) Potential fix / better error logging for a crash caused by a buffered canvas resize with illegal arguments. +* (#93) Fix Android 9 compiler warnings. +* (#83) Fix NPE when attempting to recycle an already null buffered canvas instance. +* Remove unused `PlotRenderException`. +* Added `IN_ORDER` BarRenderer mode. + +# 1.5.6 +* Adds convenience methods for saving / restoring `PanZoom` state. +* (#80) Targets SDK 28, fixing compatibility issues. + +# 1.5.5 + +* (#76) Fixed a bug that could cause a deadlock when grid steps are much larger than actual plot range. +* (#78) Fixed a bug where setting insets on XYGraphWidget would have no effect after the plot was drawn. +* XYGraphWidget.drawMarkerText is now marked `protected`. + +# 1.5.4 + +* (#69) Fixed a bug in `SimpleXYPlot` preventing the resizing of `Y_VALS_ONLY` formatted series. +* (#73) Fixed a bug where dynamically resizing a Plot (by marking a sibling view as `GONE`, etc.) would not resize the graph widget. + +# 1.5.3 + +* Minor cleanup of Widget example source. +* (#67) Fixed Javadoc link + +# 1.5.2 + +_This version is pickier than it's predecessors about proper XML configuration. Where +previous versions would silently ignore illegal XML attrs, a `RuntimeException` will be thrown._ +* Added [sizing documentation](plot_composition.md#sizing-widgets) +* Added [custom renderer documentation](custom_renderer.md) +* Fixed (#61) Bug in `XYGraphWidget.screenToSeriesY(...)`. +* Fixed (#63) Fixed compatibility issue with Gradle 3.x.x that caused issues with XML parsing. + +# 1.5.1 + +* (#52) Fixed minor NPE issue +* (#55) Fixed bug with `PieRenderer.getContainingSegment` not working for very large segments. + +# 1.5.0 + +_Updates to legend functionality in this version may result in changes to the display order +of legend items in some cases. A custom `Comparator` can be used to resolve this if necessary; +see the [legend doc](legend.md) for implementation details._ + +* Added [legend doc](legend.md) +* Added legend support to `PieChart` +* Added configurable legend item sorting +* (#45) Auto range boundaries calculation fix for when using a fixed domain range and a `FastXYSeries` +* Minor Performance Optimizations + +# 1.4.3 + +* (#39) `FastLineAndPointRenderer` now renders vertices for legend items. +* Added [XML Attrs reference doc](attrs.md). + +# 1.4.2 + +* (#32) New step mode: `INCREMENT_BY_FIT`. +* (#33) `PanZoom` support for `INCREMENT_BY_FIT`. +* (#34) Removed examples and documentation for serializing `SeriesRegistry` to preserve state. + +# 1.4.1 + +* (#26) Fixed an NPE issue when drawing null values with a `PointLabeler`. +* Fixed a broken link in Quickstart doc. + +# 1.4.0 + +* Moderate refactor of `PieRenderer`. [Documentation](piechart.md) has been updated to reflect these changes. +* Major refactor of `BarRenderer`. [Documentation](barchart.md) has been updated to reflect these changes. +* Added `ScalingXYSeries` which wraps other instances of `XYSeries` to be dynamically scaled. This is +particularly useful for creating animated intros using `XYSeries` data. +* Added [AnimatedXYPlotActivity](../demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java) +demonstrating the use of `ScalingXYSeries` to create an animated intro. +* `XYPlot.getXVal(..)` and `XYPlot.getYVal(...)` methods have been deprecated and will be removed in 1.5.0. +`XYPlot.screenToSeries(...)` and `XYPlot.seriesToScreen(...)` should be used instead. +* Domain and range cursors are now disabled by default. To enable, set a valid cursor position using +`XYGraphWidget.setCursorPosition(float, float)`. Cursor position values are expressed in screen coordinates; +you can convert between screen and series values using `XYPlot.screenToSeries(...)` and `XYPlot.seriesToScreen(...)`. + +# 1.3.1 + +* Added [NormedXYSeries](advanced_xy_plot.md#normedxyseries) wrapper to simplify the process of normalizing xy series data. +* Added [DualScaleActivity](../demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java) +demonstrating `NormedXYSeries` usage to present dual range scales. +* LineAndPointRenderer options for cases where two or mode series' of different size have been added. +* Fixed a bug causing points scrolled off-screen to occasionally accumulate and render along the left edge of the graph. +* Fixed a bug that could cause render jitter when extreme zoom levels were applied. +* Fixed a bug that prevented `PanZoom` from working properly on plots with an undefined outer limit. + +# 1.3.0 + +* Added sampling support. See the [Advanced XY Plot](advanced_xy_plot.md) doc for details. +* PanZoom performance enhancements & bug fixes. If you're currently using PanZoom you'll likely need to +update your code as the interface has slightly changed. +* Added leakcanary to DemoApp for debug builds. +* More unit test coverage +* Fixed a bug that prevented an instance of a given series from being added more than once, even +when a unique formatter is supplied. +* Added `Formatter.getLegendIconEnabled()` and `Formatter.setLegendIconEnabled(boolean)`, used to enable / disable drawing legend items for individual +series / formatter pairs. +* Added `XYGraphWidget.Edge.NONE` to be used with `XYGraphWidget.setLineLabelEdges(Edge...)` to disable all edges. + +# 1.2.2 + +* BarRenderer / BarFormatter cleanup +* More documentation! +* Bounds and XYBounds have been merged into Region and RectRegion respectively. +* ValPixConverter has been removed and it's functionality migrated to Region and RectRegion. +* Added Region.transform(...) and RectRegion.transform(...) +* Added Region.ratio(...) and RectRegion.ratio(...) +* XYPlot.getCalculatedMinX(), XYPlot.getCalculatedMaxX(), XYPlot.getCalculatedMinY() and XYPlot.getCalculatedMinY() +have been replaced with XYPlot.getBounds(). +* Configurator has become it's own library - [Fig!](https://github.com/halfhp/fig) +* New constructors have been added to Formatters to simplify XML configuration via Fig. +* Added Jacoco code coverage reporting + +# 1.2.1 + +### Pie Chart Enhancements +Pie chart has been updated with new methods and format attributes to improve segment +selection and highlighting functionality: + +* Added `offset`, `radialInset`, `innerInset` and `outerInset` properties to SegmentFormatter. +See the [pie chart documentation](piechart.md) for usage details. +* Updated PieRenderer to support the new SegmentFormatter properties. +* SimplePieChartActivity has been updated to provide an interactive demo of some of these new features. + +### Misc + +* Added `Plot.getListeners()` method. +* Added FastLineAndPointRenderer, updated OrientationSensorExampleActivity to use it. +* Updated to target SDK 24, removed sdkmanager dependency, and other misc. updates to project deps etc. +* Lots of additions and updates to documentation +* Added CircleCI support + +# 1.1.0 + +* Added drawGridOnTop param to XYGraphWidget; when set to true, grid lines will be drawn on top of rendered series data. (default is false) +* Added PanZoom class providing one-line configuration of pan/zoom behavior for instances of XYPlot. See TouchZoomExampleActivity for a usage example. +* Removed InteractiveXYPlot as PanZoom makes it obsolete. + +# 1.0.0 + +This is a factor of several core elements of the Androidplot lib. The general theme was to +make class and method names more intuitive and to make xml styling more powerful. + +* Major refactor of XYGraphWidget +* Tick renamed to Line +* Extensible Label Formatters +* getXXXWidget methods renamed to simply getXXX +* Per-edge tick extensions +* Moved documentation into vcs. Docs from 1.0 forward will be maintained here. (TODO) +* plot label xml param renamed to title \ No newline at end of file diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 00000000..b65b0c6f --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,15 @@ +# Versioning +Androidplot's versioning scheme is Major.Minor.Rev. For example, in the version 1.3.2 the Major is 1 +the Minor is 3 and the Rev is 2. + +Revisional releases (releases where only the Rev value has increased from the previous release, ie 1.3.1 -> 1.3.2) +may include new features and bug fixes but will be fully backwards compatible with releases of the same minor version. +**Updating to the latest revisional release should require no code changes in your app.** + +Minor Releases (releases where the Minor value has increased but the Major value has not) may contain +new features, bug fixes, removed methods deprecated in the previous minor release +and other moderate refactoring. **Updating to a new minor release may require code changes in your app.** + +Major releases (releases where the Major value has increased) are typically complete rewrites of core +elements of the library. Unless otherwise specified in the release notes, **updating to a new major release +will require significant code changes to your app.** \ No newline at end of file diff --git a/docs/xyplot.md b/docs/xyplot.md new file mode 100644 index 00000000..ce7e1526 --- /dev/null +++ b/docs/xyplot.md @@ -0,0 +1,391 @@ +# XYPlot +An `XYPlot` renders one or more XYSeries onto an instance of XYGraphWidget. It also includes two instances +of `TextLabelWidget` used to display an optional title for the domain and range axis, and an instance +of [XYLegendWidget](#legend) which by default will automatically display legend items for each `XYSeries` added to the plot. + +![image](images/plot_anatomy.png) + +# XYSeries +`XYSeries` is the interface Androidplot uses to retrieve your numeric data. You may either create your own +implementation of `XYSeries` or use `SimpleXYSeries` if you don't have tight performance requirements or +if your numeric data is easily accessed as an array or list of values. + +## SimpleXYSeries +As a convenience, Androidplot provides an all-purpose implementation of the `XYSeries` interface called +`SimpleXYSeries`. `SimpleXYSeries` is used to wrap your raw data with an implementation of the `XYSeries` interface. + +You can supply your data in several ways: + +As a list of y-vals (x = i for each supplied y-val): +```java +Number[] yVals = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64}; +XYSeries series1 = new SimpleXYSeries( + Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "my series"); +``` + +An interleaved list of x/y value pairs (x[0] = 1, y[0] = 4, x[1] = 2, y[1] = 8, ...): +```java +Number[] yVals = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64}; +XYSeries series1 = new SimpleXYSeries( + Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "my series"); +``` + +Separate lists of x-vals and y-vals: +```java +Number[] xVals = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64}; +Number[] yVals = {5, 2, 10, 5, 20, 10, 40, 20, 80, 40}; +XYSeries series = new SimpleXYSeries(xVals, yVals, "my series"); +``` + +Keep in mind that `SimpleXYSeries` is designed to be easy to use for a broad number of applications; it's not +optimized for any specific scenario; if you are dynamically displaying data that needs to be refreshed more than several +times a second, consider building your own implementation of `XYSeries` designed for your app's +specific needs. + +# The Graph +`XYGraphWidget` encapsulates XYPlot's graphing functionality. Given an instance of `XYPlot`, a reference +to `XYGraphWidget` can be retrieve via `XYPlot.getGraph()`. + +## Domain & Range Boundaries +By default, Androidplot will analyze all `XYSeries` instances registered with the `XYPlot`, determine the +min/max values for domain and range and adjust the `XYPlot` boundaries to match those values. If your +plot contains dynamic data, especially if your plot can periodically contain either no series data +or data with no resolution on one or both axis (all identical values for either x or y) then you may +want to manually set your `XYPlot`'s domain and range boundaries. + +To set your plot's boundaries use: + +* `XYPlot.setDomainBoundaries(Number value, BoundaryMode mode)` +* `XYPlot.setRangeBoundaries(Number value, BoundaryMode mode)` + +Note that the value argument is only used when setting `BoundaryMode.FIXED`. For all other +modes, pass in null. + +### BoundaryMode +Androidplot provides four boundary modes: + +#### FIXED +The plot's boundaries on the specified axis are fixed to user defined values. + +#### AUTO (default) +The plot's boundaries auto adjust to the min/max values for the defined axis. + +#### GROW +The plot's boundaries automatically increase to the max value encountered by the plot. The initial +determines the starting boundaries from which the `BoundaryMode.GROW` behavior will be based. + +#### SHRINK +The plot's boundaries automatically shrink to the min value encountered by the plot. The initial +determines the starting boundaries from which the shrink behavior will be based. + +## Domain & Range Lines +These are the horizontal lines drawn on a graph. These lines are configured via: + +* `XYPlot.setDomainStep(StepMode mode, Number value)` +* `XYPlot.setRangeStep(StepMode, Number value)` + +Androidplot provides these step modes: + +### Subdivide +When using `StepMode.SUBDIVIDE`, the graph is subdivided into the specified number of sections. + +### IncrementByValue +`StepMode.INCREMENT_BY_VALUE` instructs Androidplot draw grid lines at the specified interval. This +is the most commonly used modes as is produces an easy to read result. + +### IncrementByPixels +`StepMode.INCREMENT_BY_PIXELS` behaves identically to `StepMode.INCREMENT_BY_VALUE` except that +the increment quantity is expressed in pixels. + +## Domain & Range Labels +Androidplot supports labeling domain values on either or both the top and bottom graph edges +and range values on either or both the left and right graph edges. Most default styles show labels +only on the left and bottom edges. + +### Line Label Insets +Insets are used to control where line labels are drawn in relation to the graph space. The Insets instance +can be obtained via `XYPlot.getGraph().getLineLabelInsets()`. For example, to +move the range labels on the left of the graph further to the left by 5dp: + +xml +```xml +ap:lineLabelInsetLeft="-5dp" +``` + +java +```java +plot.getGraph().getLineLabelInsets().setLeft(PixelUtils.dpToPix(-5)); +``` + +### Dual Axis Labels +Androidplot provides methods for enabling / disabling axis labels along all edges of the graph. By +default, only the left and bottom edge labels are enabled. To enable labels on the right edge: + +xml +```xml +ap:lineLabels="left|bottom|right" +``` + +java +```java +plot.getGraph().setLineLabelEdges( + XYGraphWidget.Edge.BOTTOM, + XYGraphWidget.Edge.LEFT, + XYGraphWidget.Edge.RIGHT); +``` + +Once the edge has been enabled, text formatting can be controlled by enabling a custom formatter +for the desired edge: + +```java +plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.RIGHT).setFormat(new Format() { + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + // obj contains the raw Number value representing the position of the label being drawn. + // customize the labeling however you want here: + int i = Math.round(((Number) obj).floatValue()); + return toAppendTo.append(i + " thingies"); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + // unused + return null; + } +}); +``` + +You're likely also going to need to normalize your series data in order to get it to display properly. To help simplify +this step, Androidplot provides [NormedXYSeries](advanced_xy_plot.md#normedxyseries) +which can be used to wrap any `XYSeries` to provide the normalized representation of it's data. + +There's a full [reference implementation](../demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java) +of a dual scale plot using a custom `Formatter` and `NormedXYSeries` in the DemoApp. + + +### Line Label Interval +Androidplot allows you to configure the interval at which labels are rendered for domain and range lines: + +* `XYPlot.getGraph().setLinesPerDomainLabel(int interval)` +* `XYPlot.getGraph().setLinesPerRangeLabel(int interval)` + +### LineLabelStyle +The styling of the line labels drawn on each edge of the graph is controlled by it's associated style. +this style contains params that control: + +* Paint used to draw labels (determines, color, font size, etc.) +* Format used to draw text (can be NumberFormat, etc.) +* Rotation of the label text + +To get the style used to draw the left edge (range) labels: + +```java +plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT); +``` + +### LineLabelRenderer +If you need to implement special rendering behavior for your line labels, such as drawing graphics, symbols, etc. +you can create a custom renderer by extending LineLabelRenderer and injecting it into the graph: + +```java +plot.getGraph().setLineLabelRenderer(XYGraphWidget.Edge.LEFT, customLineLabelRenderer); +``` + +[f(x) plot example source](../demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java) +provides an example of this kind of customization. + +# Pan & Zoom +You can enable pan/zoom behavior on any instance of XYPlot using the PanZoom class like this: + +```java +PanZoom.attach(plot); +``` + +The default behavior is to enable horizontal and vertical panning an to zoom using uniform scaling. +If you want to override this behavior use the three argument form of `PanZoom.attach(Plot)`. For example, +to enable pan and zoom (stretch) on the horizontal axis only: + +```java +PanZoom.attach(plot, PanZoom.Pan.HORIZONTAL, PanZoom.Zoom.STRETCH_HORIZONTAL); +``` + +Pan and zoom operations abide by your plot's defined outer limits limits. If no such limits have been set +then the plot will pan and zoom on both axes infinitely. To set the plot's outer limits: + +```java +// cap pan/zoom limits for panning and zooming to a 100x100 space: +plot.getOuterLimits().set(0, 100, 0, 100); +``` + +# Saving & Restoring PanZoom State +The `PanZoom` class provides convenience methods for saving and restoring state from your `Activity`: + +```java +// save the current pan/zoom state +@Override +public void onSaveInstanceState(Bundle bundle) { + bundle.putSerializable("pan-zoom-state", panZoom.getState()); +} + +// restore the previously saved pan/zoom state +@Override +public void onRestoreInstanceState(Bundle bundle) { + PanZoom.State state = (PanZoom.State) bundle.getSerializable("pan-zoom-state"); + panZoom.setState(state); + plot.redraw(); +} +``` + +For a more detailed look at pan & zoom behavior, check out the [Touch Zoom Example source code](../demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java). + +# Series Renderers +There are several renderers available for XYPlots: + +* LineAndPointRenderer +* BarRenderer +* CandlestickRenderer + +When you add a new series to your plot, you tell Androidplot how to render it by passing +in a subclass of `XYSeriesFormatter` that corresponds to the desired renderer. For example, to use +`LineAndPointRenderer`, you'd register your series with an instance of `LineAndPointFormatter`: + +```java +LineAndPointFormatter format = new LineAndPointFormatter(); +plot.addSeries(series, format); +``` + +If you need special behavior not provided by an existing renderer you can create +your own by either extending `XYSeriesRenderer` or one of the above implementations. You'll also need +to create a matching implementation of `XYSeriesFormatter` that returns your renderer's class +from it's `getRendererClass()` method. + +## LineAndPointRenderer +`LineAndPointRenderer` is the go-to renderer when it comes to `XYSeries`. It provides the most robust +feature set of any `XYSeriesRenderer` and has been the most carefully optimized for performance. Having +said that, `LineAndPointRenderer` isn't always the best tool for the job. Androidplot includes two +additional variations of `LineAndPointRenderer`: + +* **FastLineAndPointRenderer** - intended for use in apps displaying large amounts of dynamic data where +fast refresh rates are important. +* **AdvancedLineAndPointRenderer** - provides capabilities for dynamically coloring individual line +segments, etc. See the [ECGExample source](../demoapp/src/main/java/com/androidplot/demos/ECGExample.java) +in the demo app for a complete example. + +### Labeling Points +Most implementations of `XYSeriesRenderer` support labeling rendered points with text. This functionality +is activated by setting the `PointLabelFormatter` property in the associated `XYSeriesFormatter`. For +example, to enable point labels for a `LineAndPointFormatter`: + +// create a new `PointLabelFormatter` with a text size of 12sp and a color of red: + +```java +PointLabelFormatter plf = new PointLabelFormatter(); +plf.getTextPaint().setTextSize(PixelUtils.spToPix(12)); +plf.getTextPaint().setColor(Color.RED); +lineAndPointFormatter.setPointLabelFormatter(plf); +``` + +By default this will enable labels for all points using a string representation of the yVal of each +point. This behavior can be customized by setting a custom instance of `PointLabeler` on the `XYSeriesFormatter`: + +```java +formatter.setPointLabeler(new PointLabeler() { + @Override + public String getLabel(XYSeries series, int index) { + // draw labels on even indexes only: + if(index % 2 == 0) { + return "Y=" + series.getY(index).doubleValue(); + } + return null; + } +}); +``` + +## BarRenderer +See the [barcharts documentation](barchart.md). + +## CandlestickRenderer +See the [candlestick documentation](candlestick.md) + +# Drawing Smooth Lines +Smooth lines can be created by applying the +[Catmull-Rom interpolator](http://androidplot.com/smooth-curves-and-androidplot/) to your series' Format. + +# The Legend +By default, Androidplot will automatically produce a legend for your `XYPlot`. See [the legend](legend.md) doc +for usage details. + +# Graph Rotation +Androidplot provides the `Widget.setRotation(Widget.Rotation)` method for controlling the orientation +of Widgets. For example, if you wanted to create a bar graph where the bars extended across the screen +from left to right: + +xml +```xml +ap:graphRotation="ninety_degrees" +``` + +java +```java +plot.getGraph().setRotation(Widget.Rotation.NINETY_DEGREES); +``` + +# Optimization Tips +Here are a few suggestions to improve performance when plotting dynamic data: + +* Create your own implementation of `XYSeries` to work with your data in it's rawest form. +* Use `FastLineAndPointRenderer` instead of `LineAndPointRenderer`: + +```java +plot.addSeries(azimuthHistorySeries, + new FastLineAndPointRenderer.Formatter( + Color.rgb(100, 100, 200), null, null, null)); +``` + +* Consider averaging or subsampling very large datasets before rendering. If you have the time and +inclination, [the LTTB algorithm](http://skemman.is/stream/get/1946/15343/37285/3/SS_MSthesis.pdf) is +particularly well suited for downsampling `XYSeries` data. +* If possible, avoid rendering vertices (points). +* Disable anti-aliasing on your `XYSeriesFormatter`'s paint values: + +```java +LineAndPointFormatter format = new LineAndPointFormatter(...); +format.getLinePaint().setAntiAlias(false); +``` +# Converting Values +Because the coordinate system used by your `XYSeries` data is almost always different than the screen +coordinate system upon which the data is rendered, you'll often need to convert from one system to +the other. `XYPlot` provides convenience methods for this purpose: + +## Screen to Series Conversion +```java +// x +float screenX = ... +Number x = plot.screenToSeriesX(screenX); + +// y +float screenY = ... +Number y = plot.screenToSeriesY(screenY); + +// x and y +PointF screenCoords = ... +XYCoords xy = plot.screenToSeries(screenCoords) +``` + +## Series to Screen Conversion +```java +// x +Number x = ... +float screenX = plot.seriesToScreenX(x); + +// y +Number y = ... +float screenY = plot.seriesToScreenY(y); + +// x and y +XYCoords xy = ... +PointF screenCoords = plot.series.Screen(xy); +``` + +# Whats Next? +Explore [Advanced XYPlot Topics](advanced_xy_plot.md) diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..d015431a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1ec706de..e9defc20 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,22 +1,6 @@ -# -# Copyright 2015 AndroidPlot.com -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -#Wed Dec 02 22:16:29 CST 2015 +#Fri Jul 19 09:24:31 CDT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/misc/download_google_publisher_json.sh b/misc/download_google_publisher_json.sh new file mode 100755 index 00000000..54e2f349 --- /dev/null +++ b/misc/download_google_publisher_json.sh @@ -0,0 +1,9 @@ +# use curl to download the publisher acct json file from $PUBLISHER_ACCT_JSON_FILE_URI, if set, +# to the path/filename set in $PUBLISHER_ACCT_JSON_FILE. +if [[ $PUBLISHER_ACCT_JSON_FILE && ${PUBLISHER_ACCT_JSON_FILE} && $PUBLISHER_ACCT_JSON_FILE_URI && ${PUBLISHER_ACCT_JSON_FILE_URI} ]] +then + echo "Keystore detected - downloading..." + curl -L -o ${PUBLISHER_ACCT_JSON_FILE} ${PUBLISHER_ACCT_JSON_FILE_URI} +else + echo "Keystore uri not set. .APK artifact will not be signed." +fi \ No newline at end of file diff --git a/misc/inject_circle_build_number.sh b/misc/inject_circle_build_number.sh new file mode 100755 index 00000000..6ffd0d5d --- /dev/null +++ b/misc/inject_circle_build_number.sh @@ -0,0 +1,13 @@ +# uses the vcs build number as the android version code and adds it to the display version string. +#MAJOR_MINOR=`cat version.txt` +VERSION_CODE=$CIRCLE_BUILD_NUM +VERSION_NAME="$MAJOR_MINOR.$VERSION_CODE" + +echo "Building with Version Name: $VERSION_NAME" + +# update build.gradle with new version code: +perl -pi -w -e "s/theVersionCode = \d/theVersionCode = $VERSION_CODE/g;" build.gradle + +# update build.gradle with new version name: +#perl -pi -w -e 's/_dev_build_/'$VERSION_NAME'/g;' build.gradle +