diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..1e022fdb --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ +Thanks for taking the time to help make Androidplot better! Please note +that issues should be submitted on Github for bugs only. + +How-to questions may be posted under the [Androidplot tag on Stack Overflow](https://stackoverflow.com/questions/tagged/androidplot) +and feature requests etc. may be posted to the [Google Group Forum](https://groups.google.com/forum/#!forum/androidplot). + +When possible, please include the following in your bug report: + +* Description of the problem +* Steps to reproduce the issue +* Version(s) of Androidplot being used +* A stacktrace if the issue is causing a crash + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..d38f337e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,57 @@ +name: build androidplot +on: [push] +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set Up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '17' + + - name: Build and Test + run: ./gradlew testDebugUnitTest jacocoTestDebugUnitTestReport + + - name: Build Release + env: + ENCODED: ${{ secrets.DEMOAPP_KEYSTORE }} + KEYSTORE: keystore.jks + KEY_ALIAS: Key0 + KEYSTORE_PASSWORD: ${{ secrets.DEMOAPP_KEYSTORE_PASSWORD }} + KEY_PASSWORD: ${{ secrets.DEMOAPP_KEY_PASSWORD }} + run: | + echo $ENCODED | base64 -di > "${GITHUB_WORKSPACE}/demoapp/keystore.jks" + ./gradlew assembleRelease + +# TODO: get javadoc working again at some point +# - name: Javadoc +# run: ./gradlew javadoc + + - name: Code Coverage + run: bash <(curl -s https://codecov.io/bash) + + - uses: actions/upload-artifact@v3 + with: + name: Core Library + path: androidplot-core/build/outputs/aar/*.aar + retention-days: 30 + + - uses: actions/upload-artifact@v3 + if: github.ref_name == 'master' + with: + name: Demoapp APK + path: demoapp/build/outputs/apk/release/*.apk + retention-days: 30 + + - name: Stage for Maven Central + if: github.ref_name == 'master' + env: + OSSRH_ACTOR: ${{ secrets.OSSRH_ACTOR }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} + run: ./gradlew publish \ No newline at end of file diff --git a/.gitignore b/.gitignore index 58204be4..694f2995 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,5 @@ DemoApp/.settings DemoApp/bin DemoApp/gen DemoApp/target -.idea/libraries -.idea/*.xml +.idea **/R.java diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 8d25eece..9617a26e 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,13 @@ -# ![image](docs/images/aplogo_small.png) Androidplot [![CircleCI](https://circleci.com/gh/halfhp/androidplot.svg?style=shield)](https://circleci.com/gh/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) +# ![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) -A library for creating dynamic and static charts in Android apps. It’s designed from the ground up for Android, -compatible with all versions of Android from 1.6 onward and is **used by over -[1,000 apps](http://www.appbrain.com/stats/libraries/details/androidplot/androidplot) on Google Play**. +A library for creating dynamic and static charts in Android apps. -[![Codix](http://codix.io/badge/halfhp/androidplot)](http://codix.io/repo/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. -If you enjoy the lib, please [rate us on codix.io](http://codix.io/repo/halfhp/androidplot)! +If you enjoy the lib, please [rate us on codix.io](http://codix.io/gh/repo/halfhp/androidplot)! - - - - - - - + **Features:** @@ -49,7 +42,7 @@ Technical questions should be posted using the [androidplot tag](http://stackove # License Androidplot has been made available under the Apache 2.0 license: - Copyright 2016 Androidplot.com + Copyright 2021 Androidplot.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..2f7efbea --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-minimal \ No newline at end of file diff --git a/androidplot-core/build.gradle b/androidplot-core/build.gradle index 2ead754f..b37b2b4a 100644 --- a/androidplot-core/build.gradle +++ b/androidplot-core/build.gradle @@ -14,30 +14,84 @@ * limitations under the License. */ -apply plugin: 'com.android.library' -apply plugin: 'com.github.dcendents.android-maven' -apply plugin: 'com.jfrog.bintray' -apply plugin: 'com.vanniktech.android.junit.jacoco' -apply plugin: 'com.github.kt3k.coveralls' +plugins { + id 'com.android.library' + id 'com.mxalbert.gradle.jacoco-android' + id 'maven-publish' + id 'signing' +} + +class AttrMarkdown extends DefaultTask { + + def inFile + def outFile + + @TaskAction + def generate() { + def input = project.file(inFile) + + def output = project.file(outFile) + if (output.exists()) { + output.delete() + } + output.parentFile.mkdirs() + + input.text.findAll(//) { match, g1 -> + g1 + if (!g1.startsWith("NODOC")) { + output.append(g1) + output.append "\n\n" + } + } + } +} + +/** + * Generates xml attrs markdown docs. To run: + * at the command line from the project root dir type: + * ./gradlew generateAttrsMarkdown + * + * The generated doc will appear in / replace androidplot/docs/attrs.md + */ +task generateAttrsMarkdown(type: AttrMarkdown) { + inFile = { "src/main/res/values/attrs.xml" } + outFile = { "../docs/attrs.md" } +} android { - compileSdkVersion theCompileSdkVersion - buildToolsVersion theBuildToolsVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + defaultConfig { - versionCode theVersionCode - versionName theVersionName + compileSdk theCompileSdkVersion minSdkVersion theMinSdkVersion targetSdkVersion theTargetSdkVersion testApplicationId "com.androidplot.test" } - /** - * TODO: enable and address lint issues. - */ - lintOptions { + testOptions { + unitTests.all { + jacoco { + includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] + } + } + } + lint { abortOnError false } + namespace 'com.androidplot' + + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } } group = 'com.androidplot' @@ -48,98 +102,88 @@ def gitUrl = 'https://github.com/halfhp/androidplot.git' dependencies { - compile 'com.halfhp.fig:figlib:1.0.3' - testCompile "org.mockito:mockito-core:1.10.19" - testCompile group: 'junit', name: 'junit', version: '4.12' - testCompile "org.robolectric:robolectric:3.1" - - // temp fix for: - // https://github.com/robolectric/robolectric/issues/1932 - testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1' -} - -task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - failOnError false - options { - links "http://docs.oracle.com/javase/7/docs/api/" - linksOffline "http://d.android.com/reference","${android.sdkDirectory}/docs/reference" - } - exclude '**/BuildConfig.java' - exclude '**/R.java' -} + implementation 'com.halfhp.fig:figlib:1.0.11' + implementation 'androidx.annotation:annotation:1.8.2' -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir + testImplementation "org.mockito:mockito-core:5.7.0" + testImplementation group: 'junit', name: 'junit', version: '4.13.2' + testImplementation "org.robolectric:robolectric:4.7.3" } -task sourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.srcDirs -} +// TODO: get javadoc working again at some point +//task javadoc(type: Javadoc) { +// source = android.sourceSets.main.java.srcDirs +// classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +// failOnError false +// options { +// links "http://docs.oracle.com/javase/7/docs/api/" +// linksOffline "http://d.android.com/reference","${android.sdkDirectory}/docs/reference" +// } +// exclude '**/BuildConfig.java' +// exclude '**/R.java' +//} +// + + +// TODO: get javadoc working again at some point +//javadoc { +// options.overview = "src/main/java/overview.html" +//} + +afterEvaluate { + publishing { + repositories { + maven { + name = "MavenCentral" + url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + credentials { + username = System.getenv("OSSRH_ACTOR") + password = System.getenv("OSSRH_TOKEN") + } + } + } -install { - repositories.mavenInstaller { - pom { - project { - packaging 'aar' - name 'Androidplot Core Library' - description = "Androidplot is a library for creating dynamic and static charts within your Android application." - url siteUrl - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + publications { + release(MavenPublication) { + from components.release + + // You can then customize attributes of the publication as shown below. + groupId = 'com.androidplot' + artifactId = 'androidplot-core' + version = theVersionName + + pom { + packaging 'aar' + name = 'Androidplot' + description = "Configure any object from XML." + url = gitUrl + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } } - } - developers { - developer { - id 'halfhp' - name 'Nick Fellows' - email 'halfhp@gmail.com' + developers { + developer { + id = 'halfhp' + name = 'Nick Fellows' + email = 'halfhp@gmail.com' + } + } + scm { + connection = gitUrl + developerConnection = gitUrl + url = gitUrl } - } - scm { - connection gitUrl - developerConnection gitUrl - url siteUrl - } } } } } -bintray { - - // these environment vars must be set when deploying to bintray. - // note: BINTRAY_PASSWORD is actually the api key, but naming it 'password' - // tells the CI environment to obfuscate the value in public logs. - user = System.getenv('BINTRAY_USER') - key = System.getenv('BINTRAY_PASSWORD') - - - configurations = ['archives'] - pkg { - version { - name = theVersionName - } - repo = "androidplot" - name = "com.androidplot:androidplot-core" - issueTrackerUrl = "https://github.com/halfhp/androidplot/issues" - websiteUrl = siteUrl - vcsUrl = gitUrl - licenses = ["Apache-2.0"] - } -} - -javadoc { - options.overview = "src/main/java/overview.html" -} - -artifacts { - archives javadocJar - archives sourcesJar +signing { + def signingKey = System.getenv("SIGNING_KEY") + def signingPassword = System.getenv("SIGNING_PASSWORD") + useInMemoryPgpKeys(signingKey, signingPassword) + sign(publishing.publications) } diff --git a/androidplot-core/src/main/AndroidManifest.xml b/androidplot-core/src/main/AndroidManifest.xml index e83183d1..3043c930 100644 --- a/androidplot-core/src/main/AndroidManifest.xml +++ b/androidplot-core/src/main/AndroidManifest.xml @@ -16,5 +16,5 @@ - + \ No newline at end of file diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index 0fe39d2b..63d2e831 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -1,1011 +1,1077 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.*; -import android.os.Build; -import android.os.Looper; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import com.androidplot.exception.PlotRenderException; -import com.androidplot.ui.*; -import com.androidplot.ui.Formatter; -import com.androidplot.ui.TextOrientation; -import com.androidplot.ui.widget.TextLabelWidget; -import com.androidplot.ui.SeriesRenderer; -import com.androidplot.util.AttrUtils; -import com.androidplot.util.DisplayDimensions; -import com.androidplot.util.PixelUtils; -import com.androidplot.ui.HorizontalPositioning; -import com.androidplot.ui.VerticalPositioning; -import com.halfhp.fig.*; - -import java.lang.reflect.Field; -import java.util.*; - -/** - * Base class for all Plot implementations. - */ -public abstract class Plot, - RegistryType extends SeriesRegistry> - extends View implements Resizable { - private static final String TAG = Plot.class.getName(); - private static final String XML_ATTR_PREFIX = "androidplot"; - private static final String BASE_PACKAGE = "com.androidplot."; - - private static final int DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP = 10; - - public DisplayDimensions getDisplayDimensions() { - return displayDims; - } - - /** - * Used for caching renderer instances. Note that once a renderer is initialized it remains initialized - * for the life of the application; does not and should not be destroyed until the application exits. - */ - public HashMap, RendererType> getRenderers() { - return renderers; - } - - /** - * Associates lists series and getFormatter pairs with the class of the Renderer used to render them. - */ - public RegistryType getRegistry() { - return registry; - } - - public void setRegistry(RegistryType registry) { - this.registry = registry; - for(BundleType bundle : registry.getSeriesAndFormatterList()) { - attachSeries(bundle.getSeries(), bundle.getFormatter()); - } - } - - /** - * - * @return A new instance of RegistryType - */ - protected abstract RegistryType getRegistryInstance(); - - public TextLabelWidget getTitle() { - return title; - } - - public void setTitle(TextLabelWidget title) { - this.title = title; - } - - public void setTitle(String title) { - getTitle().setText(title); - } - - public enum BorderStyle { - ROUNDED, - SQUARE, - NONE - } - - /** - * The RenderMode used by a Plot to draw it's self onto the screen. The RenderMode can be set - * in two ways. - * - * In an xml layout: - * - * - * - * - * - * Programatically: - * - * - * XYPlot myPlot = new XYPlot(context "MyPlot", Plot.RenderMode.USE_MAIN_THREAD); - * - * - * A Plot's RenderMode cannot be changed after the plot has been initialized. - * @since 0.5.1 - */ - public enum RenderMode { - /** - * Use a second thread and an off-screen buffer to do drawing. This is the preferred method - * of drawing dynamic data and static data that consists of a large number of points. This mode - * provides more efficient CPU utilization at the cost of increased memory usage. As of - * version 0.5.1 this is the default RenderMode. - * - * XML value: use_background_thread - * @since 0.5.1 - */ - USE_BACKGROUND_THREAD, - - /** - * Do everything in the primary thread. This is the preferred method of drawing static charts - * and dynamic data that consists of a small number of points. This mode uses less memory at - * the cost of poor CPU utilization. - * - * XML value: use_main_thread - * @since 0.5.1 - */ - USE_MAIN_THREAD - } - private BoxModel boxModel = new BoxModel(); - - // no border by default: - private BorderStyle borderStyle = Plot.BorderStyle.NONE; - private float borderRadiusX = 15; - private float borderRadiusY = 15; - private Paint borderPaint; - private Paint backgroundPaint; - private LayoutManager layoutManager; - private TextLabelWidget title; - private DisplayDimensions displayDims = new DisplayDimensions(); - private RenderMode renderMode = RenderMode.USE_MAIN_THREAD; - private final BufferedCanvas pingPong = new BufferedCanvas(); - - // used to get rid of flickering when drawing offScreenBitmap to the visible Canvas. - private final Object renderSynch = new Object(); - - private HashMap, RendererType> renderers; - - private RegistryType registry; - private final ArrayList listeners; - - private Thread renderThread; - private boolean keepRunning = false; - private boolean isIdle = true; - - { - listeners = new ArrayList<>(); - registry = getRegistryInstance(); - renderers = new HashMap<>(); - - borderPaint = new Paint(); - borderPaint.setColor(Color.rgb(150, 150, 150)); - borderPaint.setStyle(Paint.Style.STROKE); - borderPaint.setStrokeWidth(1.0f); - borderPaint.setAntiAlias(true); - backgroundPaint = new Paint(); - backgroundPaint.setColor(Color.DKGRAY); - backgroundPaint.setStyle(Paint.Style.FILL); - } - - - /** - * Any rendering that utilizes a buffer from this class should synchronize rendering on the instance of this class - * that is being used. - */ - private static class BufferedCanvas { - private volatile Bitmap bgBuffer; // all drawing is done on this buffer. - private volatile Bitmap fgBuffer; - private Canvas canvas = new Canvas(); - - /** - * Call this method once drawing on a Canvas retrieved by {@link #getCanvas()} to mark - * the buffer as fully rendered. Failure to call this method will result in nothing being drawn. - */ - public synchronized void swap() { - Bitmap tmp = bgBuffer; - bgBuffer = fgBuffer; - fgBuffer = tmp; - } - - public synchronized void resize(int h, int w) { - if (w <= 0 || h <= 0) { - bgBuffer = null; - fgBuffer = null; - } else { - bgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444); - fgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444); - } - } - - public void recycle() { - bgBuffer.recycle(); - bgBuffer = null; - - fgBuffer.recycle(); - fgBuffer = null; - System.gc(); - } - - /** - * Get a Canvas for drawing. Actual drawing should be synchronized on the instance - * of BufferedCanvas being used. - * @return The Canvas instance to draw onto. Returns null if drawing buffers have not - * been initialized a la {@link #resize(int, int)}. - */ - public synchronized Canvas getCanvas() { - if(bgBuffer != null) { - canvas.setBitmap(bgBuffer); - return canvas; - } else { - return null; - } - } - - /** - * @return The most recent fully rendered Bitmsp - */ - public Bitmap getBitmap() { - return fgBuffer; - } - } - - /** - * Convenience constructor - wraps {@link #Plot(android.content.Context, String, com.androidplot.Plot.RenderMode)}. - * RenderMode is set to {@link RenderMode#USE_BACKGROUND_THREAD}. - * @param context - * @param title The display title of this Plot. - */ - public Plot(Context context, String title) { - this(context, title, RenderMode.USE_MAIN_THREAD); - } - - /** - * Used for programmatic instantiation. - * @param context - * @param title The display title of this Plot. - */ - public Plot(Context context, String title, RenderMode mode) { - super(context); - this.renderMode = mode; - init(context, null, 0); - getTitle().setText(title); - } - - - /** - * Required by super-class. Extending class' implementations should add - * the following code immediately before exiting to ensure that loadAttrs - * is called only once by the derived class: - * - * if(getClass().equals(DerivedPlot.class) { - * loadAttrs(context, attrs); - * } - * - * - * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet)} - * for an example. - * @param context - * @param attrs - */ - public Plot(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs, 0); - } - - /** - * Required by super-class. Extending class' implementations should add - * the following code immediately before exiting to ensure that loadAttrs - * is called only once by the derived class: - * - * if(getClass().equals(DerivedPlot.class) { - * loadAttrs(context, attrs); - * } - * - * - * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet, int)} - * for an example. - * @param context - * @param attrs - * @param defStyle - */ - public Plot(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context, attrs, defStyle); - } - - /** - * Can be overridden by derived classes to control hardware acceleration state. - * Note that this setting is only used on Honeycomb and later environments. - * @return True if hardware acceleration is allowed, false otherwise. - * @since 0.5.1 - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - protected boolean isHwAccelerationSupported() { - return false; - } - - /** - * Sets the render mode used by the Plot. - * WARNING: This method is not currently designed for general use outside of Configurator. - * Attempting to reassign the render mode at runtime will result in unexpected behavior. - * @param mode - */ - public void setRenderMode(RenderMode mode) { - this.renderMode = mode; - } - - /** - * Concrete implementations may do any final setup / initialization - * here. Immediately following this method's invocation, AndroidPlot assumes - * that the Plot instance is ready for final configuration via the Configurator. - */ - protected void onPreInit() { - // nothing to do by default - } - - /** - * Invoked immediately following configurator / styleable attr application. - */ - protected void onAfterConfig() { - // nothing to do by default - } - - protected final void init(Context context, AttributeSet attrs, int defStyle) { - PixelUtils.init(context); - layoutManager = new LayoutManager(); - title = new TextLabelWidget(layoutManager, new Size(25, - SizeMode.ABSOLUTE, 100, - SizeMode.ABSOLUTE), - TextOrientation.HORIZONTAL); - title.position(0, HorizontalPositioning.RELATIVE_TO_CENTER, 0, - VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.TOP_MIDDLE); - - // initialize attr defaults: - title.getLabelPaint().setTextSize( - PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP)); - - onPreInit(); - // make sure the title widget is always the topmost widget: - layoutManager.moveToTop(title); - if(context != null && attrs != null) { - loadAttrs(attrs, defStyle); - } - - onAfterConfig(); - - layoutManager.onPostInit(); - if (renderMode == RenderMode.USE_BACKGROUND_THREAD) { - renderThread = new Thread(new Runnable() { - @Override - public void run() { - - keepRunning = true; - while (keepRunning) { - isIdle = false; - synchronized (pingPong) { - Canvas c = pingPong.getCanvas(); - renderOnCanvas(c); - pingPong.swap(); - } - synchronized (renderSynch) { - postInvalidate(); - // prevent this thread from becoming an orphan - // after the view is destroyed - if (keepRunning) { - try { - renderSynch.wait(); - } catch (InterruptedException e) { - keepRunning = false; - } - } - } - } - pingPong.recycle(); - } - }); - } - } - - /** - * If a styleable is available for the derived class, this method will be invoked with those attrs. - * The derived implementation is only responsible for setting derived class attributes, ie. it should - * not attempt to apply the Plot.title styleable attribute etc. Do not invoke recycle() on attrs. - * @param attrs Attrs for the derived class. - */ - protected abstract void processAttrs(TypedArray attrs); - - /** - * Apply base class attrs. - * @param attrs - */ - private void processBaseAttrs(TypedArray attrs) { - - // markup mode - boolean markupEnabled = attrs.getBoolean(R.styleable.Plot_markupEnabled, false); - setMarkupEnabled(markupEnabled); - - // renderMode - RenderMode renderMode = RenderMode.values() - [attrs.getInt(R.styleable.Plot_renderMode, getRenderMode().ordinal())]; - if(renderMode != getRenderMode()) { - setRenderMode(renderMode); - } - - // margins & padding - AttrUtils.configureBoxModelable(attrs, boxModel, R.styleable.Plot_marginTop, R.styleable.Plot_marginBottom, - R.styleable.Plot_marginLeft, R.styleable.Plot_marginRight, R.styleable.Plot_paddingTop, - R.styleable.Plot_paddingBottom, R.styleable.Plot_paddingLeft, R.styleable.Plot_paddingRight); - - // title - getTitle().setText(attrs.getString(R.styleable.Plot_title)); - getTitle().getLabelPaint().setTextSize( - attrs.getDimension(R.styleable.Plot_titleTextSize, - PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP))); - - getTitle().getLabelPaint().setColor(attrs.getColor( - R.styleable.Plot_titleTextColor, getTitle().getLabelPaint().getColor())); - - getBackgroundPaint().setColor( - attrs.getColor(R.styleable.Plot_backgroundColor, getBackgroundPaint().getColor())); - - AttrUtils.configureLinePaint(attrs, getBorderPaint(), - R.styleable.Plot_borderColor, R.styleable.Plot_borderThickness); - } - - /** - * Parse XML Attributes. Should only be called once and at the end of the base class constructor. - * The first-pass attempts to locate styleable attributes and apply those first. After that, - * configurator-style attributes are applied, overriding any styleable attrs that may have - * been previously applied. - * - * @param attrs - */ - private void loadAttrs(AttributeSet attrs, int defStyle) { - - if (attrs != null) { - - Field styleableFieldInR = null; - TypedArray typedAttrs = null; - - Class styleableClass = R.styleable.class; - String styleableName = getClass().getName().substring(BASE_PACKAGE.length()); - styleableName = styleableName.replace('.', '_'); - try { - /** - * Use reflection to safely run for the existence of styleable defs for Plot - * and it's derivatives. This safety run is necessary to avoid runtime exceptions - * in apps that don't include Androidplot as a .aar and won't have access to - * the resources defined in the core library. - */ - styleableFieldInR = styleableClass.getField(styleableName); - } catch (NoSuchFieldException e) { - Log.d(TAG, "Styleable definition not found for: " + styleableName); - } - if (styleableFieldInR != null) { - try { - int[] resIds = (int[]) styleableFieldInR.get(null); - typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0); - } catch (IllegalAccessException e) { - // nothing to do - } finally { - if (typedAttrs != null) { - // apply derived class' attrs: - processAttrs(typedAttrs); - typedAttrs.recycle(); - } - } - } - - try { - styleableFieldInR = styleableClass.getField(Plot.class.getSimpleName()); - if (styleableFieldInR != null) { - int[] resIds = (int[]) styleableFieldInR.get(null); - typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0); - } - } catch (IllegalAccessException e) { - // nothing to do - } catch (NoSuchFieldException e) { - Log.d(TAG, "Styleable definition not found for: " + Plot.class.getSimpleName()); - } finally { - if (typedAttrs != null) { - // apply base attrs: - processBaseAttrs(typedAttrs); - typedAttrs.recycle(); - } - } - - // apply "configurator" attrs: (overrides any previously applied styleable attrs) - // filter out androidplot prefixed attrs: - HashMap attrHash = new HashMap(); - for (int i = 0; i < attrs.getAttributeCount(); i++) { - String attrName = attrs.getAttributeName(i); - - // case insensitive run to see if this attr begins with our prefix: - if (attrName != null && attrName.toUpperCase().startsWith(XML_ATTR_PREFIX.toUpperCase())) { - attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i)); - } - } - Fig.configure(getContext(), this, attrHash); - } - } - - public RenderMode getRenderMode() { - return renderMode; - } - - public synchronized boolean addListener(PlotListener listener) { - return !listeners.contains(listener) && listeners.add(listener); - } - - public synchronized boolean removeListener(PlotListener listener) { - return listeners.remove(listener); - } - - protected ArrayList getListeners() { - return listeners; - } - - protected void notifyListenersBeforeDraw(Canvas canvas) { - for (PlotListener listener : listeners) { - listener.onBeforeDraw(this, canvas); - } - } - - protected void notifyListenersAfterDraw(Canvas canvas) { - for (PlotListener listener : listeners) { - listener.onAfterDraw(this, canvas); - } - } - - /** - * Convenience method to add a multiple series at once using the same formatter. - * If a problem is encountered, the method immediately returns false and the plot - * will contain whatever series were added before the failure. - * @param formatter - * @param series - * @return True if all series were successfully added, false otherwise. - * @since 0.9.7 - */ - public synchronized boolean addSeries(FormatterType formatter, SeriesType... series) { - for(SeriesType s : series) { - if(!addSeries(s, formatter)) { - return false; - } - } - return true; - } - - /** - * Add a new Series to the Plot. - * @param series - * @param formatter - * @return True if the series was added or false if the series / formatter pair already exists in the registry. - */ - public synchronized boolean addSeries(SeriesType series, FormatterType formatter) { - final boolean result = getRegistry().add(series, formatter); - attachSeries(series, formatter); - return result; - } - - protected void attachSeries(SeriesType series, FormatterType formatter) { - - Class rendererClass = formatter.getRendererClass(); - - // initialize the Renderer if necessary: - if(!getRenderers().containsKey(rendererClass)) { - getRenderers().put(rendererClass, (RendererType) formatter.getRendererInstance(this)); - } - - // if this series implements PlotListener, add it as a listener: - if(series instanceof PlotListener) { - addListener((PlotListener)series); - } - } - - /** - * - * @param series - * @param rendererClass - * @return The {@link SeriesBundle} that matches the series and rendererClass params, or null if one is not found. - */ - protected SeriesBundle getSeries(SeriesType series, Class 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 (isIdle) { - synchronized (renderSynch) { - renderSynch.notify(); - } - } - } else if(renderMode == RenderMode.USE_MAIN_THREAD) { - - // are we on the UI thread? - if (Looper.myLooper() == Looper.getMainLooper()) { - invalidate(); - } else { - postInvalidate(); - } - } else { - throw new IllegalArgumentException("Unsupported Render Mode: " + renderMode); - } - } - - @Override - public synchronized void layout(final DisplayDimensions dims) { - this.displayDims = dims; - layoutManager.layout(displayDims); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - synchronized(renderSynch) { - keepRunning = false; - renderSynch.notify(); - } - } - - - @Override - protected synchronized void onSizeChanged (int w, int h, int oldw, int oldh) { - - // update pixel conversion values - PixelUtils.init(getContext()); - - // disable hardware acceleration if it's not explicitly supported - // by the current Plot implementation. this run only applies to - // honeycomb and later environments. - if (Build.VERSION.SDK_INT >= 11) { - if (!isHwAccelerationSupported() && isHardwareAccelerated()) { - setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } - } - - // pingPong is only used in background rendering mode. - if(renderMode == RenderMode.USE_BACKGROUND_THREAD) { - pingPong.resize(h, w); - } - - RectF cRect = new RectF(0, 0, w, h); - RectF mRect = boxModel.getMarginatedRect(cRect); - RectF pRect = boxModel.getPaddedRect(mRect); - - layout(new DisplayDimensions(cRect, mRect, pRect)); - super.onSizeChanged(w, h, oldw, oldh); - if(renderThread != null && !renderThread.isAlive()) { - renderThread.start(); - } - } - - /** - * Called whenever the plot needs to be drawn via the Handler, which invokes invalidate(). - * Should never be called directly; use {@link #redraw()} instead. - * @param canvas - */ - @Override - protected void onDraw(Canvas canvas) { - if (renderMode == RenderMode.USE_BACKGROUND_THREAD) { - synchronized(pingPong) { - Bitmap bmp = pingPong.getBitmap(); - if(bmp != null) { - canvas.drawBitmap(bmp, 0, 0, null); - } - } - } else if (renderMode == RenderMode.USE_MAIN_THREAD) { - renderOnCanvas(canvas); - } else { - throw new IllegalArgumentException("Unsupported Render Mode: " + renderMode); - } - } - - /** - * Renders the plot onto a canvas. Used by both main thread to draw directly - * onto the View's canvas as well as by background draw to render onto a - * Bitmap buffer. At the end of the day this is the main entry for a plot's - * "heavy lifting". - * @param canvas - */ - protected synchronized void renderOnCanvas(Canvas canvas) { - try { - // any series interested in synchronizing with plot should - // implement PlotListener.onBeforeDraw(...) and do a read lock from within its - // invocation. This is the entry point into that call: - notifyListenersBeforeDraw(canvas); - try { - // need to completely erase what was on the canvas before redrawing, otherwise - // some odd aliasing artifacts begin to build up around the edges of aa'd entities - // over time. - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - if (backgroundPaint != null) { - drawBackground(canvas, displayDims.marginatedRect); - } - - layoutManager.draw(canvas); - - if (getBorderPaint() != null) { - drawBorder(canvas, displayDims.marginatedRect); - } - } catch (PlotRenderException e) { - Log.e(TAG, "Exception while rendering Plot.", e); - } catch (Exception e) { - Log.e(TAG, "Exception while rendering Plot.", e); - } - - isIdle = true; - // any series interested in synchronizing with plot should - // implement PlotListener.onAfterDraw(...) and do a read unlock from within that - // invocation. This is the entry point for that invocation. - notifyListenersAfterDraw(canvas); - } finally { - } - } - - - /** - * Sets the visual style of the plot's border. - * @param style - * @param radiusX Sets the X radius for BorderStyle.ROUNDED. Use null for all other styles. - * @param radiusY Sets the Y radius for BorderStyle.ROUNDED. Use null for all other styles. - */ - public void setBorderStyle(BorderStyle style, Float radiusX, Float radiusY) { - if (style == Plot.BorderStyle.ROUNDED) { - if (radiusX == null || radiusY == null){ - throw new IllegalArgumentException("radiusX and radiusY cannot be null when using BorderStyle.ROUNDED"); - } - this.borderRadiusX = radiusX; - this.borderRadiusY = radiusY; - } - this.borderStyle = style; - } - - /** - * Draws the plot's outer border. - * @param canvas - * @throws PlotRenderException - */ - protected void drawBorder(Canvas canvas, RectF dims) { - drawRect(canvas, dims, borderPaint); - } - - protected void drawBackground(Canvas canvas, RectF dims) { - drawRect(canvas, dims, backgroundPaint); - } - - protected void drawRect(Canvas canvas, RectF dims, Paint paint) { - switch (borderStyle) { - case ROUNDED: - canvas.drawRoundRect(dims, borderRadiusX, borderRadiusY, paint); - break; - case SQUARE: - default: - canvas.drawRect(dims, paint); - break; - } - } - - public LayoutManager getLayoutManager() { - return layoutManager; - } - - public void setLayoutManager(LayoutManager layoutManager) { - this.layoutManager = layoutManager; - } - - public Paint getBackgroundPaint() { - return backgroundPaint; - } - - public void setBackgroundPaint(Paint backgroundPaint) { - this.backgroundPaint = backgroundPaint; - } - - /** - * Convenience method - wraps the individual setMarginXXX methods into a single method. - * @param left - * @param top - * @param right - * @param bottom - */ - public void setPlotMargins(float left, float top, float right, float bottom) { - setPlotMarginLeft(left); - setPlotMarginTop(top); - setPlotMarginRight(right); - setPlotMarginBottom(bottom); - } - - /** - * Convenience method - wraps the individual setPaddingXXX methods into a single method. - * @param left - * @param top - * @param right - * @param bottom - */ - public void setPlotPadding(float left, float top, float right, float bottom) { - setPlotPaddingLeft(left); - setPlotPaddingTop(top); - setPlotPaddingRight(right); - setPlotPaddingBottom(bottom); - } - - public float getPlotMarginTop() { - return boxModel.getMarginTop(); - } - - public void setPlotMarginTop(float plotMarginTop) { - boxModel.setMarginTop(plotMarginTop); - } - - public float getPlotMarginBottom() { - return boxModel.getMarginBottom(); - } - - public void setPlotMarginBottom(float plotMarginBottom) { - boxModel.setMarginBottom(plotMarginBottom); - } - - public float getPlotMarginLeft() { - return boxModel.getMarginLeft(); - } - - public void setPlotMarginLeft(float plotMarginLeft) { - boxModel.setMarginLeft(plotMarginLeft); - } - - public float getPlotMarginRight() { - return boxModel.getMarginRight(); - } - - public void setPlotMarginRight(float plotMarginRight) { - boxModel.setMarginRight(plotMarginRight); - } - - public float getPlotPaddingTop() { - return boxModel.getPaddingTop(); - } - - public void setPlotPaddingTop(float plotPaddingTop) { - boxModel.setPaddingTop(plotPaddingTop); - } - - public float getPlotPaddingBottom() { - return boxModel.getPaddingBottom(); - } - - public void setPlotPaddingBottom(float plotPaddingBottom) { - boxModel.setPaddingBottom(plotPaddingBottom); - } - - public float getPlotPaddingLeft() { - return boxModel.getPaddingLeft(); - } - - public void setPlotPaddingLeft(float plotPaddingLeft) { - boxModel.setPaddingLeft(plotPaddingLeft); - } - - public float getPlotPaddingRight() { - return boxModel.getPaddingRight(); - } - - public void setPlotPaddingRight(float plotPaddingRight) { - boxModel.setPaddingRight(plotPaddingRight); - } - - public Paint getBorderPaint() { - return borderPaint; - } - - /** - * Set's the paint used to draw the border. Note that this method - * copies borderPaint and set's the copy's Paint.Style attribute to - * Paint.Style.STROKE. - * @param borderPaint - */ - public void setBorderPaint(Paint borderPaint) { - if(borderPaint == null) { - this.borderPaint = null; - } else { - this.borderPaint = new Paint(borderPaint); - this.borderPaint.setStyle(Paint.Style.STROKE); - } - } -} +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.RectF; +import android.os.Build; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.androidplot.ui.Anchor; +import com.androidplot.ui.BoxModel; +import com.androidplot.ui.Formatter; +import com.androidplot.ui.HorizontalPositioning; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.Resizable; +import com.androidplot.ui.SeriesBundle; +import com.androidplot.ui.SeriesRenderer; +import com.androidplot.ui.Size; +import com.androidplot.ui.SizeMode; +import com.androidplot.ui.TextOrientation; +import com.androidplot.ui.VerticalPositioning; +import com.androidplot.ui.widget.TextLabelWidget; +import com.androidplot.util.AttrUtils; +import com.androidplot.util.DisplayDimensions; +import com.androidplot.util.PixelUtils; +import com.halfhp.fig.Fig; +import com.halfhp.fig.FigException; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Base class for all Plot implementations. + */ +public abstract class Plot, + RegistryType extends SeriesRegistry> + extends View implements Resizable { + private static final String TAG = Plot.class.getName(); + private static final String XML_ATTR_PREFIX = "androidplot"; + private static final String BASE_PACKAGE = "com.androidplot."; + + private static final int DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP = 10; + + public DisplayDimensions getDisplayDimensions() { + return displayDims; + } + + /** + * Used for caching renderer instances. Note that once a renderer is initialized it remains initialized + * for the life of the application; does not and should not be destroyed until the application exits. + */ + public HashMap, RendererType> getRenderers() { + return renderers; + } + + /** + * Associates lists series and getFormatter pairs with the class of the Renderer used to render them. + */ + public RegistryType getRegistry() { + return registry; + } + + public void setRegistry(RegistryType registry) { + this.registry = registry; + for(BundleType bundle : registry.getSeriesAndFormatterList()) { + attachSeries(bundle.getSeries(), bundle.getFormatter()); + } + } + + /** + * + * @return A new instance of RegistryType + */ + protected abstract RegistryType getRegistryInstance(); + + public TextLabelWidget getTitle() { + return title; + } + + public void setTitle(TextLabelWidget title) { + this.title = title; + } + + public void setTitle(String title) { + getTitle().setText(title); + } + + public enum BorderStyle { + ROUNDED, + SQUARE, + NONE + } + + /** + * The RenderMode used by a Plot to draw it's self onto the screen. The RenderMode can be set + * in two ways. + * + * In an xml layout: + * + * + * + * + * + * Programatically: + * + * + * XYPlot myPlot = new XYPlot(context "MyPlot", Plot.RenderMode.USE_MAIN_THREAD); + * + * + * A Plot's RenderMode cannot be changed after the plot has been initialized. + * @since 0.5.1 + */ + public enum RenderMode { + /** + * Do everything in the primary thread. This is the preferred method of drawing static charts + * and dynamic data that consists of a small number of points. This mode uses less memory at + * the cost of poor CPU utilization. + * + * XML value: use_main_thread + * @since 0.5.1 + */ + USE_MAIN_THREAD, + + /** + * Use a second thread and an off-screen buffer to do drawing. This is the preferred method + * of drawing dynamic data and static data that consists of a large number of points. This mode + * provides more efficient CPU utilization at the cost of increased memory usage. As of + * version 0.5.1 this is the default RenderMode. + * + * XML value: use_background_thread + * @since 0.5.1 + */ + USE_BACKGROUND_THREAD + } + private BoxModel boxModel = new BoxModel(); + + // no border by default: + private BorderStyle borderStyle = Plot.BorderStyle.NONE; + private float borderRadiusX = 15; + private float borderRadiusY = 15; + private Paint borderPaint; + private Paint backgroundPaint; + private LayoutManager layoutManager; + private TextLabelWidget title; + private DisplayDimensions displayDims = new DisplayDimensions(); + private RenderMode renderMode = RenderMode.USE_MAIN_THREAD; + private final BufferedCanvas pingPong = new BufferedCanvas(); + + // used to get rid of flickering when drawing offScreenBitmap to the visible Canvas. + private final Object renderSync = new Object(); + + private HashMap, RendererType> renderers; + + private RegistryType registry; + private final ArrayList listeners; + + private Thread renderThread; + private boolean keepRunning = false; + private boolean isIdle = true; + + { + listeners = new ArrayList<>(); + registry = getRegistryInstance(); + renderers = new HashMap<>(); + + borderPaint = new Paint(); + borderPaint.setColor(Color.rgb(150, 150, 150)); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setStrokeWidth(1.0f); + borderPaint.setAntiAlias(true); + backgroundPaint = new Paint(); + backgroundPaint.setColor(Color.DKGRAY); + backgroundPaint.setStyle(Paint.Style.FILL); + } + + + /** + * Any rendering that utilizes a buffer from this class should synchronize rendering on the instance of this class + * that is being used. + */ + private static class BufferedCanvas { + private int lastHeight = 0; + private int lastWidth = 0; + private volatile Bitmap bgBuffer; // all drawing is done on this buffer. + private volatile Bitmap fgBuffer; + private Canvas canvas = new Canvas(); + + /** + * Call this method once drawing on a Canvas retrieved by {@link #getCanvas()} to mark + * the buffer as fully rendered. Failure to call this method will result in nothing being drawn. + */ + public synchronized void swap() { + Bitmap tmp = bgBuffer; + bgBuffer = fgBuffer; + fgBuffer = tmp; + } + + /** + * Used when rendering in background mode and a view is attached + * but not resized, so that rendering buffers will be reinitialized. + */ + public void resizeToLast() { + resize(lastHeight, lastWidth); + } + + public synchronized void resize(int h, int w) { + if (w <= 0 || h <= 0) { + bgBuffer = null; + fgBuffer = null; + } else { + lastHeight = h; + lastWidth = w; + try { + bgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + fgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + } catch(IllegalArgumentException e) { + throw new IllegalArgumentException("Illegal argument passed to Bitmap.createBitmap. width: " + w + " height: " + h); + } + } + } + + public void recycle() { + /** + * TODO: Issue #93 There have been rare reports of NPE's originating from here. + * Most likely there is something deeper that is amiss, but for now we'll simply + * do a null check before recycling. + */ + if(bgBuffer != null) { + bgBuffer.recycle(); + bgBuffer = null; + } + + if(fgBuffer != null) { + fgBuffer.recycle(); + fgBuffer = null; + } + + System.gc(); + } + + /** + * Get a Canvas for drawing. Actual drawing should be synchronized on the instance + * of BufferedCanvas being used. + * @return The Canvas instance to draw onto. Returns null if drawing buffers have not + * been initialized a la {@link #resize(int, int)}. + */ + public synchronized Canvas getCanvas() { + if(bgBuffer != null) { + canvas.setBitmap(bgBuffer); + return canvas; + } else { + return null; + } + } + + /** + * @return The most recent fully rendered Bitmsp + */ + public Bitmap getBitmap() { + return fgBuffer; + } + } + + /** + * Convenience constructor - wraps {@link #Plot(android.content.Context, String, com.androidplot.Plot.RenderMode)}. + * RenderMode is set to {@link RenderMode#USE_BACKGROUND_THREAD}. + * @param context + * @param title The display title of this Plot. + */ + public Plot(Context context, String title) { + this(context, title, RenderMode.USE_MAIN_THREAD); + } + + /** + * Used for programmatic instantiation. + * @param context + * @param title The display title of this Plot. + */ + public Plot(Context context, String title, RenderMode mode) { + super(context); + this.renderMode = mode; + init(context, null, 0); + getTitle().setText(title); + } + + + /** + * Required by super-class. Extending class' implementations should add + * the following code immediately before exiting to ensure that loadAttrs + * is called only once by the derived class: + * + * if(getClass().equals(DerivedPlot.class) { + * loadAttrs(context, attrs); + * } + * + * + * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet)} + * for an example. + * @param context + * @param attrs + */ + public Plot(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + /** + * Required by super-class. Extending class' implementations should add + * the following code immediately before exiting to ensure that loadAttrs + * is called only once by the derived class: + * + * if(getClass().equals(DerivedPlot.class) { + * loadAttrs(context, attrs); + * } + * + * + * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet, int)} + * for an example. + * @param context + * @param attrs + * @param defStyle + */ + public Plot(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + /** + * Can be overridden by derived classes to control hardware acceleration state. + * Note that this setting is only used on Honeycomb and later environments. + * @return True if hardware acceleration is allowed, false otherwise. + * @since 0.5.1 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + protected boolean isHwAccelerationSupported() { + return false; + } + + /** + * Sets the render mode used by the Plot. + * WARNING: This method is not currently designed for general use outside of Configurator. + * Attempting to reassign the render mode at runtime will result in unexpected behavior. + * @param mode + */ + public void setRenderMode(RenderMode mode) { + this.renderMode = mode; + } + + /** + * Concrete implementations may do any final setup / initialization + * here. Immediately following this method's invocation, AndroidPlot assumes + * that the Plot instance is ready for final configuration via the Configurator. + */ + protected void onPreInit() { + // nothing to do by default + } + + /** + * Invoked immediately following configurator / styleable attr application. + */ + protected void onAfterConfig() { + // nothing to do by default + } + + protected final void init(Context context, AttributeSet attrs, int defStyle) { + PixelUtils.init(context); + layoutManager = new LayoutManager(); + title = new TextLabelWidget(layoutManager, new Size(25, + SizeMode.ABSOLUTE, 100, + SizeMode.ABSOLUTE), + TextOrientation.HORIZONTAL); + title.position(0, HorizontalPositioning.RELATIVE_TO_CENTER, 0, + VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.TOP_MIDDLE); + + // initialize attr defaults: + title.getLabelPaint().setTextSize( + PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP)); + + onPreInit(); + // make sure the title widget is always the topmost widget: + layoutManager.moveToTop(title); + if(context != null && attrs != null) { + loadAttrs(attrs, defStyle); + } + + onAfterConfig(); + + layoutManager.onPostInit(); + if (renderMode == RenderMode.USE_BACKGROUND_THREAD) { + startBackgroundRendering(); + } + } + + protected void startBackgroundRendering() { + if(renderThread != null) { + return; + } + + renderThread = new Thread(() -> { + System.out.println("Thread started with id " + this.hashCode()); + + keepRunning = true; + while (keepRunning) { + isIdle = false; + synchronized (pingPong) { + Canvas c = pingPong.getCanvas(); + renderOnCanvas(c); + pingPong.swap(); + } + synchronized (renderSync) { + postInvalidate(); + // prevent this thread from becoming an orphan + // after the view is destroyed + if (keepRunning) { + try { + renderSync.wait(); + } catch (InterruptedException e) { + keepRunning = false; + } + } + } + } + System.out.println("Thread exited with id " + this.hashCode()); + renderThread = null; + pingPong.recycle(); + }, "Androidplot renderThread"); + } + + /** + * If a styleable is available for the derived class, this method will be invoked with those attrs. + * The derived implementation is only responsible for setting derived class attributes, ie. it should + * not attempt to apply the Plot.title styleable attribute etc. Do not invoke recycle() on attrs. + * @param attrs Attrs for the derived class. + */ + protected abstract void processAttrs(TypedArray attrs); + + /** + * Apply base class attrs. + * @param attrs + */ + private void processBaseAttrs(TypedArray attrs) { + + // markup mode + boolean markupEnabled = attrs.getBoolean(R.styleable.Plot_markupEnabled, false); + setMarkupEnabled(markupEnabled); + + // renderMode + RenderMode renderMode = RenderMode.values() + [attrs.getInt(R.styleable.Plot_renderMode, getRenderMode().ordinal())]; + if(renderMode != getRenderMode()) { + setRenderMode(renderMode); + } + + // margins & padding + AttrUtils.configureBoxModelable(attrs, boxModel, R.styleable.Plot_marginTop, R.styleable.Plot_marginBottom, + R.styleable.Plot_marginLeft, R.styleable.Plot_marginRight, R.styleable.Plot_paddingTop, + R.styleable.Plot_paddingBottom, R.styleable.Plot_paddingLeft, R.styleable.Plot_paddingRight); + + // title + getTitle().setText(attrs.getString(R.styleable.Plot_title)); + getTitle().getLabelPaint().setTextSize( + attrs.getDimension(R.styleable.Plot_titleTextSize, + PixelUtils.spToPix(DEFAULT_TITLE_WIDGET_TEXT_SIZE_SP))); + + getTitle().getLabelPaint().setColor(attrs.getColor( + R.styleable.Plot_titleTextColor, getTitle().getLabelPaint().getColor())); + + getBackgroundPaint().setColor( + attrs.getColor(R.styleable.Plot_backgroundColor, getBackgroundPaint().getColor())); + + AttrUtils.configureLinePaint(attrs, getBorderPaint(), + R.styleable.Plot_borderColor, R.styleable.Plot_borderThickness); + } + + /** + * Parse XML Attributes. Should only be called once and at the end of the base class constructor. + * The first-pass attempts to locate styleable attributes and apply those first. After that, + * configurator-style attributes are applied, overriding any styleable attrs that may have + * been previously applied. + * + * @param attrs + */ + private void loadAttrs(AttributeSet attrs, int defStyle) { + + if (attrs != null) { + + Field styleableFieldInR = null; + TypedArray typedAttrs = null; + + Class styleableClass = R.styleable.class; + String styleableName = getClass().getName().substring(BASE_PACKAGE.length()); + styleableName = styleableName.replace('.', '_'); + try { + /** + * Use reflection to safely run for the existence of styleable defs for Plot + * and it's derivatives. This safety run is necessary to avoid runtime exceptions + * in apps that don't include Androidplot as a .aar and won't have access to + * the resources defined in the core library. + */ + styleableFieldInR = styleableClass.getField(styleableName); + } catch (NoSuchFieldException e) { + Log.d(TAG, "Styleable definition not found for: " + styleableName); + } + if (styleableFieldInR != null) { + try { + int[] resIds = (int[]) styleableFieldInR.get(null); + typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0); + } catch (IllegalAccessException e) { + // nothing to do + } finally { + if (typedAttrs != null) { + // apply derived class' attrs: + processAttrs(typedAttrs); + typedAttrs.recycle(); + } + } + } + + try { + styleableFieldInR = styleableClass.getField(Plot.class.getSimpleName()); + if (styleableFieldInR != null) { + int[] resIds = (int[]) styleableFieldInR.get(null); + typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0); + } + } catch (IllegalAccessException e) { + // nothing to do + } catch (NoSuchFieldException e) { + Log.d(TAG, "Styleable definition not found for: " + Plot.class.getSimpleName()); + } finally { + if (typedAttrs != null) { + // apply base attrs: + processBaseAttrs(typedAttrs); + typedAttrs.recycle(); + } + } + + // apply "configurator" attrs: (overrides any previously applied styleable attrs) + // filter out androidplot prefixed attrs: + HashMap attrHash = new HashMap<>(); + for (int i = 0; i < attrs.getAttributeCount(); i++) { + String attrName = attrs.getAttributeName(i); + + // case insensitive run to see if this attr begins with our prefix: + if (attrName != null && attrName.toUpperCase().startsWith(XML_ATTR_PREFIX.toUpperCase())) { + attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i)); + } + } + try { + Fig.configure(getContext(), this, attrHash); + } catch (FigException e) { + throw new RuntimeException(e); + } + } + } + + public RenderMode getRenderMode() { + return renderMode; + } + + public synchronized boolean addListener(PlotListener listener) { + return !listeners.contains(listener) && listeners.add(listener); + } + + public synchronized boolean removeListener(PlotListener listener) { + return listeners.remove(listener); + } + + protected ArrayList getListeners() { + return listeners; + } + + protected void notifyListenersBeforeDraw(Canvas canvas) { + for (PlotListener listener : listeners) { + listener.onBeforeDraw(this, canvas); + } + } + + protected void notifyListenersAfterDraw(Canvas canvas) { + for (PlotListener listener : listeners) { + listener.onAfterDraw(this, canvas); + } + } + + /** + * Convenience method to add a multiple series at once using the same formatter. + * If a problem is encountered, the method immediately returns false and the plot + * will contain whatever series were added before the failure. + * @param formatter + * @param series + * @return True if all series were successfully added, false otherwise. + * @since 0.9.7 + */ + public synchronized boolean addSeries(FormatterType formatter, SeriesType... series) { + for(SeriesType s : series) { + if(!addSeries(s, formatter)) { + return false; + } + } + return true; + } + + /** + * Add a new Series to the Plot. + * @param series + * @param formatter + * @return True if the series was added or false if the series / formatter pair already exists in the registry. + */ + public synchronized boolean addSeries(SeriesType series, FormatterType formatter) { + final boolean result = getRegistry().add(series, formatter); + attachSeries(series, formatter); + return result; + } + + protected void attachSeries(SeriesType series, FormatterType formatter) { + + Class rendererClass = formatter.getRendererClass(); + + // initialize the Renderer if necessary: + if(!getRenderers().containsKey(rendererClass)) { + getRenderers().put(rendererClass, (RendererType) formatter.getRendererInstance(this)); + } + + // if this series implements PlotListener, add it as a listener: + if(series instanceof PlotListener) { + addListener((PlotListener)series); + } + } + + /** + * + * @param series + * @param rendererClass + * @return The {@link SeriesBundle} that matches the series and rendererClass params, or null if one is not found. + */ + protected SeriesBundle getSeries(SeriesType series, Class 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/Region.java b/androidplot-core/src/main/java/com/androidplot/Region.java index 2ace04a7..f22f9115 100644 --- a/androidplot-core/src/main/java/com/androidplot/Region.java +++ b/androidplot-core/src/main/java/com/androidplot/Region.java @@ -17,7 +17,7 @@ package com.androidplot; -import com.androidplot.util.*; +import com.androidplot.util.FastNumber; /** * A one dimensional region represented by a starting and ending value. @@ -71,7 +71,7 @@ public Number length() { Number l = getMax() == null || getMin() == null ? null : getMax().doubleValue() - getMin().doubleValue(); if(l != null) { - cachedLength = new FastNumber(l); + cachedLength = FastNumber.orNull(l); } } return cachedLength; @@ -215,8 +215,8 @@ public void setMin(Number min) { } else { this.min = null; } - } else { - this.min = new FastNumber(min); + } else if (this.min == null || !this.min.equals(min)) { + this.min = FastNumber.orNull(min); } } @@ -237,8 +237,8 @@ public void setMax(Number max) { } else { this.max = null; } - } else { - this.max = new FastNumber(max); + } else if (this.max == null || !this.max.equals(max)) { + this.max = FastNumber.orNull(max); } } @@ -249,4 +249,20 @@ public void setMax(Number max) { public boolean isDefined() { return min != null && max != null; } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Region{"); + sb.append("min=").append(min); + sb.append(", max=").append(max); + sb.append(", cachedLength=").append(cachedLength); + sb.append(", defaults="); + if (defaults != this) { + sb.append(defaults); + } else { + sb.append("this"); + } + sb.append('}'); + return sb.toString(); + } } diff --git a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java index 78f3d805..7a7b3023 100644 --- a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java +++ b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java @@ -19,7 +19,7 @@ import com.androidplot.ui.Formatter; import com.androidplot.ui.SeriesBundle; -import java.io.*; +import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -38,7 +38,7 @@ public List getSeriesAndFormatterList() { return registry; } public List getSeriesList() { - List result = new ArrayList<>(); + List result = new ArrayList<>(registry.size()); for(SeriesBundle sfPair : registry) { result.add(sfPair.getSeries()); } diff --git a/androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java b/androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java deleted file mode 100644 index 62a6e6d9..00000000 --- a/androidplot-core/src/main/java/com/androidplot/exception/PlotRenderException.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot.exception; - -public class PlotRenderException extends Exception { - public PlotRenderException(String message) { - super(message); - } -} diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java index 615efaaa..35e5e416 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java @@ -37,15 +37,18 @@ public class PieChart extends Plot { + + private PieChart pieChart; + + public PieLegendWidget(LayoutManager layoutManager, PieChart pieChart, + Size widgetSize, + TableModel tableModel, + Size iconSize) { + super(tableModel, layoutManager, widgetSize, iconSize); + this.pieChart = pieChart; + } + + @Override + protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull PieLegendItem item) { + canvas.drawRect(iconRect, item.formatter.getFillPaint()); + } + + @Override + protected List getLegendItems() { + final List legendItems = new ArrayList<>(); + for(SeriesBundle item : pieChart.getRegistry().getLegendEnabledItems()) { + legendItems.add(new PieLegendItem(item.getSeries(), item.getFormatter())); + } + return legendItems; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java index 8b663a82..db8921dc 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java @@ -18,7 +18,6 @@ import android.graphics.*; -import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.SeriesRenderer; import com.androidplot.ui.RenderStack; @@ -30,11 +29,14 @@ */ public class PieRenderer extends SeriesRenderer { + private static final float FULL_PIE_DEGS = 360f; + private static final float HALF_PIE_DEGS = 180f; + // starting angle to use when drawing the first radial line of the first segment. private float startDegs = 0; // number of degrees to extend from startDegs; can be used to "shape" the pie chart. - private float extentDegs = 360; + private float extentDegs = FULL_PIE_DEGS; // TODO: express donut in units other than px. private float donutSize = 0.5f; @@ -55,7 +57,7 @@ public float getRadius(RectF rect) { @Override public void onRender(Canvas canvas, RectF plotArea, Segment series, SegmentFormatter formatter, - RenderStack stack) throws PlotRenderException { + RenderStack stack) { // This renderer renders all series in one shot, so exclude any remaining series // from causing subsequent invocations of onRender: @@ -167,7 +169,7 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm 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); + canvas.save(); Path chart = new Path(); chart.addCircle(cx, cy, outerRad, Path.Direction.CW); Path inside = new Path(); @@ -240,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); @@ -292,11 +294,10 @@ public Segment getContainingSegment(PointF point) { float dx = point.x - origin.x; float dy = point.y - origin.y; double theta = Math.atan2(dy, dx); - double angle = (theta * (180f / Math.PI)); + double angle = (theta * (HALF_PIE_DEGS / Math.PI)); if (angle < 0) { - // convert angle to 0-360 range with 0 being in the - // traditional "east" orientation: - angle += 360f; + // bring into 0-360 range + angle += FULL_PIE_DEGS; } // find the segment whose starting and ending angle (degs) contains @@ -310,10 +311,16 @@ public Segment getContainingSegment(PointF point) { float lastOffset = offset; float sweep = (float) (scale * (values[i]) * extentDegs); offset += sweep; - offset = offset % 360; + offset = offset % FULL_PIE_DEGS; final double dist = signedDistance(offset, angle); - if(dist > 0 && dist <= signedDistance(offset, lastOffset)) { + 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++; @@ -328,10 +335,10 @@ public Segment getContainingSegment(PointF point) { * @return */ protected static float degsToScreenDegs(float degs) { - degs = degs % 360; + degs = degs % FULL_PIE_DEGS; if (degs > 0) { - return 360 - degs; + return FULL_PIE_DEGS - degs; } else { return degs; } @@ -344,12 +351,12 @@ protected static float degsToScreenDegs(float degs) { * @return */ protected static double signedDistance(double angle1, double angle2) { - double d = Math.abs(angle1 - angle2) % 360; - double r = d > 180 ? 360 - d : d; + 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 <= 180) - || (angle1 - angle2 <= -180 && angle1 - angle2 >= -360) ? 1 : -1; + 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; } @@ -359,7 +366,7 @@ protected static double signedDistance(double angle1, double angle2) { * @param degs */ protected static void validateInputDegs(float degs) { - if(degs < 0 || degs > 360) { + if(degs < 0 || degs > FULL_PIE_DEGS) { throw new IllegalArgumentException("Degrees values must be between 0.0 and 360."); } } diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieWidget.java b/androidplot-core/src/main/java/com/androidplot/pie/PieWidget.java index 38c23b27..1b7b2a80 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieWidget.java @@ -17,7 +17,6 @@ package com.androidplot.pie; import android.graphics.*; -import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.LayoutManager; import com.androidplot.ui.Size; import com.androidplot.ui.widget.Widget; @@ -38,7 +37,7 @@ public PieWidget(LayoutManager layoutManager, PieChart pieChart, Size metrics) { } @Override - protected void doOnDraw(Canvas canvas, RectF widgetRect) throws PlotRenderException { + protected void doOnDraw(Canvas canvas, RectF widgetRect) { renderStack.sync(); for(RenderStack.StackElement thisElement : renderStack.getElements()) { if(thisElement.isEnabled()) { diff --git a/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java b/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java index c7c7f066..ff973b9d 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java @@ -149,10 +149,10 @@ public TableModelIterator(DynamicTableModel dynamicTableModel, RectF tableRect, calculatedRows = dynamicTableModel.getNumRows(); // round up: - calculatedColumns = new Float((totalElements / (float) calculatedRows) + 0.5).intValue(); + calculatedColumns = Float.valueOf((totalElements / (float) calculatedRows) + 0.5f).intValue(); } else if(dynamicTableModel.getNumRows() == 0 && dynamicTableModel.getNumColumns() >= 1) { calculatedColumns = dynamicTableModel.getNumColumns(); - calculatedRows = new Float((totalElements / (float) calculatedColumns) + 0.5).intValue(); + calculatedRows = Float.valueOf((totalElements / (float) calculatedColumns) + 0.5f).intValue(); // unlimited rows and columns (impossible) so default a single row with n columns: }else if(dynamicTableModel.getNumColumns() == 0 && dynamicTableModel.getNumRows() == 0) { calculatedRows = 1; diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java index 8f2a354b..d920ab36 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java @@ -48,7 +48,11 @@ public Formatter(Context ctx, int xmlCfgId) { } public void configure(Context ctx, int xmlCfgId) { - Fig.configure(ctx, this, xmlCfgId); + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } } /** diff --git a/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java b/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java index d9107442..45f79a43 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java @@ -16,10 +16,15 @@ package com.androidplot.ui; -import android.graphics.*; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.Region; import android.view.MotionEvent; import android.view.View; -import com.androidplot.exception.PlotRenderException; + import com.androidplot.ui.widget.Widget; import com.androidplot.util.DisplayDimensions; import com.androidplot.util.LinkedLayerList; @@ -77,7 +82,7 @@ public void setMarkupEnabled(boolean enabled) { setDrawOutlineShadowsEnabled(enabled); } - public void draw(Canvas canvas) throws PlotRenderException { + public void draw(Canvas canvas) { if(isDrawMarginsEnabled()) { drawSpacing(canvas, displayDims.canvasRect, displayDims.marginatedRect, marginPaint); } @@ -86,11 +91,11 @@ public void draw(Canvas canvas) throws PlotRenderException { } for (Widget widget : elements()) { 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(); @@ -132,9 +137,9 @@ public void draw(Canvas canvas) throws PlotRenderException { } } - private void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) { + private static void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) { try { - canvas.save(Canvas.ALL_SAVE_FLAG); + canvas.save(); canvas.clipRect(inner, Region.Op.DIFFERENCE); canvas.drawRect(outer, paint); } finally { diff --git a/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java b/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java index 3381f20f..377e9e04 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java @@ -16,6 +16,8 @@ package com.androidplot.ui; +import androidx.annotation.NonNull; + public class PositionMetrics implements Comparable { private HorizontalPosition horizontalPosition; @@ -47,7 +49,7 @@ public void setAnchor(Anchor anchor) { } @Override - public int compareTo(PositionMetrics o) { + public int compareTo(@NonNull PositionMetrics o) { if(this.layerDepth < o.layerDepth) { return -1; } else if(this.layerDepth == o.layerDepth) { diff --git a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java index d9b9d812..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 { 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 882bdcfc..bfedeeb8 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java @@ -20,7 +20,6 @@ import android.graphics.RectF; import android.graphics.Region; import com.androidplot.Series; -import com.androidplot.exception.PlotRenderException; import com.androidplot.Plot; import java.util.ArrayList; @@ -52,10 +51,9 @@ public SeriesFormatterType getFormatter(SeriesType series) { * @param canvas * @param plotArea * @param sfPair The series / formatter pair to be rendered - * @throws PlotRenderException */ public void render(Canvas canvas, RectF plotArea, SeriesBundle sfPair, RenderStack stack) throws PlotRenderException { + SeriesFormatterType> sfPair, RenderStack stack) { onRender(canvas, plotArea, sfPair.getSeries(), sfPair.getFormatter(), stack); } @@ -67,10 +65,9 @@ public void render(Canvas canvas, RectF plotArea, SeriesBundle + */ +public abstract class LegendWidget extends Widget { + + private static final float DEFAULT_TEXT_SIZE_DP = 20; + + private TableModel tableModel; + private Size iconSize; + + private Paint textPaint; + private Paint iconBackgroundPaint; + private Paint iconBorderPaint; + + private boolean drawIconBackgroundEnabled = true; + private boolean drawIconBorderEnabled = true; + + private Comparator legendItemComparator; + + { + textPaint = new Paint(); + textPaint.setColor(Color.LTGRAY); + textPaint.setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_DP)); + textPaint.setAntiAlias(true); + + iconBackgroundPaint = new Paint(); + iconBackgroundPaint.setColor(Color.BLACK); + + iconBorderPaint = new Paint(); + iconBorderPaint.setColor(Color.TRANSPARENT); + iconBorderPaint.setStyle(Paint.Style.STROKE); + } + + + public LegendWidget(@NonNull TableModel tableModel, @NonNull LayoutManager layoutManager, + @NonNull Size size, @NonNull Size iconSize) { + super(layoutManager, size); + setTableModel(tableModel); + this.iconSize = iconSize; + } + + @Override + protected void doOnDraw(Canvas canvas, RectF widgetRect) { + final List items = getLegendItems(); + if(legendItemComparator != null) { + Collections.sort(items, legendItemComparator); + } + final Iterator cellRectIterator = tableModel.getIterator(widgetRect, items.size()); + for(ItemT item : items) { + final RectF cellRect = cellRectIterator.next(); + final RectF iconRect = getIconRect(cellRect); + beginDrawingCell(canvas, iconRect); + drawItem(canvas, iconRect, item); + finishDrawingCell(canvas, cellRect, iconRect, item); + } + } + + protected void drawItem(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item) { + drawIcon(canvas, iconRect, item); + } + + /** + * Draw the icon representing the legend item + * @param canvas + * @param iconRect The space to be occupied by the icon. + * @param item + */ + protected abstract void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item); + + /** + * + * @return The list of legend items to be drawn. This is used to calculate table dimensions etc. + */ + protected abstract List getLegendItems(); + + private RectF getIconRect(RectF cellRect) { + float cellRectCenterY = cellRect.top + (cellRect.height()/2); + RectF iconRect = iconSize.getRectF(cellRect); + + // center the icon rect vertically + float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2); + iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY); + return iconRect; + } + + /** + * Done at the start of rendering a new cell. Whatever is drawn here will be beneath the rest + * of the cell content; typically used to draw backgrounds. + * @param canvas + * @param iconRect + */ + protected void beginDrawingCell(Canvas canvas, RectF iconRect) { + + if(drawIconBackgroundEnabled && iconBackgroundPaint != null) { + canvas.drawRect(iconRect, iconBackgroundPaint); + } + } + + /** + * Done at the end of rendering a new cell. Whatever is drawn here will be on top of + * the rest of the cell content; typically used to draw borders and text. + * @param canvas + * @param cellRect + * @param iconRect + * @param legendItem + */ + protected void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, LegendItem legendItem) { + + if(drawIconBorderEnabled && iconBorderPaint != null) { + canvas.drawRect(iconRect, iconBorderPaint); + } + + float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2); + + if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) { + canvas.drawText(legendItem.getTitle(), iconRect.left - 2, centeredTextOriginY, textPaint); + } else { + canvas.drawText(legendItem.getTitle(), iconRect.right + 2, centeredTextOriginY, textPaint); + } + } + + protected static float getRectCenterY(RectF cellRect) { + return cellRect.top + (cellRect.height()/2); + } + + public synchronized void setTableModel(TableModel tableModel) { + this.tableModel = tableModel; + } + + public Paint getTextPaint() { + return textPaint; + } + + public void setTextPaint(Paint textPaint) { + this.textPaint = textPaint; + } + + public boolean isDrawIconBackgroundEnabled() { + return drawIconBackgroundEnabled; + } + + public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) { + this.drawIconBackgroundEnabled = drawIconBackgroundEnabled; + } + + public boolean isDrawIconBorderEnabled() { + return drawIconBorderEnabled; + } + + public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) { + this.drawIconBorderEnabled = drawIconBorderEnabled; + } + + public Size getIconSize() { + return iconSize; + } + + public void setIconSize(Size iconSize) { + this.iconSize = iconSize; + } + + public Comparator getLegendItemComparator() { + return legendItemComparator; + } + + /** + * Set a scheme for sorting the display order or legend items. By default no sorting is applied + * and {@link com.androidplot.Series} items typically appear in the order which the series was + * added to the {@link com.androidplot.Plot}. + * @param legendItemComparator + */ + public void setLegendItemComparator(Comparator legendItemComparator) { + this.legendItemComparator = legendItemComparator; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java index 9f47aea9..06bf54ff 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java @@ -21,13 +21,9 @@ import com.androidplot.util.FontUtils; public class TextLabelWidget extends Widget { - private static final String TAG = TextLabelWidget.class.getName(); - private String text; private Paint labelPaint; - private TextOrientation orientation; - private boolean autoPackEnabled = true; { @@ -67,16 +63,12 @@ public void onPostInit() { } } - //protected abstract String getText(); - /** * Sets the dimensions of the widget to exactly contain the text contents */ public void pack() { - //Log.d(TAG, "Packing..."); Rect size = FontUtils.getStringDimensions(text, getLabelPaint()); if(size == null) { - //Log.w(TAG, "Attempt to pack empty text."); return; } switch(orientation) { @@ -103,16 +95,13 @@ public void doOnDraw(Canvas canvas, RectF widgetRect) { if(text == null || text.length() == 0) { return; } - //FontUtils.getStringDimensions(text, labelPaint); + float vOffset = labelPaint.getFontMetrics().descent; PointF start = getAnchorCoordinates(widgetRect, Anchor.CENTER); - // BEGIN ROTATION CALCULATION - //int canvasState = canvas.save(Canvas.ALL_SAVE_FLAG); - try { - canvas.save(Canvas.ALL_SAVE_FLAG); + canvas.save(); canvas.translate(start.x, start.y); switch (orientation) { case HORIZONTAL: @@ -129,11 +118,8 @@ public void doOnDraw(Canvas canvas, RectF widgetRect) { } canvas.drawText(text, 0, vOffset, labelPaint); } finally { - //canvas.restoreToCount(canvasState); canvas.restore(); } - - // END ROTATION CALCULATION } public Paint getLabelPaint() { @@ -173,7 +159,6 @@ public void setAutoPackEnabled(boolean autoPackEnabled) { } public void setText(String text) { - //Log.d(TAG, "Setting textLabel to: " + text); this.text = text; if(autoPackEnabled) { pack(); diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java index 6009ce36..6d21767b 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java @@ -17,7 +17,9 @@ package com.androidplot.ui.widget; import android.graphics.*; -import com.androidplot.exception.PlotRenderException; +import androidx.annotation.Nullable; +import androidx.annotation.NonNull; + import com.androidplot.ui.*; import com.androidplot.util.DisplayDimensions; import com.androidplot.ui.HorizontalPositioning; @@ -42,6 +44,7 @@ public abstract class Widget implements BoxModelable, Resizable { private LayoutManager layoutManager; private Rotation rotation = Rotation.NONE; + private RectF lastWidgetRect = null; public enum Rotation { NINETY_DEGREES, @@ -77,6 +80,7 @@ public void setAnchor(Anchor anchor) { /** * Same as {@link #position(float, HorizontalPositioning, float, VerticalPositioning, Anchor)} * but with the anchor parameter defaulted to the upper left corner. + * * @param x * @param horizontalPositioning * @param y @@ -87,11 +91,11 @@ public void position(float x, HorizontalPositioning horizontalPositioning, float } /** - * @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1. + * @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1. * @param horizontalPositioning LayoutType to use when orienting this element's X-Coordinate. - * @param y Y_VALS_ONLY-Coordinate of the top-left corner of element. When using RELATIVE, must be a value between 0 and 1. - * @param verticalPositioning LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate. - * @param anchor The point of reference used by this positioning call. + * @param y Y_VALS_ONLY-Coordinate of the top-left corner of element. When using RELATIVE, must be a value between 0 and 1. + * @param verticalPositioning LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate. + * @param anchor The point of reference used by this positioning call. */ public void position(float x, HorizontalPositioning horizontalPositioning, float y, VerticalPositioning verticalPositioning, Anchor anchor) { @@ -271,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. @@ -279,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, @@ -296,69 +300,90 @@ public synchronized void layout(final DisplayDimensions plotDimensions) { refreshLayout(); } - public PointF getElementCoordinates(float height, float width, RectF viewRect, PositionMetrics metrics) { - float x = metrics.getXPositionMetric().getPixelValue(viewRect.width()) + viewRect.left; - float y = metrics.getYPositionMetric().getPixelValue(viewRect.height()) + viewRect.top; - PointF point = new PointF(x, y); - return PixelUtils.sub(point, getAnchorOffset(width, height, metrics.getAnchor())); - } + + public static PointF calculateCoordinates(float height, float width, RectF viewRect, PositionMetrics metrics) { + float x = metrics.getXPositionMetric().getPixelValue(viewRect.width()) + viewRect.left; + float y = metrics.getYPositionMetric().getPixelValue(viewRect.height()) + viewRect.top; + PointF point = new PointF(x, y); + return PixelUtils.sub(point, getAnchorOffset(width, height, metrics.getAnchor())); + } public static PointF getAnchorOffset(float width, float height, Anchor anchor) { - PointF point = new PointF(); - switch (anchor) { - case LEFT_TOP: - break; - case LEFT_MIDDLE: - point.set(0, height / 2); - break; - case LEFT_BOTTOM: - point.set(0, height); - break; - case RIGHT_TOP: - point.set(width, 0); - break; - case RIGHT_BOTTOM: - point.set(width, height); - break; - case RIGHT_MIDDLE: - point.set(width, height / 2); - break; - case TOP_MIDDLE: - point.set(width / 2, 0); - break; - case BOTTOM_MIDDLE: - point.set(width / 2, height); - break; - case CENTER: - point.set(width / 2, height / 2); - break; - default: - throw new IllegalArgumentException("Unsupported anchor location: " + anchor); - } - return point; + PointF point = new PointF(); + switch (anchor) { + case LEFT_TOP: + break; + case LEFT_MIDDLE: + point.set(0, height / 2); + break; + case LEFT_BOTTOM: + point.set(0, height); + break; + case RIGHT_TOP: + point.set(width, 0); + break; + case RIGHT_BOTTOM: + point.set(width, height); + break; + case RIGHT_MIDDLE: + point.set(width, height / 2); + break; + case TOP_MIDDLE: + point.set(width / 2, 0); + break; + case BOTTOM_MIDDLE: + point.set(width / 2, height); + break; + case CENTER: + point.set(width / 2, height / 2); + break; + default: + throw new IllegalArgumentException("Unsupported anchor location: " + anchor); } + return point; + } public static PointF getAnchorCoordinates(RectF widgetRect, Anchor anchor) { - return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top), - getAnchorOffset(widgetRect.width(), widgetRect.height(), anchor)); - } + return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top), + getAnchorOffset(widgetRect.width(), widgetRect.height(), anchor)); + } + + public static PointF getAnchorCoordinates(float x, float y, float width, float height, Anchor anchor) { + return getAnchorCoordinates(new RectF(x, y, x + width, y + height), anchor); + } - public static PointF getAnchorCoordinates(float x, float y, float width, float height, Anchor anchor) { - return getAnchorCoordinates(new RectF(x, y, x+width, y+height), anchor); + private void checkSize(@NonNull RectF widgetRect) { + if (lastWidgetRect == null || !lastWidgetRect.equals(widgetRect)) { + onResize(lastWidgetRect, widgetRect); } + lastWidgetRect = widgetRect; + } + + /** + * 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) throws PlotRenderException { + public void draw(Canvas canvas) { if (isVisible()) { if (backgroundPaint != null) { drawBackground(canvas, widgetDimensions.canvasRect); } canvas.save(); - final RectF paddedRect = applyRotation(canvas, widgetDimensions.paddedRect); - doOnDraw(canvas, paddedRect); + final RectF widgetRect = applyRotation(canvas, widgetDimensions.paddedRect); + checkSize(widgetRect); + doOnDraw(canvas, widgetRect); canvas.restore(); if (borderPaint != null) { - drawBorder(canvas, paddedRect); + drawBorder(canvas, widgetRect); } } } @@ -395,7 +420,7 @@ protected RectF applyRotation(Canvas canvas, RectF rect) { throw new UnsupportedOperationException("Not yet implemented."); } - if(rotation != Rotation.NONE) { + if (rotation != Rotation.NONE) { canvas.rotate(rotationDegs, cx, cy); } return rect; @@ -413,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; 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 b34e7d47..853ae25a 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java @@ -237,7 +237,7 @@ private static Number getIntFloatDimenValue(TypedArray attrs, int valueAttr, Num } else if (valueType == TypedValue.TYPE_FLOAT) { result = attrs.getFloat(valueAttr, defaultValue.floatValue()); } else { - throw new IllegalArgumentException("Invalid value type - must be float or dimension."); + throw new IllegalArgumentException("Invalid value type - must be int, float or dimension."); } } return result; diff --git a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java index 049431b3..4fa11d8a 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java +++ b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java @@ -1,11 +1,14 @@ package com.androidplot.util; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + /** * An extension of {@link Number} optimized for speed at the cost of memory. */ public class FastNumber extends Number { - private Number number; + @NonNull private final Number number; private boolean hasDoublePrimitive; private boolean hasFloatPrimitive; private boolean hasIntPrimitive; @@ -14,7 +17,25 @@ public class FastNumber extends Number { private float floatPrimitive; private int intPrimitive; - public FastNumber(Number number) { + /** + * Safe-instantiator of FastNumber; returns a null result if the input Number is also null. + * @param number + * @return + */ + public static FastNumber orNull(@NonNull Number number) { + if(number == null) { + return null; + } else { + return new FastNumber(number); + } + } + + private FastNumber(@NonNull Number number) { + + //noinspection ConstantConditions //in case someone ignores the @NonNull annotation + if (number == null) { + throw new IllegalArgumentException("number parameter cannot be null"); + } // avoid nested instances of FastNumber : if(number instanceof FastNumber) { @@ -63,4 +84,36 @@ public double doubleValue() { } return doublePrimitive; } + + /** + * To be equal, two instances must both be instances of {@link FastNumber}. The inner {@link + * #number} field must also be a common type. Numbers which are mathematically equal are not + * necessarily equal. This keeps with the java implementation of common Number classes where for + * instance {@code new Integer(0).equals(new Double(0))} returns {@code false} + */ + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FastNumber that = (FastNumber) o; + + return number.equals(that.number); + + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @NonNull + @Override + public String toString() { + return String.valueOf(doubleValue()); + } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java b/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java index 61e25c97..f11737a6 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java +++ b/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java @@ -153,4 +153,28 @@ public synchronized boolean remove(KeyType key) { return false; } } + + public ValueType getTop() { + return hash.get(zlist.getLast()); + } + + public ValueType getBottom() { + return hash.get(zlist.getFirst()); + } + + public ValueType getAbove(KeyType key) { + final int index = zlist.indexOf(key); + if(index >= 0 && index < size() - 1) { + return hash.get(zlist.get(index + 1)); + } + return null; + } + + public ValueType getBeneath(KeyType key) { + final int index = zlist.indexOf(key); + if(index > 0) { + return hash.get(zlist.get(index - 1)); + } + return null; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java b/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java index 1d597b9e..b56337ed 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java +++ b/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java @@ -39,6 +39,7 @@ public class PlotStatistics implements PlotListener { long latencySamples = 0; long latencySum = 0; String annotationString = ""; + private boolean annotatePlotEnabled; private Paint paint; { @@ -49,11 +50,6 @@ public class PlotStatistics implements PlotListener { resetCounters(); } - - private boolean annotatePlotEnabled; - - - public PlotStatistics(long updateDelayMs, boolean annotatePlotEnabled) { this.updateDelayMs = updateDelayMs; this.annotatePlotEnabled = annotatePlotEnabled; @@ -109,4 +105,8 @@ public void onAfterDraw(Plot source, Canvas canvas) { latencySamples++; annotatePlot(source, canvas); } + + public void setEnabled(boolean isEnabled) { + this.annotatePlotEnabled = isEnabled; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java index 00174027..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,11 +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; /** @@ -52,12 +53,12 @@ public class Redrawer implements Runnable { * @param startImmediately If true, invokes run() immediately after construction. */ public Redrawer(List plots, float maxRefreshRate, boolean startImmediately) { - this.plots = new ArrayList<>(); + this.plots = new ArrayList<>(plots.size()); for(Plot plot : plots) { this.plots.add(new WeakReference<>(plot)); } setMaxRefreshRate(maxRefreshRate); - thread = new Thread(this); + thread = new Thread(this, "Androidplot Redrawer"); thread.start(); if(startImmediately) { start(); @@ -65,7 +66,7 @@ public Redrawer(List plots, float maxRefreshRate, boolean startImmediately } public Redrawer(Plot plot, float maxRefreshRate, boolean startImmediately) { - this(Arrays.asList(new Plot[]{plot}), maxRefreshRate, startImmediately); + this(Collections.singletonList(plot), maxRefreshRate, startImmediately); } /** @@ -121,7 +122,7 @@ public void run() { } } } - } catch(InterruptedException e) { + } catch (InterruptedException ignored) { } finally { Log.d(TAG, "Redrawer thread exited."); diff --git a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java index 2b269757..b9526a24 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java @@ -16,10 +16,14 @@ package com.androidplot.util; -import com.androidplot.*; -import com.androidplot.xy.*; +import com.androidplot.Region; +import com.androidplot.xy.FastXYSeries; +import com.androidplot.xy.OrderedXYSeries; +import com.androidplot.xy.RectRegion; +import com.androidplot.xy.XYConstraints; +import com.androidplot.xy.XYSeries; -import java.util.*; +import java.util.List; /** * Utilities for dealing with Series data. @@ -61,7 +65,6 @@ public static Region minMaxY(XYSeries... seriesList) { * @since 0.9.7 */ public static RectRegion minMax(XYConstraints constraints, List seriesList) { - // TODO: this is inefficient...clean it up! return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()])); } @@ -82,28 +85,24 @@ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArr for (XYSeries series : seriesArray) { // if this is an advanced xy series then minMax have already been calculated for us: - if(series instanceof FastXYSeries) { + if (series instanceof FastXYSeries) { final RectRegion b = ((FastXYSeries) series).minMax(); - if(constraints == null) { + if(b == null) { + //this series doesn't currently have min or max region (might be empty) + continue; + } + if(constraints == null || constraints.contains(b)) { bounds.union(b); - } else { - if(constraints.contains(b.getMinX(), b.getMinY())) { - bounds.union(b.getMinX(), b.getMinY()); - } - if(constraints.contains(b.getMaxX(), b.getMaxY())) { - bounds.union(b.getMaxX(), b.getMaxY()); - } + continue; } + } + for (int i = 0; i < series.size(); i++) { + final Number xi = series.getX(i); + final Number yi = series.getY(i); - } else if (series.size() > 0) { - for (int i = 0; i < series.size(); i++) { - final Number xi = series.getX(i); - final Number yi = series.getY(i); - - // if constraints have been set, make sure this xy coordinate exists within them: - if (constraints == null || constraints.contains(xi, yi)) { - bounds.union(xi, yi); - } + // if constraints have been set, make sure this xy coordinate exists within them: + if (constraints == null || constraints.contains(xi, yi)) { + bounds.union(xi, yi); } } } @@ -259,26 +258,6 @@ public static Region minMax(List... lists) { return minMax(new Region(), lists); } - public static void main(String[] args) { - - // seed the list: - ArrayList values = new ArrayList<>(); - for (int i = 0; i < 1000000; i++) { - values.add(Math.random() * 100); - } - final int numIterations = 20; - long sumTime = 0; - for(int j = 0; j < numIterations; j++) { - final long startTime = System.currentTimeMillis(); - Region bounds = minMax(values); - final long thisIteration = System.currentTimeMillis() - startTime; - System.out.println("thisIteration took: " + thisIteration + "ms"); - sumTime += thisIteration; - } - - System.out.println("Benchmark avg:" + (sumTime / numIterations) + "ms."); - } - /** * Determine the XVal order of an XYSeries. If series does not implement {@link OrderedXYSeries} * then {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE} is assumed. @@ -287,6 +266,6 @@ public static void main(String[] args) { */ public static OrderedXYSeries.XOrder getXYOrder(XYSeries series) { return series instanceof OrderedXYSeries ? - ((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE; + ((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE; } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java index ee3e7e24..11b9e8e8 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java @@ -18,7 +18,6 @@ import android.content.*; import android.graphics.*; -import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesRenderer; @@ -41,7 +40,7 @@ public AdvancedLineAndPointRenderer(XYPlot plot) { } @Override - protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) throws PlotRenderException { + protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) { PointF thisPoint; PointF lastPoint = null; for (int i = 0; i < series.size(); i++) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java index c696bbf2..ced83885 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java @@ -51,7 +51,13 @@ public class BarRenderer extends GroupRender public enum BarOrientation { /** - * Bars are drawn "overlapping" one another, with taller bars being drawn behind + * Bars are drawn overlapping one another, in the order their respective series + * was added to the plot. + */ + IN_ORDER, + + /** + * 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) @@ -187,6 +193,7 @@ public void onRender(Canvas canvas, RectF plotArea, List bar, RectF rect) { // null yVals are skipped: if(bar.getY() == null) { @@ -366,6 +373,7 @@ public int compare(Bar bar1, Bar bar2) { } else { return Float.valueOf(bar1.yPix).compareTo(bar2.yPix); } + case IN_ORDER: case SIDE_BY_SIDE: case STACKED: default: diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java index b351358b..3a794b5c 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java @@ -16,11 +16,12 @@ package com.androidplot.xy; -import android.content.*; -import android.graphics.*; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Paint; -import com.androidplot.ui.*; -import com.androidplot.util.*; +import com.androidplot.ui.SeriesRenderer; +import com.androidplot.util.PixelUtils; /** * Format for drawing a value using {@link BubbleRenderer}. @@ -51,7 +52,7 @@ public class BubbleFormatter extends XYSeriesFormatter { setPointLabeler(new PointLabeler() { @Override public String getLabel(BubbleSeries series, int index) { - return series.getZ(index) + ""; + return String.valueOf(series.getZ(index)); } }); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java index 1e0ba9c5..dcebf6cb 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java @@ -3,7 +3,6 @@ import android.graphics.*; import com.androidplot.Region; -import com.androidplot.exception.*; import com.androidplot.ui.*; import com.androidplot.util.*; @@ -45,7 +44,7 @@ public BubbleRenderer(XYPlot plot) { @Override protected void onRender(Canvas canvas, RectF plotArea, BubbleSeries series, - FormatterType formatter, RenderStack stack) throws PlotRenderException { + FormatterType formatter, RenderStack stack) { Region magnitudeBounds = calculateBounds(); for(int i = 0; i < series.size(); i++) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java b/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java index 31e272f6..aea93ecd 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java @@ -20,7 +20,7 @@ import java.util.List; /** - * A primitive implementation of Catmull-Rom interpolation, based on the information found at: + * An implementation of Catmull-Rom interpolation, based on the information found at: * http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/19283471#19283471 */ public class CatmullRomInterpolator implements Interpolator { @@ -196,7 +196,7 @@ public List interpolate(XYSeries series, Params params) { * @return the list of coordinates that define the CatmullRom curve * between the points defined by index+1 and index+2. */ - public List interpolate(XYSeries series, int index, Params params) { + protected List interpolate(XYSeries series, int index, Params params) { List result = new ArrayList<>(); double[] x = new double[4]; double[] y = new double[4]; @@ -251,7 +251,7 @@ public List interpolate(XYSeries series, int index, Params params) { * position between p1 and p2 to interpolate the value. * @return */ - public static double interpolate(double[] p, double[] time, double t) { + protected static double interpolate(double[] p, double[] time, double t) { double L01 = p[0] * (time[1] - t) / (time[1] - time[0]) + p[1] * (t - time[0]) / (time[1] - time[0]); double L12 = p[1] * (time[2] - t) / (time[2] - time[1]) + p[2] * (t - time[1]) / (time[2] - time[1]); double L23 = p[2] * (time[3] - t) / (time[3] - time[2]) + p[3] * (t - time[2]) / (time[3] - time[2]); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java index e5aa7bc9..df070325 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java @@ -16,12 +16,17 @@ package com.androidplot.xy; -import android.graphics.*; -import com.androidplot.exception.PlotRenderException; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; +import androidx.annotation.NonNull; + import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesRenderer; -import java.util.*; +import java.util.ArrayList; +import java.util.List; /** * A faster implementation of of {@link LineAndPointRenderer}. For performance reasons, has these constraints: @@ -32,6 +37,11 @@ */ public class FastLineAndPointRenderer extends XYSeriesRenderer { + /** + * A line drawn by {@link Canvas#drawLines(float[], int, int, Paint)} must be defined by at + * least four points {@code x0, y0, x1, y1} + */ + private static final int MINIMUM_NUMBER_OF_POINTS_TO_DEFINE_A_LINE = 4; private float[] points; List segmentOffsets = new ArrayList<>(); List segmentLengths = new ArrayList<>(); @@ -40,7 +50,7 @@ public FastLineAndPointRenderer(XYPlot plot) { } @Override - protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) throws PlotRenderException { + protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) { segmentOffsets.clear(); segmentLengths.clear(); @@ -53,11 +63,11 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte int segmentLen = 0; boolean isLastPointNull = true; + PointF resultPoint = new PointF(); for (int i = 0, j = 0; i < series.size(); i++, j+=2) { Number y = series.getY(i); Number x = series.getX(i); - PointF thisPoint; if (y != null && x != null) { if(isLastPointNull) { segmentOffsets.add(j); @@ -65,9 +75,9 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte isLastPointNull = false; } - thisPoint = getPlot().getBounds().transformScreen(x, y, plotArea); - points[j] = thisPoint.x; - points[j+1] = thisPoint.y; + getPlot().getBounds().transformScreen(resultPoint, x, y, plotArea); + points[j] = resultPoint.x; + points[j + 1] = resultPoint.y; segmentLen+=2; // if this is the last point, account for it in segment lengths: @@ -90,10 +100,14 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte } } - protected void drawSegment(Canvas canvas, float[] points, int offset, int len, Formatter formatter) { + protected void drawSegment(@NonNull Canvas canvas, + @NonNull float[] points, + int offset, + int len, + Formatter formatter) { if(formatter.linePaint != null) { // draw lines: - if (len >= 4) { + if (len >= MINIMUM_NUMBER_OF_POINTS_TO_DEFINE_A_LINE) { // optimization to avoid using 2x storage space to represent the full path: if ((len & 2) != 0) { canvas.drawLines(points, offset, len - 2, formatter.linePaint); @@ -112,10 +126,16 @@ protected void drawSegment(Canvas canvas, float[] points, int offset, int len, F } @Override - protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) { + protected void doDrawLegendIcon(@NonNull Canvas canvas, + @NonNull RectF rect, + @NonNull Formatter formatter) { if(formatter.hasLinePaint()) { canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint()); } + + if(formatter.hasVertexPaint()) { + canvas.drawPoint(rect.centerX(), rect.centerY(), formatter.getVertexPaint()); + } } /** diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java index 4faa4d54..de5d534b 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java @@ -3,6 +3,10 @@ /** * An implementation of {@link XYSeries} that defines additional methods to speed up rendering by * giving a hint to the renderer about the min/max values contained in the series. + * + * Note that these hints can only be leveraged if the containing XYPlot's constraints completely + * contain the FastXYSeries min/max values. If this condition is not met then XYPlot falls back + * to manually determining the min/max values of the series that exist within the defined constraints. */ public interface FastXYSeries extends XYSeries { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java index 240f58dc..36ff371e 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java @@ -1,8 +1,12 @@ package com.androidplot.xy; -import com.androidplot.util.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import java.util.*; +import com.androidplot.util.FastNumber; + +import java.util.ArrayList; +import java.util.List; /** * An efficient implementation of {@link EditableXYSeries} intended for use cases where @@ -14,7 +18,10 @@ */ public class FixedSizeEditableXYSeries implements EditableXYSeries { + @NonNull private List xVals = new ArrayList<>(); + + @NonNull private List yVals = new ArrayList<>(); private String title; @@ -24,13 +31,13 @@ public FixedSizeEditableXYSeries(String title, int size) { } @Override - public void setX(Number x, int index) { - xVals.set(index, new FastNumber(x)); + public void setX(@Nullable Number x, int index) { + xVals.set(index, FastNumber.orNull(x)); } @Override - public void setY(Number y, int index) { - yVals.set(index, new FastNumber(y)); + public void setY(@Nullable Number y, int index) { + yVals.set(index, FastNumber.orNull(y)); } /** @@ -44,7 +51,7 @@ public void resize(int size) { resize(yVals, size); } - protected void resize(List list, int size) { + protected void resize(@NonNull List list, int size) { if (size > list.size()) { while (list.size() < size) { list.add(null); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java index d2fe4c73..503d624d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java @@ -19,7 +19,6 @@ import android.graphics.Canvas; import android.graphics.RectF; import android.util.Log; -import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesBundle; @@ -42,7 +41,7 @@ public GroupRenderer(XYPlot plot) { @Override protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, - FormatterType formatter, RenderStack stack) throws PlotRenderException { + FormatterType formatter, RenderStack stack) { // get all the data associated with this renderer: diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java index bbe145d4..f50d6186 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java @@ -25,7 +25,6 @@ import com.androidplot.Plot; import com.androidplot.PlotListener; import com.androidplot.Region; -import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; import com.androidplot.util.*; @@ -63,7 +62,7 @@ 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); } @@ -124,7 +123,6 @@ protected ArrayList getPointsCache(XYSeries series) { protected void cullPointsCache() { for(XYSeries series : pointsCaches.keySet()) { if(!getPlot().getRegistry().contains(series, LineAndPointFormatter.class)) { - //pointsCaches.put(series, null); pointsCaches.remove(series); } } @@ -246,16 +244,18 @@ protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, int 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 vertexPaint is available, draw vertex: + if (vertexPaint != null) { + canvas.drawPoint(p.x, p.y, vertexPaint); + } - // if textPaint and pointLabeler are available, draw point's text label: - if (pointLabeler != null) { - 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()); + } } } } @@ -308,7 +308,7 @@ protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF first RectF thisRegionRectF = thisRegionTransformed.asRectF(); if (thisRegionRectF != null) { try { - canvas.save(Canvas.ALL_SAVE_FLAG); + canvas.save(); canvas.clipPath(path); canvas.drawRect(thisRegionRectF, regionFormatter.getPaint()); } finally { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java index 659e38ea..820e4330 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java @@ -9,13 +9,13 @@ public interface OrderedXYSeries extends XYSeries { enum XOrder { /** * XVals are in strict ascending order such that: - * x(i) > x(i+1) == true + * x(i) < x(i+1) == true */ ASCENDING, /** * XVals are in strict descending order such that: - * x(i) < x(i+1) == true + * x(i) > x(i+1) == true */ DESCENDING, diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java index 00faa739..9cd80847 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java @@ -2,11 +2,13 @@ import android.graphics.RectF; import android.graphics.PointF; +import androidx.annotation.NonNull; import android.view.*; import com.androidplot.*; import com.androidplot.util.*; +import java.io.Serializable; import java.util.*; /** @@ -25,6 +27,8 @@ public class PanZoom implements View.OnTouchListener { private XYPlot plot; private Pan pan; private Zoom zoom; + + private ZoomLimit zoomLimit; private boolean isEnabled = true; private DragState dragState = DragState.NONE; @@ -33,6 +37,7 @@ public class PanZoom implements View.OnTouchListener { // rectangle created by the space between two fingers protected RectF fingersRect; private View.OnTouchListener delegate; + private State state = new State(); // Definition of the touch states protected enum DragState { @@ -77,25 +82,125 @@ public enum Zoom { SCALE } - protected PanZoom(XYPlot plot, Pan pan, Zoom zoom) { + /** + * Limits imposed on the zoom. + */ + public enum ZoomLimit { + /** + * Do not zoom outside the plots outer bounds, if they are defined. + */ + OUTER, + + /** + * Additionally to the outer bounds if plot.StepModel defines a value based increment + * make sure at least one tick is visible by not zooming in further. + */ + MIN_TICKS + } + + // TODO: consider making this immutable / threadsafe + public static class State implements Serializable { + private Number domainLowerBoundary; + private Number domainUpperBoundary; + private Number rangeLowerBoundary; + private Number rangeUpperBoundary; + private BoundaryMode domainBoundaryMode; + private BoundaryMode rangeBoundaryMode; + + public void setDomainBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) { + this.domainLowerBoundary = lowerBoundary; + this.domainUpperBoundary = upperBoundary; + this.domainBoundaryMode = mode; + } + + public void setRangeBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) { + this.rangeLowerBoundary = lowerBoundary; + this.rangeUpperBoundary = upperBoundary; + this.rangeBoundaryMode = mode; + } + + public void applyDomainBoundaries(@NonNull XYPlot plot) { + plot.setDomainBoundaries(domainLowerBoundary, domainUpperBoundary, domainBoundaryMode); + } + + public void applyRangeBoundaries(@NonNull XYPlot plot) { + plot.setRangeBoundaries(rangeLowerBoundary, rangeUpperBoundary, rangeBoundaryMode); + } + + public void apply(@NonNull XYPlot plot) { + applyDomainBoundaries(plot); + applyRangeBoundaries(plot); + } + } + + protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom) { this.plot = plot; this.pan = pan; this.zoom = zoom; + this.zoomLimit = ZoomLimit.OUTER; + } + + // additional constructor not to break api + protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom, ZoomLimit limit) { + this.plot = plot; + this.pan = pan; + this.zoom = zoom; + this.zoomLimit = limit; + } + + public State getState() { + return this.state; + } + + public void setState(@NonNull State state) { + this.state = state; + state.apply(plot); + } + + protected void adjustRangeBoundary(Number lower, Number upper, BoundaryMode mode) { + state.setRangeBoundaries(lower, upper, mode); + state.applyRangeBoundaries(plot); + } + + protected void adjustDomainBoundary(Number lower, Number upper, BoundaryMode mode) { + state.setDomainBoundaries(lower, upper, mode); + state.applyDomainBoundaries(plot); } /** * Convenience method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using * a default behavior of {@link Pan#BOTH} and {@link Zoom#SCALE}. - * Use {@link PanZoom#attach(XYPlot, Pan, Zoom)} for finer grain control of this behavior. + * Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior. * @param plot * @return */ - public static PanZoom attach(XYPlot plot) { + public static PanZoom attach(@NonNull XYPlot plot) { return attach(plot, Pan.BOTH, Zoom.SCALE); } - public static PanZoom attach(XYPlot plot, Pan pan, Zoom zoom) { - PanZoom pz = new PanZoom(plot, pan, zoom); + /** + * Old method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using + * the default behavior of {@link ZoomLimit#OUTER}. + * Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior. + * @param plot + * @param pan + * @param zoom + * @return + */ + public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom) { + return attach(plot,pan,zoom, ZoomLimit.OUTER); + } + + /** + * New method for enabling pan/zoom behavior on an instance of {@link XYPlot}. + * @param plot + * @param pan + * @param zoom + * @param limit + * @return + */ + public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom, @NonNull ZoomLimit limit) { + PanZoom pz = new PanZoom(plot, pan, zoom, limit); plot.setOnTouchListener(pz); return pz; } @@ -189,12 +294,12 @@ protected void pan(final MotionEvent motionEvent) { if (EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) { Region newBounds = new Region(); calculatePan(oldFirstFinger, newBounds, true); - plot.setDomainBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); + adjustDomainBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); } if (EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) { Region newBounds = new Region(); calculatePan(oldFirstFinger, newBounds, false); - plot.setRangeBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); + adjustRangeBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); } plot.redraw(); @@ -243,10 +348,9 @@ protected void calculatePan(final PointF oldFirstFinger, Region bounds, final bo } protected boolean isValidScale(float scale) { - if (Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) { - return false; - } - return true; + return !Float.isInfinite(scale) + && !Float.isNaN(scale) + && (!(scale > -0.001) || !(scale < 0.001)); } protected void zoom(final MotionEvent motionEvent) { @@ -301,14 +405,14 @@ protected void zoom(final MotionEvent motionEvent) { Zoom.STRETCH_BOTH, Zoom.SCALE).contains(zoom)) { calculateZoom(newRect, scaleX, true); - plot.setDomainBoundaries(newRect.left, newRect.right, BoundaryMode.FIXED); + adjustDomainBoundary(newRect.left, newRect.right, BoundaryMode.FIXED); } if (EnumSet.of( Zoom.STRETCH_VERTICAL, Zoom.STRETCH_BOTH, Zoom.SCALE).contains(zoom)) { calculateZoom(newRect, scaleY, false); - plot.setRangeBoundaries(newRect.top, newRect.bottom, BoundaryMode.FIXED); + adjustRangeBoundary(newRect.top, newRect.bottom, BoundaryMode.FIXED); } plot.redraw(); } @@ -332,10 +436,18 @@ protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) { } final float midPoint = calcMax - (span / 2.0f); - final float offset = span * scale / 2.0f; + float offset = span * scale / 2.0f; + final RectRegion limits = plot.getOuterLimits(); if (isHorizontal ) { - final RectRegion limits = plot.getOuterLimits(); + // zoom limited and increment by value StepMode? + if (zoomLimit == ZoomLimit.MIN_TICKS) { + // make sure we do not zoom in too far (there should be at least one grid line visible) + if (plot.getDomainStepValue() > (scale*span)) { + offset = (float)(plot.getDomainStepValue() / 2.0f); + } + } + newRect.left = midPoint - offset; newRect.right = midPoint + offset; if(limits.isFullyDefined()) { @@ -347,7 +459,14 @@ protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) { } } } else { - final RectRegion limits = plot.getOuterLimits(); + // zoom limited and increment by value StepMode? + if (zoomLimit == ZoomLimit.MIN_TICKS) { + // make sure we do not zoom in too far (there should be at least one grid line visible) + if (plot.getRangeStepValue() > (scale*span)) { + offset = (float)(plot.getRangeStepValue() / 2.0f); + } + } + newRect.top = midPoint - offset; newRect.bottom = midPoint + offset; if(limits.isFullyDefined()) { @@ -377,6 +496,14 @@ public void setZoom(Zoom zoom) { this.zoom = zoom; } + public ZoomLimit getZoomLimit() { + return zoomLimit; + } + + public void setZoomLimit(ZoomLimit zoomLimit) { + this.zoomLimit = zoomLimit; + } + public View.OnTouchListener getDelegate() { return delegate; } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java index c2182691..0bb8988d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java @@ -20,6 +20,7 @@ import android.graphics.RectF; import com.androidplot.Region; + import java.util.ArrayList; import java.util.List; @@ -244,7 +245,7 @@ public Number getHeight() { * @param y * @return */ - private Number distanceBetween(Number x, Number y) { + private static Number distanceBetween(Number x, Number y) { return Math.abs(x.doubleValue() - y.doubleValue()); } @@ -345,38 +346,12 @@ public boolean contains(Number x, Number y) { return getxRegion().contains(x) && getyRegion().contains(y); } - /** - * Checks to see whether the specified line. Note that this implementation will return - * true even if the line is completely enclosed by this RectRegion. - * WARNING: this implementation has problems. See associated unit test for details. - * @param x1 x-coord of the line beginning - * @param y1 y-coord of the line beginning - * @param x2 x-coord of the line end - * @param y2 y-coord of the line end - * @return True if this RectRegion overlaps any part of the specified line. - */ - public boolean intersectsWithLine(Number x1, Number y1, Number x2, Number y2) { - if(contains(x1, y1) || contains(x2, y2)) { - return true; - } - - // if true, it means that these points exist on different sides of the rect's edges - final boolean x1MinRelation = x1.doubleValue() < getMinX().doubleValue(); - final boolean x2MinRelation = x2.doubleValue() < getMinX().doubleValue(); - final boolean xMinRelation = x1MinRelation &! x2MinRelation; - - final boolean x1MaxRelation = x1.doubleValue() < getMaxX().doubleValue(); - final boolean x2MaxRelation = x2.doubleValue() < getMaxX().doubleValue(); - final boolean xMaxRelation = x1MaxRelation &! x2MaxRelation; - - final boolean y1MinRelation = y1.doubleValue() < getMinY().doubleValue(); - final boolean y2MinRelation = y2.doubleValue() < getMinY().doubleValue(); - final boolean yMinRelation = y1MinRelation &! y2MinRelation; - - final boolean y1MaxRelation = y1.doubleValue() < getMaxY().doubleValue(); - final boolean y2MaxRelation = y2.doubleValue() < getMaxY().doubleValue(); - final boolean yMaxRelation = y1MaxRelation &! y2MaxRelation; - - return ((xMinRelation | xMaxRelation) || getxRegion().contains(x1) & (yMinRelation | yMaxRelation) || getyRegion().contains(y1)); + @Override + public String toString() { + return "RectRegion{" + + "xRegion=" + xRegion + + ", yRegion=" + yRegion + + ", label='" + label + '\'' + + '}'; } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java index b10903b1..f795536b 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java @@ -89,7 +89,7 @@ public void run() { lastResamplingException = ex; } } - }); + }, "Androidplot XY Series Sampler"); getZoomLevels().add(thisSeries); threads.add(thread); thread.start(); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java index f5ed5458..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,10 +17,15 @@ 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; @@ -28,9 +33,6 @@ * A convenience class used to create instances of XYPlot generated from Lists of Numbers. */ public class SimpleXYSeries implements EditableXYSeries, OrderedXYSeries, PlotListener { - - private static final String TAG = SimpleXYSeries.class.getName(); - private volatile LinkedList xVals = new LinkedList<>(); private volatile LinkedList yVals = new LinkedList<>(); private volatile String title = null; @@ -82,10 +84,8 @@ public void onAfterDraw(Plot source, Canvas canvas) { } 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; } @@ -141,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: @@ -153,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."); @@ -214,7 +215,6 @@ public void resize(int size) { try { lock.writeLock().lock(); if (xVals.size() < size) { - for (int i = xVals.size(); i < size; i++) { xVals.add(null); yVals.add(null); @@ -341,7 +341,9 @@ public LinkedList getyVals() { public void clear() { lock.writeLock().lock(); try { - xVals.clear(); + if (xVals != null) { + xVals.clear(); + } yVals.clear(); } finally { lock.writeLock().unlock(); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java b/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java index d5db2c9d..00f60747 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java @@ -20,9 +20,11 @@ * INCREMENTAL_VALUE - (default) draw a tick every n values. * INCREMENTAL_PIXEL - draw a tick every n pixels. * SUBDIVIDE - draw n number of evenly spaced lines. + * INCREMENT_BY_FIT choose increment from a list of possible values */ public enum StepMode { SUBDIVIDE, // default INCREMENT_BY_VAL, - INCREMENT_BY_PIXELS + INCREMENT_BY_PIXELS, + INCREMENT_BY_FIT } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java b/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java new file mode 100644 index 00000000..d2b12f0b --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java @@ -0,0 +1,85 @@ +package com.androidplot.xy; + +import com.androidplot.Region; + +import java.util.Arrays; + +/** + * Subclass of StepModel that chooses from predefined step values. Depending on the currently + * displayed range (by value) choose increment so that the number of lines + * is closest to StepModel.value + */ +public class StepModelFit extends StepModel { + + private double[] steps; // list of steps to choose from + private Region scale; // axis region on display + + public StepModelFit(Region axisRegion, double[] increments, double numLines) { + super(StepMode.INCREMENT_BY_FIT, numLines); + + setSteps(increments); + setScale(axisRegion); + } + + public double[] getSteps() { + return steps; + } + + public void setSteps(double[] steps) { + + // sanity checks: no null, 0 or negative + if (steps == null || steps.length == 0) + return; + + for (double step : steps) { + if (step <= 0.0d) + return; + } + + this.steps = steps; + } + + public Region getScale() { + return scale; + } + + public void setScale(Region scale) { + this.scale = scale; + } + + // does not return StepModel.value instead calculates best fit + @Override + public double getValue() { + + // no possible increments where supplied + // or no region defined + if (steps == null || scale == null || !scale.isDefined()) + return super.getValue(); + + double curStep = steps[0]; + double oldDistance = Math.abs((scale.length().doubleValue() / curStep)-super.getValue() ); + + // determine which step size comes closest to the desired number of steps + // since steps[] is a small array brute force search is ok + for (double step : steps) { + + double newDistance = Math.abs((scale.length().doubleValue() / step)-super.getValue() ); + + // closer than previous stepping? + if (newDistance < oldDistance){ + curStep = step; + oldDistance = newDistance; + } + } + return curStep; + } + + @Override + public String toString() { + return "StepModelFit{" + + "steps=" + Arrays.toString(steps) + + ", scale=" + scale + + ", current stepping=" + getValue() + + '}'; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java index 04f0449e..f14608d7 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java @@ -23,7 +23,6 @@ * Renders a point as a line with the vertices marked. Requires 2 or more points to * be rendered. */ - public class StepRenderer extends LineAndPointRenderer { public StepRenderer(XYPlot plot) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java b/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java index 30e9aacd..de0436b2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java @@ -16,10 +16,14 @@ package com.androidplot.xy; +import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.RectF; + import com.androidplot.ui.PositionMetric; import com.androidplot.ui.TextOrientation; +import com.androidplot.util.FontUtils; /** * Encapsulates a single axis line marker drawn onto an XYPlot at a specified value. @@ -27,6 +31,8 @@ */ public abstract class ValueMarker { + private static final int MARKER_LABEL_SPACING = 2; + public String getText() { return text; } @@ -135,4 +141,37 @@ public PositionMetricType getTextPosition() { public void setTextPosition(PositionMetricType textPosition) { this.textPosition = textPosition; } + + /** + * Renders the text associated with user defined markers + * + * @param canvas + * @param text + * @param gridRect + * @param x + * @param y + */ + protected void drawMarkerText(Canvas canvas, String text, RectF gridRect, + float x, float y) { + if (getText() != null) { + x += MARKER_LABEL_SPACING; + y -= MARKER_LABEL_SPACING; + RectF textRect = new RectF(FontUtils.getStringDimensions(text, getTextPaint() + )); + textRect.offsetTo(x, y - textRect.height()); + + if (textRect.right > gridRect.right) { + textRect.offset(-(textRect.right - gridRect.right), 0); + } + + if (textRect.top < gridRect.top) { + textRect.offset(0, gridRect.top - textRect.top); + } + + canvas.drawText(text, textRect.left, textRect.bottom, getTextPaint() + ); + } + } + + public abstract void draw(Canvas canvas, XYPlot plot, RectF gridRect); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java b/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java index af02ca64..a4c12438 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java @@ -16,7 +16,10 @@ package com.androidplot.xy; +import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.RectF; + import com.androidplot.ui.VerticalPositioning; import com.androidplot.ui.VerticalPosition; @@ -55,4 +58,19 @@ public XValueMarker(Number value, String text, VerticalPosition textPosition, Pa public XValueMarker(Number value, String text, VerticalPosition textPosition, int linePaint, int textPaint) { super(value, text, textPosition, linePaint, textPaint); } + + @Override + public void draw(Canvas canvas, XYPlot plot, RectF gridRect) { + if (getValue() != null) { + float xPix = (float) plot.getBounds().xRegion + .transform(getValue().doubleValue(), gridRect.left, gridRect.right, false); + canvas.drawLine(xPix, gridRect.top, xPix, gridRect.bottom, getLinePaint() + ); + float yPix = getTextPosition().getPixelValue(gridRect.height()); + yPix += gridRect.top; + if (getText() != null) { + drawMarkerText(canvas, getText(), gridRect, xPix, yPix); + } + } + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java index ae22a4ea..c4452d33 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java @@ -16,8 +16,12 @@ package com.androidplot.xy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + /** * Calculates the min/max constraints for an xy plane. + * * @since 0.9.7 */ public class XYConstraints { @@ -44,113 +48,148 @@ public XYConstraints() { this(null, null, null, null); } - public XYConstraints(Number minX, Number maxX, Number minY, Number maxY) { + public XYConstraints(@Nullable Number minX, @Nullable Number maxX, @Nullable Number minY, @Nullable Number maxY) { this.minX = minX; this.minY = minY; this.maxX = maxX; this.maxY = maxY; } + public boolean contains(@NonNull RectRegion rectRegion) { + return contains(rectRegion.getMinY(), rectRegion.getMinY()) + && contains(rectRegion.getMaxX(), rectRegion.getMaxY()); + } + public boolean contains(Number x, Number y) { - if(x == null || y == null) { + if (x == null || y == null) { // this is essentially an invisible point: return false; - } else { - final double dx = x.doubleValue(); - - if(minX != null && dx < minX.doubleValue()) { - return false; - } else if(maxX != null && dx > maxX.doubleValue()) { - return false; - } else { - final double dy = y.doubleValue(); - if(minY != null && dy < minY.doubleValue()) { - return false; - } else if(maxY != null && dy > maxY.doubleValue()) { - return false; - } - } + } + + if (minX == null && maxX == null && minY == null && maxY == null) { + //there are no constraints return true; } + + final double dx = x.doubleValue(); + if (minX != null && dx < minX.doubleValue()) { + return false; + } else if (maxX != null && dx > maxX.doubleValue()) { + return false; + } + + final double dy = y.doubleValue(); + if (minY != null && dy < minY.doubleValue()) { + return false; + } else if (maxY != null && dy > maxY.doubleValue()) { + return false; + } + + return true; } + @Nullable public Number getMinX() { return minX; } + @Nullable public Number getMaxX() { return maxX; } + @Nullable public Number getMinY() { return minY; } + @Nullable public Number getMaxY() { return maxY; } + public void setMinX(@Nullable Number minX) { + this.minX = minX; + } + + public void setMaxX(@Nullable Number maxX) { + this.maxX = maxX; + } + + public void setMinY(@Nullable Number minY) { + this.minY = minY; + } + + public void setMaxY(@Nullable Number maxY) { + this.maxY = maxY; + } + + @NonNull public XYFramingModel getDomainFramingModel() { return domainFramingModel; } - public void setDomainFramingModel(XYFramingModel domainFramingModel) { + public void setDomainFramingModel(@NonNull XYFramingModel domainFramingModel) { this.domainFramingModel = domainFramingModel; } + @NonNull public XYFramingModel getRangeFramingModel() { return rangeFramingModel; } - public void setRangeFramingModel(XYFramingModel rangeFramingModel) { + public void setRangeFramingModel(@NonNull XYFramingModel rangeFramingModel) { this.rangeFramingModel = rangeFramingModel; } + @NonNull public BoundaryMode getDomainUpperBoundaryMode() { return domainUpperBoundaryMode; } - public void setDomainUpperBoundaryMode(BoundaryMode domainUpperBoundaryMode) { + public void setDomainUpperBoundaryMode(@NonNull BoundaryMode domainUpperBoundaryMode) { this.domainUpperBoundaryMode = domainUpperBoundaryMode; } + @NonNull public BoundaryMode getDomainLowerBoundaryMode() { return domainLowerBoundaryMode; } - public void setDomainLowerBoundaryMode(BoundaryMode domainLowerBoundaryMode) { + public void setDomainLowerBoundaryMode(@NonNull BoundaryMode domainLowerBoundaryMode) { this.domainLowerBoundaryMode = domainLowerBoundaryMode; } + @NonNull public BoundaryMode getRangeUpperBoundaryMode() { return rangeUpperBoundaryMode; } - public void setRangeUpperBoundaryMode(BoundaryMode rangeUpperBoundaryMode) { + public void setRangeUpperBoundaryMode(@NonNull BoundaryMode rangeUpperBoundaryMode) { this.rangeUpperBoundaryMode = rangeUpperBoundaryMode; } + @NonNull public BoundaryMode getRangeLowerBoundaryMode() { return rangeLowerBoundaryMode; } - public void setRangeLowerBoundaryMode(BoundaryMode rangeLowerBoundaryMode) { + public void setRangeLowerBoundaryMode(@NonNull BoundaryMode rangeLowerBoundaryMode) { this.rangeLowerBoundaryMode = rangeLowerBoundaryMode; } - public void setMinX(Number minX) { - this.minX = minX; - } - - public void setMaxX(Number maxX) { - this.maxX = maxX; - } - - public void setMinY(Number minY) { - this.minY = minY; - } - - public void setMaxY(Number maxY) { - this.maxY = maxY; + @Override + public String toString() { + return "XYConstraints{" + "domainFramingModel=" + domainFramingModel + + ", rangeFramingModel=" + rangeFramingModel + + ", domainUpperBoundaryMode=" + domainUpperBoundaryMode + + ", domainLowerBoundaryMode=" + domainLowerBoundaryMode + + ", rangeUpperBoundaryMode=" + rangeUpperBoundaryMode + + ", rangeLowerBoundaryMode=" + rangeLowerBoundaryMode + + ", minX=" + minX + + ", maxX=" + maxX + + ", minY=" + minY + + ", maxY=" + maxY + + '}'; } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java index 7f9114b4..8fa0571f 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java @@ -16,23 +16,38 @@ package com.androidplot.xy; -import android.content.res.*; -import android.graphics.*; - -import com.androidplot.*; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.androidplot.R; import com.androidplot.Region; -import com.androidplot.exception.PlotRenderException; -import com.androidplot.ui.*; +import com.androidplot.ui.Insets; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.RenderStack; +import com.androidplot.ui.Size; import com.androidplot.ui.widget.Widget; -import com.androidplot.util.*; +import com.androidplot.util.AttrUtils; +import com.androidplot.util.FontUtils; +import com.androidplot.util.PixelUtils; +import com.androidplot.util.RectFUtils; import java.text.DecimalFormat; import java.text.Format; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; /** - * Displays graphical data (lines, points, etc.) annotated with domain and range tick markers. - * The inner area of the graph upon which grid lines and points are rendered is called the "grid" area. + * Displays graphical data (lines, points, etc.) annotated with domain and range tick markers. The + * inner area of the graph upon which grid lines and points are rendered is called the "grid" area. */ public class XYGraphWidget extends Widget { @@ -47,8 +62,6 @@ public class XYGraphWidget extends Widget { private static final float DEFAULT_LINE_LABEL_TEXT_SIZE_PX = PixelUtils.spToPix(15); - private static final int MARKER_LABEL_SPACING = TWO; - /** * Line interval per range label */ @@ -121,21 +134,26 @@ public class XYGraphWidget extends Widget { /** * Set of edges for which line labels should be displayed */ - private Set lineLabelEdges = new HashSet<>(); + private EnumSet lineLabelEdges = EnumSet.noneOf(Edge.class); private RenderStack renderStack; private CursorLabelFormatter cursorLabelFormatter; - private HashMap lineLabelStyles = getDefaultLineLabelStyles(); - private HashMap lineLabelRenderers = getDefaultLineLabelRenderers(); + private Map lineLabelStyles = getDefaultLineLabelStyles(); + private Map lineLabelRenderers = getDefaultLineLabelRenderers(); public static class LineLabelRenderer { - public void drawLabel(Canvas canvas, LineLabelStyle style, Number val, float x, float y, boolean isOrigin) { + public void drawLabel(Canvas canvas, + LineLabelStyle style, + Number val, + float x, + float y, + boolean isOrigin) { final int canvasState = canvas.save(); try { - final String txt = style.format.format(val.doubleValue()); + final String txt = style.format.format(val); canvas.rotate(style.getRotation(), x, y); drawLabel(canvas, txt, style.getPaint(), x, y, isOrigin); } finally { @@ -143,7 +161,12 @@ public void drawLabel(Canvas canvas, LineLabelStyle style, Number val, float x, } } - protected void drawLabel(Canvas canvas, String text, Paint paint, float x, float y, boolean isOrigin) { + protected void drawLabel(Canvas canvas, + String text, + Paint paint, + float x, + float y, + boolean isOrigin) { canvas.drawText(text, x, y, paint); } } @@ -159,6 +182,7 @@ public static class LineLabelStyle { paint.setTextAlign(Paint.Align.CENTER); paint.setTextSize(DEFAULT_LINE_LABEL_TEXT_SIZE_PX); } + public Format getFormat() { return format; } @@ -187,17 +211,16 @@ public void setPaint(Paint paint) { public interface CursorLabelFormatter { /** - * * @return The Paint to be used to draw the cursor text label. */ Paint getTextPaint(); /** - * - * @return Null if no background should be drawn, - * the Paint used to draw the background otherwise. + * @return Null if no background should be drawn, the Paint used to draw the background + * otherwise. */ Paint getBackgroundPaint(); + String getLabelText(Number x, Number y); } @@ -256,18 +279,21 @@ public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) { /** * Apply xml attrs + * * @param attrs */ public void processAttrs(TypedArray attrs) { setDrawGridOnTop(attrs.getBoolean(R.styleable.xy_XYPlot_drawGridOnTop, isDrawGridOnTop())); int tlp = attrs.getInt(R.styleable.xy_XYPlot_lineLabels, 0); - if(tlp != 0) { + if (tlp != 0) { setLineLabelEdges(tlp); } - setGridClippingEnabled(attrs.getBoolean(R.styleable.xy_XYPlot_gridClippingEnabled, - isGridClippingEnabled())); + setGridClippingEnabled(attrs.getBoolean( + R.styleable.xy_XYPlot_gridClippingEnabled, + isGridClippingEnabled() + )); final LineLabelStyle lineLabelStyleTop = getLineLabelStyle(Edge.TOP); final LineLabelStyle lineLabelStyleBottom = getLineLabelStyle(Edge.BOTTOM); @@ -276,19 +302,23 @@ public void processAttrs(TypedArray attrs) { lineLabelStyleTop.setRotation(attrs.getFloat( R.styleable.xy_XYPlot_lineLabelRotationTop, - lineLabelStyleTop.getRotation())); + lineLabelStyleTop.getRotation() + )); lineLabelStyleBottom.setRotation(attrs.getFloat( R.styleable.xy_XYPlot_lineLabelRotationBottom, - lineLabelStyleBottom.getRotation())); + lineLabelStyleBottom.getRotation() + )); lineLabelStyleLeft.setRotation(attrs.getFloat( R.styleable.xy_XYPlot_lineLabelRotationLeft, - lineLabelStyleLeft.getRotation())); + lineLabelStyleLeft.getRotation() + )); lineLabelStyleRight.setRotation(attrs.getFloat( R.styleable.xy_XYPlot_lineLabelRotationRight, - lineLabelStyleRight.getRotation())); + lineLabelStyleRight.getRotation() + )); setLineExtensionTop(attrs.getDimension( R.styleable.xy_XYPlot_lineExtensionTop, getLineExtensionTop())); @@ -302,34 +332,40 @@ public void processAttrs(TypedArray attrs) { AttrUtils.configureTextPaint(attrs, lineLabelStyleTop.getPaint(), R.styleable.xy_XYPlot_lineLabelTextColorTop, R.styleable.xy_XYPlot_lineLabelTextSizeTop, - R.styleable.xy_XYPlot_lineLabelAlignTop); + R.styleable.xy_XYPlot_lineLabelAlignTop + ); AttrUtils.configureTextPaint(attrs, lineLabelStyleBottom.getPaint(), R.styleable.xy_XYPlot_lineLabelTextColorBottom, R.styleable.xy_XYPlot_lineLabelTextSizeBottom, - R.styleable.xy_XYPlot_lineLabelAlignBottom); + R.styleable.xy_XYPlot_lineLabelAlignBottom + ); AttrUtils.configureTextPaint(attrs, lineLabelStyleLeft.getPaint(), R.styleable.xy_XYPlot_lineLabelTextColorLeft, R.styleable.xy_XYPlot_lineLabelTextSizeLeft, - R.styleable.xy_XYPlot_lineLabelAlignLeft); + R.styleable.xy_XYPlot_lineLabelAlignLeft + ); AttrUtils.configureTextPaint(attrs, lineLabelStyleRight.getPaint(), R.styleable.xy_XYPlot_lineLabelTextColorRight, R.styleable.xy_XYPlot_lineLabelTextSizeRight, - R.styleable.xy_XYPlot_lineLabelAlignRight); + R.styleable.xy_XYPlot_lineLabelAlignRight + ); AttrUtils.configureInsets(attrs, getGridInsets(), R.styleable.xy_XYPlot_gridInsetTop, R.styleable.xy_XYPlot_gridInsetBottom, R.styleable.xy_XYPlot_gridInsetLeft, - R.styleable.xy_XYPlot_gridInsetRight); + R.styleable.xy_XYPlot_gridInsetRight + ); AttrUtils.configureInsets(attrs, getLineLabelInsets(), R.styleable.xy_XYPlot_lineLabelInsetTop, R.styleable.xy_XYPlot_lineLabelInsetBottom, R.styleable.xy_XYPlot_lineLabelInsetLeft, - R.styleable.xy_XYPlot_lineLabelInsetRight); + R.styleable.xy_XYPlot_lineLabelInsetRight + ); // graph size & position AttrUtils.configureWidget(attrs, this, @@ -337,7 +373,8 @@ public void processAttrs(TypedArray attrs) { R.styleable.xy_XYPlot_graphWidthMode, R.styleable.xy_XYPlot_graphWidth, R.styleable.xy_XYPlot_graphHorizontalPositioning, R.styleable.xy_XYPlot_graphHorizontalPosition, R.styleable.xy_XYPlot_graphVerticalPositioning, R.styleable.xy_XYPlot_graphVerticalPosition, - R.styleable.xy_XYPlot_graphAnchor, R.styleable.xy_XYPlot_graphVisible); + R.styleable.xy_XYPlot_graphAnchor, R.styleable.xy_XYPlot_graphVisible + ); // domainLabel size & position AttrUtils.configureWidget(attrs, this, @@ -345,7 +382,8 @@ public void processAttrs(TypedArray attrs) { R.styleable.xy_XYPlot_domainTitleWidthMode, R.styleable.xy_XYPlot_domainTitleWidth, R.styleable.xy_XYPlot_domainTitleHorizontalPositioning, R.styleable.xy_XYPlot_domainTitleHorizontalPosition, R.styleable.xy_XYPlot_domainTitleVerticalPositioning, R.styleable.xy_XYPlot_domainTitleVerticalPosition, - R.styleable.xy_XYPlot_domainTitleAnchor, R.styleable.xy_XYPlot_domainTitleVisible); + R.styleable.xy_XYPlot_domainTitleAnchor, R.styleable.xy_XYPlot_domainTitleVisible + ); // rangeLabel size & position AttrUtils.configureWidget(attrs, this, @@ -353,7 +391,8 @@ public void processAttrs(TypedArray attrs) { R.styleable.xy_XYPlot_rangeTitleWidthMode, R.styleable.xy_XYPlot_rangeTitleWidth, R.styleable.xy_XYPlot_rangeTitleHorizontalPositioning, R.styleable.xy_XYPlot_rangeTitleHorizontalPosition, R.styleable.xy_XYPlot_rangeTitleVerticalPositioning, R.styleable.xy_XYPlot_rangeTitleVerticalPosition, - R.styleable.xy_XYPlot_rangeTitleAnchor, R.styleable.xy_XYPlot_rangeTitleVisible); + R.styleable.xy_XYPlot_rangeTitleAnchor, R.styleable.xy_XYPlot_rangeTitleVisible + ); // rotation AttrUtils.configureWidgetRotation(attrs, this, R.styleable.xy_XYPlot_graphRotation); @@ -363,31 +402,38 @@ public void processAttrs(TypedArray attrs) { R.styleable.xy_XYPlot_graphMarginTop, R.styleable.xy_XYPlot_graphMarginBottom, R.styleable.xy_XYPlot_graphMarginLeft, R.styleable.xy_XYPlot_graphMarginRight, R.styleable.xy_XYPlot_graphPaddingTop, R.styleable.xy_XYPlot_graphPaddingBottom, - R.styleable.xy_XYPlot_graphPaddingLeft, R.styleable.xy_XYPlot_graphPaddingRight); + R.styleable.xy_XYPlot_graphPaddingLeft, R.styleable.xy_XYPlot_graphPaddingRight + ); // domainOriginLinePaint AttrUtils.configureLinePaint(attrs, getDomainOriginLinePaint(), R.styleable.xy_XYPlot_domainOriginLineColor, - R.styleable.xy_XYPlot_domainOriginLineThickness); + R.styleable.xy_XYPlot_domainOriginLineThickness + ); // rangeOriginLinePaint AttrUtils.configureLinePaint(attrs, getRangeOriginLinePaint(), R.styleable.xy_XYPlot_rangeOriginLineColor, - R.styleable.xy_XYPlot_rangeOriginLineThickness); + R.styleable.xy_XYPlot_rangeOriginLineThickness + ); AttrUtils.configureLinePaint(attrs, getDomainGridLinePaint(), R.styleable.xy_XYPlot_domainLineColor, - R.styleable.xy_XYPlot_domainLineThickness); + R.styleable.xy_XYPlot_domainLineThickness + ); AttrUtils.configureLinePaint(attrs, getRangeGridLinePaint(), R.styleable.xy_XYPlot_rangeLineColor, - R.styleable.xy_XYPlot_rangeLineThickness); + R.styleable.xy_XYPlot_rangeLineThickness + ); AttrUtils.setColor(attrs, getBackgroundPaint(), - R.styleable.xy_XYPlot_graphBackgroundColor); + R.styleable.xy_XYPlot_graphBackgroundColor + ); AttrUtils.setColor(attrs, getGridBackgroundPaint(), - R.styleable.xy_XYPlot_gridBackgroundColor); + R.styleable.xy_XYPlot_gridBackgroundColor + ); } /** @@ -399,7 +445,7 @@ public void processAttrs(TypedArray attrs) { * @return */ protected XYCoords screenToSeries(PointF point) { - if(!plot.getBounds().isFullyDefined()) { + if (!plot.getBounds().isFullyDefined()) { return null; } return new RectRegion(gridRect) @@ -450,6 +496,7 @@ protected Number screenToSeriesX(float xPix) { * 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 */ @@ -462,7 +509,7 @@ protected Number screenToSeriesY(float yPix) { } protected PointF seriesToScreen(XYCoords xy) { - if(!plot.getBounds().isFullyDefined()) { + if (!plot.getBounds().isFullyDefined()) { return null; } return plot.getBounds().transform(xy, gridRect, false, true); @@ -475,20 +522,24 @@ protected float seriesToScreenX(Number x) { protected float seriesToScreenY(Number y) { return (float) plot.getBounds().getyRegion(). - transform(y.doubleValue(), gridRect.left, gridRect.right, true); + 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); + } - if(gridRect == null) { - gridRect = RectFUtils.applyInsets(widgetRect, gridInsets); + protected void recalculateSizes(@Nullable RectF rect) { + if(rect == null) { + rect = getWidgetDimensions().paddedRect; } + gridRect = RectFUtils.applyInsets(rect, gridInsets); + labelRect = RectFUtils.applyInsets(rect, lineLabelInsets); + } - if(labelRect == null) { - labelRect = RectFUtils.applyInsets(widgetRect, lineLabelInsets); - } + @Override + protected void doOnDraw(Canvas canvas, RectF widgetRect) { // don't draw if we have no space to draw into if (gridRect.height() > ZERO && gridRect.width() > ZERO) { @@ -497,7 +548,7 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect) && bounds.getMaxX() != null && bounds.getMinY() != null && bounds.getMaxY() != null) { - if(drawGridOnTop) { + if (drawGridOnTop) { drawData(canvas); drawGrid(canvas); } else { @@ -513,101 +564,104 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect) } protected void drawDomainLine(Canvas canvas, float xPix, Number xVal, - Paint linePaint, boolean isOrigin) { + Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) { // lines if (linePaint != null) { - canvas.drawLine(xPix, gridRect.top - lineExtensionTop, - xPix, gridRect.bottom + lineExtensionBottom, linePaint); + canvas.drawLine(xPix, gridRect.top - lineExtensionTop, + xPix, gridRect.bottom + lineExtensionBottom, linePaint + ); } // labels - drawLineLabel(canvas, Edge.TOP, xVal, xPix, labelRect.top, isOrigin); - drawLineLabel(canvas, Edge.BOTTOM, xVal, xPix, labelRect.bottom, isOrigin); + if(shouldDrawLabel) { + if (isLineLabelEnabled(Edge.TOP)) { + drawLineLabel(canvas, Edge.TOP, xVal, xPix, labelRect.top, isOrigin); + } + + if (isLineLabelEnabled(Edge.BOTTOM)) { + drawLineLabel(canvas, Edge.BOTTOM, xVal, xPix, labelRect.bottom, isOrigin); + } + } } protected void drawRangeLine(Canvas canvas, float yPix, Number yVal, - Paint linePaint, boolean isOrigin) { + Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) { // lines if (linePaint != null) { canvas.drawLine(gridRect.left - lineExtensionLeft, yPix, - gridRect.right + lineExtensionRight, yPix, linePaint); + gridRect.right + lineExtensionRight, yPix, linePaint + ); } - // labels - drawLineLabel(canvas, Edge.LEFT, yVal, labelRect.left, yPix, isOrigin); - drawLineLabel(canvas, Edge.RIGHT, yVal, labelRect.right, yPix, isOrigin); + if(shouldDrawLabel) { + // labels + if (isLineLabelEnabled(Edge.LEFT)) { + drawLineLabel(canvas, Edge.LEFT, yVal, labelRect.left, yPix, isOrigin); + } + if (isLineLabelEnabled(Edge.RIGHT)) { + drawLineLabel(canvas, Edge.RIGHT, yVal, labelRect.right, yPix, isOrigin); + } + } } - protected void drawLineLabel(Canvas canvas, Edge edge, Number val, float x, float y, boolean isOrigin) { - if(isLineLabelEnabled(edge)) { - getLineLabelRenderer(edge).drawLabel(canvas, getLineLabelStyle(edge), val, x, y, isOrigin); - } + protected void drawLineLabel(Canvas canvas, + Edge edge, + Number val, + float x, + float y, + boolean isOrigin) { + getLineLabelRenderer(edge).drawLabel(canvas, getLineLabelStyle(edge), val, x, y, isOrigin); } /** - * Draws the drid and domain/range labels for the plot. + * Draws the grid and domain/range labels for the plot. * * @param canvas */ protected void drawGrid(Canvas canvas) { - if(!drawGridOnTop) { + if (!drawGridOnTop) { drawGridBackground(canvas); } Number domainOrigin = plot.getDomainOrigin(); - double domainOriginPix; + final double domainOriginPix; if (domainOrigin != null) { domainOriginPix = plot.getBounds().getxRegion().transform( plot.getDomainOrigin().doubleValue(), gridRect.left, gridRect.right, false); } else { // if no domain origin is set, use the leftmost value visible on the grid: domainOriginPix = gridRect.left; - domainOrigin=plot.getBounds().getMinX(); + domainOrigin = plot.getBounds().getMinX(); } Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect); - // draw domain origin: - if (domainOriginPix >= gridRect.left - && domainOriginPix <= gridRect.right) { - drawDomainLine(canvas, (float) domainOriginPix, - domainOrigin, domainOriginLinePaint, true); - } - - // draw lines LEFT of origin: - double xPix = domainOriginPix - domainStep.getStepPix(); - for (int i = ONE; xPix >= gridRect.left - FUDGE; xPix = domainOriginPix - - (i * domainStep.getStepPix())) { - double xVal = domainOrigin.doubleValue() - i - * domainStep.getStepVal(); - - if (xPix <= gridRect.right) { - final boolean isDomainTick = i% getLinesPerDomainLabel() == ZERO; - final Paint lp = isDomainTick ? domainGridLinePaint : domainSubGridLinePaint; - drawDomainLine(canvas, (float) xPix, xVal, lp, false); + // Draw Domain Lines: + + final double domainStepPix = domainStep.getStepPix(); + final double iMin = (gridRect.left - domainOriginPix - FUDGE) / domainStepPix; + final double iMax = (gridRect.right - domainOriginPix + FUDGE) / domainStepPix; + + for (int i = (int) Math.ceil(iMin); i <= iMax; i++) { + double xVal = domainOrigin.doubleValue() + i * domainStep.getStepVal(); + double xPix = domainOriginPix + i * domainStepPix; + boolean isMajorTick = i % getLinesPerDomainLabel() == ZERO; + boolean isOrigin = i == 0; + Paint linePaint; + if (isOrigin) { + linePaint = domainOriginLinePaint; + } else if (isMajorTick) { + linePaint = domainGridLinePaint; + } else { + linePaint = domainSubGridLinePaint; } - i++; - } - - // draw lines RIGHT of origin: - xPix = domainOriginPix + domainStep.getStepPix(); - for (int i = ONE; xPix <= gridRect.right + FUDGE; xPix = domainOriginPix - + (i * domainStep.getStepPix())) { - double xVal = domainOrigin.doubleValue() + i - * domainStep.getStepVal(); - - if (xPix >= gridRect.left) { - final boolean isDomainTick = i% getLinesPerDomainLabel() == ZERO; - final Paint lp = isDomainTick ? domainGridLinePaint : domainSubGridLinePaint; - drawDomainLine(canvas, (float) xPix, xVal, lp, false); - } - i++; + drawDomainLine(canvas, (float) xPix, xVal, linePaint, isOrigin, isMajorTick); } Number rangeOrigin = plot.getRangeOrigin(); - double rangeOriginPix; + final double rangeOriginPix; if (rangeOrigin != null) { rangeOriginPix = plot.getBounds().getyRegion().transform( rangeOrigin.doubleValue(), gridRect.top, gridRect.bottom, true); @@ -619,106 +673,42 @@ protected void drawGrid(Canvas canvas) { Step rangeStep = XYStepCalculator.getStep(plot, Axis.RANGE, gridRect); - // draw range origin: - if (rangeOriginPix >= gridRect.top && rangeOriginPix <= gridRect.bottom) { - drawRangeLine(canvas, (float) rangeOriginPix, - rangeOrigin, rangeOriginLinePaint, true); - } + // Draw Range Lines: final double rangeStepPix = rangeStep.getStepPix(); - - // draw lines ABOVE origin: - double yPix = rangeOriginPix - rangeStep.getStepPix(); - for (int i = ONE; yPix >= gridRect.top - FUDGE; yPix = rangeOriginPix - (i * rangeStepPix)) { - double yVal = rangeOrigin.doubleValue() + i - * rangeStep.getStepVal(); - - if (yPix <= gridRect.bottom) { - final boolean isRangeTick = i% getLinesPerRangeLabel() == ZERO; - final Paint lp = isRangeTick ? rangeGridLinePaint : rangeSubGridLinePaint; - drawRangeLine(canvas, (float)yPix, yVal, lp, false); + final double kMin = (gridRect.top - rangeOriginPix - FUDGE) / rangeStepPix; + final double kMax = (gridRect.bottom - rangeOriginPix + FUDGE) / rangeStepPix; + + for (int k = (int) Math.ceil(kMin); k <= kMax; k++) { + // Android vertical coordinates (zero at the top of the screen) are the opposite + // direction of default range values (lowest on bottom of screen) so we subtract when + // calculating yVal + double yVal = rangeOrigin.doubleValue() - k * rangeStep.getStepVal(); + double yPix = rangeOriginPix + k * rangeStepPix; + boolean isMajorTick = k % getLinesPerRangeLabel() == ZERO; + boolean isOrigin = k == 0; + Paint linePaint; + if (isOrigin) { + linePaint = rangeOriginLinePaint; + } else if (isMajorTick) { + linePaint = rangeGridLinePaint; + } else { + linePaint = rangeSubGridLinePaint; } - i++; + drawRangeLine(canvas, (float) yPix, yVal, linePaint, isOrigin, isMajorTick); } - - // draw lines BENEATH origin: - yPix = rangeOriginPix + rangeStep.getStepPix(); - for (int i = ONE; yPix <= gridRect.bottom + FUDGE; yPix = rangeOriginPix + (i * rangeStepPix)) { - double yVal = rangeOrigin.doubleValue() - i - * rangeStep.getStepVal(); - if (yPix >= gridRect.top) { - final boolean isRangeTick = i% getLinesPerRangeLabel() == ZERO; - final Paint lp = isRangeTick ? rangeGridLinePaint : rangeSubGridLinePaint; - drawRangeLine(canvas, (float)yPix, yVal, lp, false); - } - 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) { - x += MARKER_LABEL_SPACING; - y -= MARKER_LABEL_SPACING; - RectF textRect = new RectF(FontUtils.getStringDimensions(text, - marker.getTextPaint())); - textRect.offsetTo(x, y - textRect.height()); - - if (textRect.right > gridRect.right) { - textRect.offset(-(textRect.right - gridRect.right), ZERO); - } - - if (textRect.top < gridRect.top) { - textRect.offset(0, gridRect.top - textRect.top); - } - - canvas.drawText(text, textRect.left, textRect.bottom, - marker.getTextPaint()); } protected void drawMarkers(Canvas canvas) { - if(plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) { + if (plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) { for (YValueMarker marker : plot.getYValueMarkers()) { - if (marker.getValue() != null) { - float yPix = (float) plot.getBounds().yRegion - .transform(marker.getValue() - .doubleValue(), gridRect.top, gridRect.bottom, true); - canvas.drawLine(gridRect.left, yPix, - gridRect.right, yPix, marker.getLinePaint()); - - float xPix = marker.getTextPosition().getPixelValue( - gridRect.width()); - xPix += gridRect.left; - - if (marker.getText() != null) { - drawMarkerText(canvas, marker.getText(), marker, xPix, yPix); - } - } + marker.draw(canvas, plot, gridRect); } } - if(plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) { + if (plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) { for (XValueMarker marker : plot.getXValueMarkers()) { - if (marker.getValue() != null) { - float xPix = (float) plot.getBounds().xRegion - .transform(marker.getValue() - .doubleValue(), gridRect.left, gridRect.right, false); - canvas.drawLine(xPix, gridRect.top, xPix, gridRect.bottom, - marker.getLinePaint()); - float yPix = marker.getTextPosition().getPixelValue(gridRect.height()); - yPix += gridRect.top; - if (marker.getText() != null) { - drawMarkerText(canvas, marker.getText(), marker, xPix, yPix); - } - } + marker.draw(canvas, plot, gridRect); } } } @@ -733,7 +723,8 @@ protected void drawCursors(Canvas canvas) { hasDomainCursor = true; canvas.drawLine(domainCursorPosition, gridRect.top, domainCursorPosition, gridRect.bottom, - domainCursorPaint); + domainCursorPaint + ); } boolean hasRangeCursor = false; @@ -744,10 +735,11 @@ protected void drawCursors(Canvas canvas) { && rangeCursorPosition <= gridRect.bottom) { hasRangeCursor = true; canvas.drawLine(gridRect.left, rangeCursorPosition, - gridRect.right, rangeCursorPosition, rangeCursorPaint); + gridRect.right, rangeCursorPosition, rangeCursorPaint + ); } - if(getCursorLabelFormatter() != null && hasRangeCursor && hasDomainCursor) { + if (getCursorLabelFormatter() != null && hasRangeCursor && hasDomainCursor) { drawCursorLabel(canvas); } } @@ -765,8 +757,10 @@ protected void drawCursorLabel(Canvas canvas) { // if we are too close to the right edge of the plot, we will move // the label to the left side of our cursor: if (cursorRect.right >= gridRect.right) { - cursorRect.offsetTo(domainCursorPosition - cursorRect.width(), - cursorRect.top); + cursorRect.offsetTo( + domainCursorPosition - cursorRect.width(), + cursorRect.top + ); } // same thing for the top edge of the plot: @@ -780,11 +774,12 @@ protected void drawCursorLabel(Canvas canvas) { } canvas.drawText(label, cursorRect.left, cursorRect.bottom, - getCursorLabelFormatter().getTextPaint()); + getCursorLabelFormatter().getTextPaint() + ); } protected void drawGridBackground(Canvas canvas) { - if(gridBackgroundPaint != null) { + if (gridBackgroundPaint != null) { canvas.drawRect(gridRect, gridBackgroundPaint); } } @@ -793,22 +788,21 @@ protected void drawGridBackground(Canvas canvas) { * Draws lines and points for each element in the series. * * @param canvas - * @throws PlotRenderException */ - protected void drawData(Canvas canvas) throws PlotRenderException { + protected void drawData(Canvas canvas) { if (drawGridOnTop) { drawGridBackground(canvas); } try { - if(isGridClippingEnabled) { - canvas.save(Canvas.ALL_SAVE_FLAG); + if (isGridClippingEnabled) { + canvas.save(); canvas.clipRect(gridRect, android.graphics.Region.Op.INTERSECT); } renderStack.sync(); - for(RenderStack.StackElement thisElement : renderStack.getElements()) { - if(thisElement.isEnabled()) { + for (RenderStack.StackElement thisElement : renderStack.getElements()) { + if (thisElement.isEnabled()) { Class rendererClass = thisElement.get().getFormatter().getRendererClass(); plot.getRenderer(rendererClass).render( @@ -817,7 +811,7 @@ protected void drawData(Canvas canvas) throws PlotRenderException { } } finally { - if(isGridClippingEnabled) { + if (isGridClippingEnabled) { canvas.restore(); } } @@ -844,6 +838,7 @@ public Paint getDomainGridLinePaint() { /** * Set the paint used to draw the domain grid line. + * * @param gridLinePaint */ public void setDomainGridLinePaint(Paint gridLinePaint) { @@ -866,6 +861,7 @@ public Paint getDomainSubGridLinePaint() { /** * Set the paint used to draw the domain grid line. + * * @param gridLinePaint */ public void setDomainSubGridLinePaint(Paint gridLinePaint) { @@ -874,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) { @@ -889,6 +886,7 @@ public Paint getRangeSubGridLinePaint() { /** * Set the Paint used to draw the range grid line. + * * @param gridLinePaint */ public void setRangeSubGridLinePaint(Paint gridLinePaint) { @@ -929,6 +927,7 @@ public void setRangeOriginLinePaint(Paint rangeOriginLinePaint) { /** * Set domain and range cursor position using screen coordinates + * * @param x * @param y */ @@ -939,6 +938,7 @@ public void setCursorPosition(Float x, Float y) { /** * Set domain and range cursor position using screen coordinates + * * @param point */ public void setCursorPosition(PointF point) { @@ -955,6 +955,7 @@ public Number getDomainCursorVal() { /** * Set domain cursor position using screen coordinates + * * @param domainCursorPosition */ public void setDomainCursorPosition(Float domainCursorPosition) { @@ -971,6 +972,7 @@ public Number getRangeCursorVal() { /** * Set range cursor position using screen coordinates + * * @param rangeCursorPosition */ public void setRangeCursorPosition(Float rangeCursorPosition) { @@ -998,9 +1000,8 @@ public Paint getDomainCursorPaint() { } /** - * - * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line. - * Set to null (default) to disable. + * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line. Set to null + * (default) to disable. */ public void setDomainCursorPaint(Paint domainCursorPaint) { this.domainCursorPaint = domainCursorPaint; @@ -1011,9 +1012,8 @@ public Paint getRangeCursorPaint() { } /** - * - * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line. - * Set to null (default) to disable. + * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line. Set to null + * (default) to disable. */ public void setRangeCursorPaint(Paint rangeCursorPaint) { this.rangeCursorPaint = rangeCursorPaint; @@ -1051,8 +1051,8 @@ public void setLineExtensionRight(float lineExtensionRight) { this.lineExtensionRight = lineExtensionRight; } - protected HashMap getDefaultLineLabelStyles() { - HashMap defaults = new HashMap<>(); + protected Map getDefaultLineLabelStyles() { + EnumMap defaults = new EnumMap<>(Edge.class); defaults.put(Edge.TOP, new LineLabelStyle()); defaults.put(Edge.BOTTOM, new LineLabelStyle()); defaults.put(Edge.LEFT, new LineLabelStyle()); @@ -1060,8 +1060,8 @@ protected HashMap getDefaultLineLabelStyles() { return defaults; } - protected HashMap getDefaultLineLabelRenderers() { - HashMap defaults = new HashMap<>(); + protected Map getDefaultLineLabelRenderers() { + EnumMap defaults = new EnumMap<>(Edge.class); defaults.put(Edge.TOP, new LineLabelRenderer()); defaults.put(Edge.BOTTOM, new LineLabelRenderer()); defaults.put(Edge.LEFT, new LineLabelRenderer()); @@ -1103,6 +1103,7 @@ public Insets getGridInsets() { public void setGridInsets(Insets gridInsets) { this.gridInsets = gridInsets; + recalculateSizes(null); } /** @@ -1114,6 +1115,7 @@ public Insets getLineLabelInsets() { public void setLineLabelInsets(Insets lineLabelInsets) { this.lineLabelInsets = lineLabelInsets; + recalculateSizes(null); } public RectF getGridRect() { @@ -1145,22 +1147,20 @@ public boolean isLineLabelEnabled(Edge position) { } public void setLineLabelEdges(Edge... positions) { - Set positionSet = new HashSet<>(); - if(positions != null) { - for(Edge position : positions) { - positionSet.add(position); - } + EnumSet positionSet = EnumSet.noneOf(Edge.class); + if (positions != null) { + Collections.addAll(positionSet, positions); } - setLineLabelEdges(positionSet); + this.lineLabelEdges = positionSet; } - public void setLineLabelEdges(Set positions) { - this.lineLabelEdges = positions; + public void setLineLabelEdges(Collection positions) { + this.lineLabelEdges = EnumSet.copyOf(positions); } protected void setLineLabelEdges(int bitfield) { - for(Edge tp : Edge.values()) { - if((tp.value & bitfield) == tp.value) { + for (Edge tp : Edge.values()) { + if ((tp.value & bitfield) == tp.value) { lineLabelEdges.add(tp); } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java new file mode 100644 index 00000000..51c67159 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java @@ -0,0 +1,28 @@ +package com.androidplot.xy; + +import androidx.annotation.NonNull; + +import com.androidplot.ui.widget.LegendItem; + +public class XYLegendItem implements LegendItem { + + public enum Type { + SERIES, + REGION + } + + public final Type type; + public final Object item; + private final String text; + + public XYLegendItem(@NonNull Type cellType, @NonNull Object item, @NonNull String text) { + this.type = cellType; + this.item = item; + this.text = text; + } + + @Override + public String getTitle() { + return this.text; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java index e5b981c5..680fd4b2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java @@ -17,233 +17,80 @@ package com.androidplot.xy; import android.graphics.*; +import androidx.annotation.NonNull; + import com.androidplot.ui.LayoutManager; import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.Size; import com.androidplot.ui.TableModel; -import com.androidplot.ui.widget.Widget; -import com.androidplot.util.FontUtils; +import com.androidplot.ui.widget.LegendWidget; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Hashtable; +import java.util.List; +import java.util.Map.Entry; /** * Displays a legend for each series added to the owning {@link XYPlot}. */ -public class XYLegendWidget extends Widget { - - /** - * This class is of no use outside of XYLegendWidget. It's just used to alphabetically sort - * Region legend entries. - */ - private static class RegionEntryComparator implements Comparator> { - @Override - public int compare(Map.Entry o1, Map.Entry o2) { - return o1.getValue().compareTo(o2.getValue()); - } - } - - private enum CellType { - SERIES, - REGION - } +public class XYLegendWidget extends LegendWidget { private XYPlot plot; - //private float iconWidth = 12; - private Paint textPaint; - private Paint iconBorderPaint; - private TableModel tableModel; - private boolean drawIconBackgroundEnabled = true; - private boolean drawIconBorderEnabled = true; - - private Size iconSize; - private static final RegionEntryComparator regionEntryComparator = new RegionEntryComparator(); - //private RectF iconRect = new RectF(0, 0, ICON_WIDTH_DEFAULT, ICON_HEIGHT_DEFAULT); - - { - textPaint = new Paint(); - textPaint.setColor(Color.LTGRAY); - textPaint.setAntiAlias(true); - - iconBorderPaint = new Paint(); - iconBorderPaint.setStyle(Paint.Style.STROKE); - //regionEntryComparator = new RegionEntryComparator(); - } public XYLegendWidget(LayoutManager layoutManager, XYPlot plot, Size widgetSize, TableModel tableModel, Size iconSize) { - super(layoutManager, widgetSize); + super(tableModel, layoutManager, widgetSize, iconSize); this.plot = plot; - setTableModel(tableModel); - this.iconSize = iconSize; - } - - public synchronized void setTableModel(TableModel tableModel) { - this.tableModel = tableModel; - } - private RectF getIconRect(RectF cellRect) { - float cellRectCenterY = cellRect.top + (cellRect.height()/2); - RectF iconRect = iconSize.getRectF(cellRect); - - // center the icon rect vertically - float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2); - iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY); - return iconRect; - } - - private static float getRectCenterY(RectF cellRect) { - return cellRect.top + (cellRect.height()/2); - } - - private void beginDrawingCell(Canvas canvas, RectF iconRect) { - - Paint bgPaint = plot.getGraph().getGridBackgroundPaint(); - if(drawIconBackgroundEnabled && bgPaint != null) { - canvas.drawRect(iconRect, bgPaint); - } - } - - private void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, String text) { - - Paint bgPaint = plot.getGraph().getGridBackgroundPaint(); - if(drawIconBorderEnabled && bgPaint != null) { - iconBorderPaint.setColor(bgPaint.getColor()); - canvas.drawRect(iconRect, iconBorderPaint); - } - - float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2); - - if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) { - canvas.drawText(text, iconRect.left - 2, centeredTextOriginY, textPaint); - } else { - canvas.drawText(text, iconRect.right + 2, centeredTextOriginY, textPaint); - } + // Set a default comparator that sorts by type and then alphabetically + setLegendItemComparator(new Comparator() { + @Override + public int compare(XYLegendItem o1, XYLegendItem o2) { + if(o1.type == o2.type) { + return o1.getTitle().compareTo(o2.getTitle()); + } else { + return(o1.type.compareTo(o2.type)); + } + } + }); } protected void drawRegionLegendIcon(Canvas canvas, RectF rect, XYRegionFormatter formatter) { - canvas.drawRect(rect, formatter.getPaint()); - } - - private void drawRegionLegendCell(Canvas canvas, XYRegionFormatter formatter, RectF cellRect, String text) { - RectF iconRect = getIconRect(cellRect); - beginDrawingCell(canvas, iconRect); - - drawRegionLegendIcon( - canvas, - iconRect, - formatter - ); - finishDrawingCell(canvas, cellRect, iconRect, text); + canvas.drawRect(rect, formatter.getPaint()); } - private void drawSeriesLegendCell(Canvas canvas, XYSeriesRenderer renderer, XYSeriesFormatter formatter, RectF cellRect, String seriesTitle) { - RectF iconRect = getIconRect(cellRect); - beginDrawingCell(canvas, iconRect); - - renderer.drawSeriesLegendIcon( - canvas, - iconRect, - formatter); - finishDrawingCell(canvas, cellRect, iconRect, seriesTitle); + @Override + protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull XYLegendItem XYLegendItem) { + switch (XYLegendItem.type) { + case REGION: + drawRegionLegendIcon(canvas, iconRect, (XYRegionFormatter) XYLegendItem.item); + break; + case SERIES: + final XYSeriesFormatter formatter = (XYSeriesFormatter) XYLegendItem.item; + plot.getRenderer(formatter.getRendererClass()).drawSeriesLegendIcon(canvas, iconRect, formatter); + break; + default: + throw new UnsupportedOperationException("Unexpected item type: " + XYLegendItem.type); + } } -// protected List> getLegendEnabledSeriesAndFormatterList() { -// List> sfList = new ArrayList<>(); -// ListIterator> it = plot.getSeriesRegistry().listIterator(); -// while(it.hasNext()) { -// SeriesAndFormatter thisSf = it.next(); -// if(thisSf.getFormatter().isLegendIconEnabled()) { -// sfList.add(thisSf); -// } -// } -// return sfList; -// } - @Override - protected synchronized void doOnDraw(Canvas canvas, RectF widgetRect) { - if(plot.isEmpty()) { - return; + protected List getLegendItems() { + final ArrayList items = new ArrayList<>(); + for (SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) { + items.add(new XYLegendItem(XYLegendItem.Type.SERIES, sfPair.getFormatter(), sfPair.getSeries().getTitle())); } - // Keep an alphabetically sorted list of regions: - TreeSet> sortedRegions = new TreeSet>(new RegionEntryComparator()); - - // Calculate the number of cells needed to draw the Legend: - int seriesCount = plot.getRegistry().size(); - - for(XYSeriesRenderer renderer : plot.getRendererList()) { + for (XYSeriesRenderer renderer : plot.getRendererList()) { Hashtable urf = renderer.getUniqueRegionFormatters(); - sortedRegions.addAll(urf.entrySet()); - } - - seriesCount += sortedRegions.size(); - - // Create an iterator specially created to draw the number of cells we calculated: - Iterator it = tableModel.getIterator(widgetRect, seriesCount); - - RectF cellRect; - - // draw each series legend item: - for(SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) { - //for(SeriesAndFormatter sfPair : plot.getSeriesRegistry()) { - cellRect = it.next(); - XYSeriesFormatter format = sfPair.getFormatter(); - drawSeriesLegendCell(canvas, plot.getRenderer(sfPair.getFormatter().getRendererClass()), - format, cellRect, sfPair.getSeries().getTitle()); - } - - // draw each region legend item: - for(Map.Entry entry : sortedRegions) { - if(!it.hasNext()) { - break; + for (Entry entry : urf.entrySet()) { + items.add(new XYLegendItem(XYLegendItem.Type.REGION, entry.getKey(), entry.getValue())); } - cellRect = it.next(); - XYRegionFormatter formatter = entry.getKey(); - drawRegionLegendCell(canvas, formatter, cellRect, entry.getValue()); } - } - - - public Paint getTextPaint() { - return textPaint; - } - - public void setTextPaint(Paint textPaint) { - this.textPaint = textPaint; - } - - public boolean isDrawIconBackgroundEnabled() { - return drawIconBackgroundEnabled; - } - - public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) { - this.drawIconBackgroundEnabled = drawIconBackgroundEnabled; - } - - public boolean isDrawIconBorderEnabled() { - return drawIconBorderEnabled; - } - - public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) { - this.drawIconBorderEnabled = drawIconBorderEnabled; - } - - public TableModel getTableModel() { - return tableModel; - } - - public Size getIconSize() { - return iconSize; - } - /** - * Set the size of each legend's icon. Note that when using relative sizing, - * the size is calculated against the countaining cell's size, not the plot's size. - * @param iconSize - */ - public void setIconSize(Size iconSize) { - this.iconSize = iconSize; + return items; } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java index 9f72cf18..e519b5a0 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -22,11 +22,18 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; +import androidx.annotation.NonNull; import android.util.AttributeSet; -import com.androidplot.*; -import com.androidplot.ui.*; +import com.androidplot.Plot; +import com.androidplot.R; +import com.androidplot.ui.Anchor; +import com.androidplot.ui.DynamicTableModel; +import com.androidplot.ui.HorizontalPositioning; +import com.androidplot.ui.Size; +import com.androidplot.ui.SizeMode; import com.androidplot.ui.TextOrientation; +import com.androidplot.ui.VerticalPositioning; import com.androidplot.ui.widget.TextLabelWidget; import com.androidplot.util.AttrUtils; import com.androidplot.util.PixelUtils; @@ -41,9 +48,6 @@ */ public class XYPlot extends Plot { - private static final int DEFAULT_LEGEND_WIDGET_H_DP = 10; - private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 7; - private static final int DEFAULT_GRAPH_WIDGET_H_DP = 18; private static final int DEFAULT_GRAPH_WIDGET_W_DP = 10; @@ -53,8 +57,11 @@ public class XYPlot extends Plot min.doubleValue())) ? value : min); @@ -595,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); } @@ -607,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); @@ -626,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); } @@ -639,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); @@ -677,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 { @@ -687,15 +694,15 @@ private double distance(double x, double y) { public void updateDomainMinMaxForOriginModel() { double origin = userDomainOrigin.doubleValue(); - double maxXDelta = distance(bounds.getMaxX().doubleValue(), origin); - double minXDelta = distance(bounds.getMinX().doubleValue(), origin); - double delta = maxXDelta > minXDelta ? maxXDelta : minXDelta; - double dlb = origin - delta; - double dub = origin + delta; + double maxDelta = distance(bounds.getMaxX().doubleValue(), origin); + double minDelta = distance(bounds.getMinX().doubleValue(), origin); + double delta = maxDelta > minDelta ? maxDelta : minDelta; + double lowerBoundary = origin - delta; + double upperBoundary = origin + delta; switch (domainOriginBoundaryMode) { case AUTO: - bounds.setMinX(dlb); - bounds.setMaxX(dub); + bounds.setMinX(lowerBoundary); + bounds.setMaxX(upperBoundary); break; // if fixed, then the value already exists within "user" vals. @@ -703,28 +710,28 @@ public void updateDomainMinMaxForOriginModel() { break; case GROW: { - if (prevMinX == null || dlb < prevMinX.doubleValue()) { - bounds.setMinX(dlb); + if (prevMinX == null || lowerBoundary < prevMinX.doubleValue()) { + bounds.setMinX(lowerBoundary); } else { bounds.setMinX(prevMinX); } - if (prevMaxX == null || dub > prevMaxX.doubleValue()) { - bounds.setMaxX(dub); + if (prevMaxX == null || upperBoundary > prevMaxX.doubleValue()) { + bounds.setMaxX(upperBoundary); } else { bounds.setMaxX(prevMaxX); } } break; case SHRINK: - if (prevMinX == null || dlb > prevMinX.doubleValue()) { - bounds.setMinX(dlb); + if (prevMinX == null || lowerBoundary > prevMinX.doubleValue()) { + bounds.setMinX(lowerBoundary); } else { bounds.setMinX(prevMinX); } - if (prevMaxX == null || dub < prevMaxX.doubleValue()) { - bounds.setMaxX(dub); + if (prevMaxX == null || upperBoundary < prevMaxX.doubleValue()) { + bounds.setMaxX(upperBoundary); } else { bounds.setMaxX(prevMaxX); } @@ -738,14 +745,14 @@ public void updateRangeMinMaxForOriginModel() { switch (rangeOriginBoundaryMode) { case AUTO: double origin = userRangeOrigin.doubleValue(); - double maxYDelta = distance(bounds.getMaxY().doubleValue(), origin); - double minYDelta = distance(bounds.getMinY().doubleValue(), origin); - if (maxYDelta > minYDelta) { - bounds.setMinY(origin - maxYDelta); - bounds.setMaxY(origin + maxYDelta); + double maxDelta = distance(bounds.getMaxY().doubleValue(), origin); + double minDelta = distance(bounds.getMinY().doubleValue(), origin); + if (maxDelta > minDelta) { + bounds.setMinY(origin - maxDelta); + bounds.setMaxY(origin + maxDelta); } else { - bounds.setMinY(origin - minYDelta); - bounds.setMaxY(origin + minYDelta); + bounds.setMinY(origin - minDelta); + bounds.setMaxY(origin + minDelta); } break; case FIXED: @@ -1036,12 +1043,12 @@ public synchronized void setUserRangeOrigin(Number origin) { } @SuppressWarnings("SameParameterValue") - protected void setDomainFramingModel(XYFramingModel model) { + protected void setDomainFramingModel(@NonNull XYFramingModel model) { constraints.setDomainFramingModel(model); } @SuppressWarnings("SameParameterValue") - protected void setRangeFramingModel(XYFramingModel model) { + protected void setRangeFramingModel(@NonNull XYFramingModel model) { constraints.setRangeFramingModel(model); } @@ -1089,9 +1096,7 @@ public YValueMarker removeMarker(YValueMarker marker) { * @return */ public int removeMarkers() { - int removed = removeXMarkers(); - removed += removeYMarkers(); - return removed; + return removeXMarkers() + removeYMarkers(); } /** diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java index b0a3186f..0492c6c2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java @@ -43,7 +43,11 @@ public class XYRegionFormatter { public XYRegionFormatter(Context ctx, int xmlCfgId) { // prevent configuration of classes derived from this one: if (getClass().equals(XYRegionFormatter.class)) { - Fig.configure(ctx, this, xmlCfgId); + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java index 0c18d2e9..27924ab1 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java @@ -16,7 +16,7 @@ package com.androidplot.xy; -import android.content.*; +import android.content.Context; import com.androidplot.ui.Formatter; import com.androidplot.util.LayerHash; @@ -28,7 +28,7 @@ public abstract class XYSeriesFormatter - + + - + @@ -48,36 +60,35 @@ - + - - - + + + - + - + - + - + @@ -190,7 +201,7 @@ - + @@ -267,180 +278,857 @@ - + + + + + + + + + + + + + + + + + - + - + - + + - + + - - + + + + + + + + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + - + - + + + + - - + + + + + +Enable line labels on one or more edge of the graph. For example, to enable labels on the left +and bottom edges: +``` +ap:lineLabels="left|bottom" +``` +--> + +Text alignment of line labels drawn on top edge of the plot. This alignment is applied relative +to the line label insets defined by `lineLabelInsetTop`. +--> + + - + +Text alignment of line labels drawn on right edge of the plot. This alignment is applied relative +to the line label insets defined by `lineLabelInsetRight`. +--> + + + + + - + + + + + + + + - + + + + - - - - - + + + + + + + + + + + + - - + - - + - - + + + + + + + + + + + + + + + + - - + + - - + + + \ No newline at end of file diff --git a/androidplot-core/src/test/java/com/androidplot/PlotTest.java b/androidplot-core/src/test/java/com/androidplot/PlotTest.java index 6c013ea6..9e4753bf 100644 --- a/androidplot-core/src/test/java/com/androidplot/PlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/PlotTest.java @@ -20,11 +20,11 @@ import android.graphics.*; import android.util.*; -import com.androidplot.exception.PlotRenderException; import com.androidplot.test.*; import com.androidplot.ui.*; import com.halfhp.fig.*; import org.junit.Test; +import org.mockito.Mock; import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; import java.util.HashMap; @@ -33,138 +33,20 @@ import static junit.framework.Assert.assertNotSame; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PlotTest extends AndroidplotTest { - static class MockPlotListener implements PlotListener { - - public void onBeforeDraw(Plot source, Canvas canvas) {} - - public void onAfterDraw(Plot source, Canvas canvas) {} - } - - static class MockSeries implements Series { - - public String getTitle() { - return null; - } - - } - - static class MockSeries2 implements Series { - - public String getTitle() { - return null; - } - } - - static class MockSeries3 implements Series { - - public String getTitle() { - return null; - } - } - - static class MockRenderer1 extends SeriesRenderer { - - public MockRenderer1(Plot plot) { - super(plot); - } - - @Override - public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) throws PlotRenderException { - - } - - @Override - public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) { - - } - } - static class MockRenderer2 extends SeriesRenderer { - - public MockRenderer2(Plot plot) { - super(plot); - } - - @Override - public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) throws PlotRenderException { - - } - - @Override - public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) { - - } - } - - static class MockFormatter1 extends Formatter { - - @Override - public Class getRendererClass() { - return MockRenderer1.class; - } - - @Override - public SeriesRenderer doGetRendererInstance(MockPlot plot) { - return new MockRenderer1(plot); - } - } - - static class MockFormatter2 extends Formatter { - - @Override - public Class getRendererClass() { - return MockRenderer2.class; - } - - @Override - public SeriesRenderer doGetRendererInstance(MockPlot plot) { - return new MockRenderer2(plot); - } - } - - public static class MockSeriesBundle extends SeriesBundle { - - public MockSeriesBundle(MockSeries series, Formatter formatter) { - super(series, formatter); - } - } - - public static class MockPlot extends Plot> { - public MockPlot(String title) { - super(RuntimeEnvironment.application, title); - } - - @Override - protected SeriesRegistry getRegistryInstance() { - return new SeriesRegistry() { - @Override - protected MockSeriesBundle newSeriesBundle( - MockSeries series, Formatter formatter) { - return new MockSeriesBundle(series, formatter); - } - }; - } - - @Override - protected void onPreInit() { - - } - - @Override - protected void processAttrs(TypedArray attrs) { - - } - } + @Mock + SeriesRegistry mockSeriesRegistry; @Test - public void testInit_withoutAttrs() throws Exception { - Plot plot = mock(Plot.class); + public void testInit_withoutAttrs() { + //Plot plot = mock(Plot.class); + Plot plot = spy(new MockPlot("MockPlot")); plot.init(RuntimeEnvironment.application, null, 0); verify(plot, times(1)).onPreInit(); @@ -172,15 +54,17 @@ public void testInit_withoutAttrs() throws Exception { } @Test - public void testInit_withAttrs() throws Exception { - Plot plot = new MockPlot("MockPlot"); + public void testInit_withAttrs() { + Plot plot = spy(new MockPlot("MockPlot")); AttributeSet attrs = mock(AttributeSet.class); plot.init(RuntimeEnvironment.application, attrs, 0); - // TODO: verifications + + verify(plot, times(1)).onPreInit(); + verify(plot, times(1)).onAfterConfig(); } @Test - public void testAddSeries() throws Exception { + public void testAddSeries() { Plot plot = new MockPlot("MockPlot"); MockSeries m1 = new MockSeries(); @@ -213,7 +97,7 @@ public void testAddSeries() throws Exception { } @Test - public void testRemoveSeries() throws Exception { + public void testRemoveSeries() { Plot plot = new MockPlot("MockPlot"); @@ -273,7 +157,7 @@ public void testRemoveSeries() throws Exception { @Test - public void testGetFormatter() throws Exception { + public void testGetFormatter() { Plot plot = new MockPlot("MockPlot"); MockSeries m1 = new MockSeries(); @@ -301,7 +185,7 @@ public void testGetFormatter() throws Exception { } @Test - public void testGetRendererList() throws Exception { + public void testGetRendererList() { Plot plot = new MockPlot("MockPlot"); @@ -322,7 +206,7 @@ public void testGetRendererList() throws Exception { } @Test - public void testAddListener() throws Exception { + public void testAddListener() { Plot plot = new MockPlot("MockPlot"); ArrayList listeners = plot.getListeners(); @@ -344,11 +228,11 @@ public void testAddListener() throws Exception { plot.addListener(pl2); assertEquals(2, listeners.size()); - + } @Test - public void testRemoveListener() throws Exception { + public void testRemoveListener() { Plot plot = new MockPlot("MockPlot"); ArrayList listeners = plot.getListeners(); @@ -397,4 +281,188 @@ public void testConfigure() throws Exception { assertEquals(Plot.RenderMode.USE_BACKGROUND_THREAD, plot.getRenderMode()); assertEquals(Color.parseColor(param3), plot.getBackgroundPaint().getColor()); } + + @Test + public void setTitle_setsTitle() { + Plot plot = new MockPlot("foo"); + plot.setTitle("bar"); + assertEquals("bar", plot.getTitle().getText()); + } + + @Test + public void clear_unregistersAllPlotListeners() { + Plot plot = new MockPlot("MockPlot"); + plot.addSeries(new MockSeries(), new MockFormatter1()); + plot.addSeries(new MockSeries(), new MockFormatter1()); + plot.addSeries(new MockSeries(), new MockFormatter1()); + assertEquals(3, plot.getListeners().size()); + + plot.clear(); + assertEquals(0, plot.getListeners().size()); + } + + @Test + public void clear_clearsRegistry() { + Plot plot = new MockPlot("MockPlot"); + plot.setRegistry(mockSeriesRegistry); + + plot.clear(); + verify(mockSeriesRegistry).clear(); + } + + @Test + public void setPlotMargins_updatesMargins() { + Plot plot = new MockPlot("MockPlot"); + plot.setPlotMargins(11, 22, 33, 44); + + assertEquals(11f, plot.getPlotMarginLeft()); + assertEquals(22f, plot.getPlotMarginTop()); + assertEquals(33f, plot.getPlotMarginRight()); + assertEquals(44f, plot.getPlotMarginBottom()); + } + + @Test + public void setPlotPadding_updatesPadding() { + Plot plot = new MockPlot("MockPlot"); + plot.setPlotPadding(11, 22, 33, 44); + + assertEquals(11f, plot.getPlotPaddingLeft()); + assertEquals(22f, plot.getPlotPaddingTop()); + assertEquals(33f, plot.getPlotPaddingRight()); + assertEquals(44f, plot.getPlotPaddingBottom()); + } + + static class MockPlotListener implements PlotListener { + + public void onBeforeDraw(Plot source, Canvas canvas) { + } + + public void onAfterDraw(Plot source, Canvas canvas) { + } + } + + static class MockSeries implements Series, PlotListener { + + public String getTitle() { + return null; + } + + @Override + public void onBeforeDraw(Plot source, Canvas canvas) { + + } + + @Override + public void onAfterDraw(Plot source, Canvas canvas) { + + } + } + + static class MockSeries2 implements Series { + + public String getTitle() { + return null; + } + } + + static class MockSeries3 implements Series { + + public String getTitle() { + return null; + } + } + + static class MockRenderer1 extends SeriesRenderer { + + public MockRenderer1(Plot plot) { + super(plot); + } + + @Override + public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) { + + } + + @Override + public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) { + + } + } + + static class MockRenderer2 extends SeriesRenderer { + + public MockRenderer2(Plot plot) { + super(plot); + } + + @Override + public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) { + + } + + @Override + public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) { + + } + } + + static class MockFormatter1 extends Formatter { + + @Override + public Class getRendererClass() { + return MockRenderer1.class; + } + + @Override + public SeriesRenderer doGetRendererInstance(MockPlot plot) { + return new MockRenderer1(plot); + } + } + + static class MockFormatter2 extends Formatter { + + @Override + public Class getRendererClass() { + return MockRenderer2.class; + } + + @Override + public SeriesRenderer doGetRendererInstance(MockPlot plot) { + return new MockRenderer2(plot); + } + } + + public static class MockSeriesBundle extends SeriesBundle { + + public MockSeriesBundle(MockSeries series, Formatter formatter) { + super(series, formatter); + } + } + + public static class MockPlot extends Plot> { + public MockPlot(String title) { + super(RuntimeEnvironment.application, title); + } + + @Override + protected SeriesRegistry getRegistryInstance() { + return new SeriesRegistry() { + @Override + protected MockSeriesBundle newSeriesBundle( + MockSeries series, Formatter formatter) { + return new MockSeriesBundle(series, formatter); + } + }; + } + + @Override + protected void onPreInit() { + + } + + @Override + protected void processAttrs(TypedArray attrs) { + + } + } } diff --git a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java index dba4ce43..e504582a 100644 --- a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java @@ -110,7 +110,7 @@ public void testOnRender() throws Exception { } @Test - public void testGetContainingSegment() throws Exception { + public void getContainingSegment_returnsCorrectSegment() throws Exception { Segment segment1 = spy(new Segment("s1", 25)); Segment segment2 = spy(new Segment("s2", 25)); Segment segment3 = spy(new Segment("s3", 25)); @@ -150,6 +150,45 @@ public void testGetContainingSegment() throws Exception { assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0))); } + @Test + public void getContainingSegment_handlesSegmentsLargerThanHalfPie() throws Exception { + Segment segment1 = spy(new Segment("s1", 25)); + Segment segment2 = spy(new Segment("s2", 24)); + Segment segment3 = spy(new Segment("s3", 51)); + SegmentFormatter formatter = spy( + new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN)); + PieRenderer renderer = formatter.getRendererInstance(pieChart); + + pieChart.addSegment(segment1, formatter); + pieChart.addSegment(segment2, formatter); + pieChart.addSegment(segment3, formatter); + + // southeast + assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 100))); + + // southwest + assertEquals(segment2, renderer.getContainingSegment(new PointF(0, 100))); + + // northwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0))); + + // northeast + assertEquals(segment3, renderer.getContainingSegment(new PointF(100, 0))); + + renderer.setStartDegs(90); + // southeast + assertEquals(segment2, renderer.getContainingSegment(new PointF(100, 100))); + + // southwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 100))); + + // northwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0))); + + // northeast + assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0))); + } + @Test public void testDegsToScreenDegs() throws Exception { assertEquals(0f, PieRenderer.degsToScreenDegs(0)); diff --git a/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java index ecb78333..1f8f1457 100644 --- a/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java +++ b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java @@ -56,11 +56,11 @@ public static XYSeries generateXYSeriesWithNulls(String title, int size) { */ @SuppressLint("NewApi") public static MotionEvent newPointerDownEvent(int finger1x, int finger1y, int finger2x, int finger2y) { - MotionEvent me = MotionEvent.obtain(0, 0, 0, 0, 0, 0); + MotionEvent me = MotionEvent.obtain(0, 0, MotionEvent.ACTION_POINTER_DOWN, finger1x, finger1y, 0); ShadowMotionEvent sme = shadowOf(me); - sme.setAction(MotionEvent.ACTION_POINTER_DOWN); + //sme.setAction(MotionEvent.ACTION_POINTER_DOWN); sme.setPointerIds(0, 1); - sme.setLocation(finger1x, finger1y); + //sme.setLocation(finger1x, finger1y); sme.setPointer2(finger2x, finger2y); return me; } diff --git a/androidplot-core/src/test/java/com/androidplot/ui/RenderStackTest.java b/androidplot-core/src/test/java/com/androidplot/ui/RenderStackTest.java new file mode 100644 index 00000000..59bab380 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/ui/RenderStackTest.java @@ -0,0 +1,52 @@ +package com.androidplot.ui; + +import com.androidplot.Plot; +import com.androidplot.SeriesRegistry; +import com.androidplot.test.AndroidplotTest; +import com.androidplot.xy.LineAndPointFormatter; +import com.androidplot.xy.LineAndPointRenderer; +import com.androidplot.xy.XYSeries; +import com.google.common.collect.Lists; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RenderStackTest extends AndroidplotTest { + + @Mock Plot plot; + @Mock SeriesRegistry seriesRegistry; + + @Before + public void before() { + when(plot.getRegistry()).thenReturn(seriesRegistry); + } + + @Test + public void disable_disablesElements_ofSpecifiedRendererOnly() { + final SeriesBundle s1 = new SeriesBundle(mock(XYSeries.class), + new LineAndPointFormatter()); + final SeriesBundle s2 = new SeriesBundle(mock(XYSeries.class), + new LineAndPointFormatter()); + when(seriesRegistry.getSeriesAndFormatterList()).thenReturn(Lists.newArrayList(s1, s2)); + RenderStack renderStack = new RenderStack<>(plot); + + renderStack.sync(); + assertEquals(2, renderStack.getElements().size()); + for(RenderStack.StackElement element : renderStack.getElements()) { + assertTrue(element.isEnabled()); + } + + renderStack.disable(LineAndPointRenderer.class); + assertEquals(2, renderStack.getElements().size()); + for(RenderStack.StackElement element : renderStack.getElements()) { + assertFalse(element.isEnabled()); + } + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/ui/widget/TextLabelWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/ui/widget/TextLabelWidgetTest.java new file mode 100644 index 00000000..bdb48f92 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/ui/widget/TextLabelWidgetTest.java @@ -0,0 +1,82 @@ +package com.androidplot.ui.widget; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import com.androidplot.test.AndroidplotTest; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.Size; +import com.androidplot.ui.TextOrientation; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class TextLabelWidgetTest extends AndroidplotTest { + + @Mock + LayoutManager layoutManager; + + @Mock + Size size; + + @Mock + Canvas canvas; + + @Mock + RectF rectF; + + private TextLabelWidget textLabelWidget; + + @Before + public void before() { + textLabelWidget = spy(new TextLabelWidget(layoutManager, size)); + } + + @Test + public void onMetricsChanged_invokesPack_ifAutopackEnabled() { + textLabelWidget.setAutoPackEnabled(true); + textLabelWidget.onMetricsChanged(size, size); + verify(textLabelWidget, times(2)).pack(); + } + + @Test + public void onMetricsChanged_doesNotInvokePack_ifAutopackDisabled() { + textLabelWidget.setAutoPackEnabled(false); + textLabelWidget.onMetricsChanged(size, size); + verify(textLabelWidget, never()).pack(); + } + + @Test + public void doOnDraw_rotatesThenDraws_ifVerticalAscending() { + textLabelWidget.setText("this is a test"); + textLabelWidget.setOrientation(TextOrientation.VERTICAL_ASCENDING); + textLabelWidget.doOnDraw(canvas, rectF); + + InOrder inOrder = inOrder(canvas); + inOrder.verify(canvas).rotate(-90); + inOrder.verify(canvas).drawText(eq(textLabelWidget.getText()), anyFloat(), anyFloat(), any(Paint.class)); + } + + @Test + public void doOnDraw_rotatesThenDraws_ifVerticalDescending() { + textLabelWidget.setText("this is a test"); + textLabelWidget.setOrientation(TextOrientation.VERTICAL_DESCENDING); + textLabelWidget.doOnDraw(canvas, rectF); + + InOrder inOrder = inOrder(canvas); + inOrder.verify(canvas).rotate(90); + inOrder.verify(canvas).drawText(eq(textLabelWidget.getText()), anyFloat(), anyFloat(), any(Paint.class)); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/ui/widget/WidgetTest.java b/androidplot-core/src/test/java/com/androidplot/ui/widget/WidgetTest.java new file mode 100644 index 00000000..cae6c056 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/ui/widget/WidgetTest.java @@ -0,0 +1,113 @@ +package com.androidplot.ui.widget; + +import android.graphics.Canvas; +import android.graphics.RectF; +import androidx.annotation.NonNull; + +import com.androidplot.test.AndroidplotTest; +import com.androidplot.ui.Anchor; +import com.androidplot.ui.HorizontalPositioning; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.PositionMetrics; +import com.androidplot.ui.Size; +import com.androidplot.ui.SizeMetric; +import com.androidplot.ui.SizeMode; +import com.androidplot.ui.VerticalPositioning; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class WidgetTest extends AndroidplotTest { + + @Mock + LayoutManager layoutManager; + + @Mock + Size size; + + @Mock + Canvas canvas; + + Widget widget; + + PositionMetrics positionMetrics; + + @Before + public void before() { + + positionMetrics = new PositionMetrics( + 0, HorizontalPositioning.ABSOLUTE_FROM_LEFT, + 0, VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.LEFT_TOP); + when(size.getHeight()).thenReturn(new SizeMetric(100, SizeMode.ABSOLUTE)); + when(size.getWidth()).thenReturn(new SizeMetric(100, SizeMode.ABSOLUTE)); + widget = spy(new TestWidget(layoutManager, size)); + widget.setPositionMetrics(positionMetrics); + widget.refreshLayout(); + } + + @Test + public void setGetAnchor_setsAndGetsAnchor() { + widget.setAnchor(Anchor.LEFT_TOP); + assertEquals(Anchor.LEFT_TOP, widget.getAnchor()); + + widget.setAnchor(Anchor.RIGHT_BOTTOM); + assertEquals(Anchor.RIGHT_BOTTOM, widget.getAnchor()); + } + + @Test + public void position_withoutAnchor_updatesPositionWithExistingAnchor() { + final Anchor existingAnchor = widget.getAnchor(); + widget.position(1, HorizontalPositioning.ABSOLUTE_FROM_RIGHT, + 1, VerticalPositioning.ABSOLUTE_FROM_BOTTOM); + + assertEquals(existingAnchor, widget.getAnchor()); + assertEquals(1F, widget.getPositionMetrics().getYPositionMetric().getValue()); + assertEquals(1F, widget.getPositionMetrics().getXPositionMetric().getValue()); + } + + @Test + public void position_withAnchor_updatesPositionWithNewAnchor() { + final Anchor newAnchor = Anchor.CENTER; + widget.position(1, HorizontalPositioning.ABSOLUTE_FROM_RIGHT, + 1, VerticalPositioning.ABSOLUTE_FROM_BOTTOM, newAnchor); + + assertEquals(newAnchor, widget.getAnchor()); + assertEquals(1F, widget.getPositionMetrics().getYPositionMetric().getValue()); + assertEquals(1F, widget.getPositionMetrics().getXPositionMetric().getValue()); + } + + @Test + public void draw_sizeChanged_invokesOnResizeBeforeDoOnDraw() { + InOrder inOrder = Mockito.inOrder(widget); + + widget.draw(canvas); + + inOrder.verify(widget).onResize(isNull(), any(RectF.class)); + inOrder.verify(widget).doOnDraw(eq(canvas), any(RectF.class)); + verify(widget).onResize(isNull(), any(RectF.class)); + } + + static class TestWidget extends Widget { + + public TestWidget(@NonNull LayoutManager layoutManager, @NonNull Size size) { + super(layoutManager, size); + } + + @Override + protected void doOnDraw(Canvas canvas, RectF widgetRect) { + // nothing to do + } + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java b/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java new file mode 100644 index 00000000..8bca2a38 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java @@ -0,0 +1,136 @@ +package com.androidplot.util; + +import android.annotation.SuppressLint; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@SuppressWarnings({"UnnecessaryBoxing", "ObjectEqualsNull", "EqualsBetweenInconvertibleTypes", "EqualsWithItself", "NumberEquality"}) +@SuppressLint("UseValueOf") +public class FastNumberTest { + + private static final Number[] NUMBERS = new Number[]{ + new Integer(0), + new Integer(1), + new Integer(-1), + new Integer(Integer.MAX_VALUE), + new Integer(Integer.MIN_VALUE), + + new Long(0), + new Long(1), + new Long(-1), + new Long(Long.MAX_VALUE), + new Long(Long.MIN_VALUE), + + new Double(0), + new Double(1), + new Double(-1), + new Double(87654321.12345d), + new Double(0.0002f), + new Double(Double.MAX_VALUE), + new Double(Double.MIN_VALUE), + + new Float(0), + new Float(1.0f), + new Float(-1.0f), + new Float(8765.4321f), + new Float(1098.576f), + new Float(Float.MIN_VALUE), + new Float(Float.MAX_VALUE), + }; + + private static final Number[] NUMBERS_CLONE = new Number[]{ + new Integer(0), + new Integer(1), + new Integer(-1), + new Integer(Integer.MAX_VALUE), + new Integer(Integer.MIN_VALUE), + + new Long(0), + new Long(1), + new Long(-1), + new Long(Long.MAX_VALUE), + new Long(Long.MIN_VALUE), + + new Double(0), + new Double(1), + new Double(-1), + new Double(87654321.12345d), + new Double(0.0002f), + new Double(Double.MAX_VALUE), + new Double(Double.MIN_VALUE), + + new Float(0), + new Float(1.0f), + new Float(-1.0f), + new Float(8765.4321f), + new Float(1098.576f), + new Float(Float.MIN_VALUE), + new Float(Float.MAX_VALUE), + }; + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void orNull_returnsNull_ifNullNumber() { + //noinspection ConstantConditions + assertNull(FastNumber.orNull(null)); + } + + @Test + public void equals_returnsTrue_ifFromSameNumberInstance() { + for (Number number : NUMBERS) { + assertTrue("Equality test failed on " + number, FastNumber.orNull(number).equals(FastNumber.orNull(number))); + } + } + + @Test + public void equals_returnsTrue_ifFromSameNumber() { + assertEquals("misconfigured test values", NUMBERS.length, NUMBERS_CLONE.length); + for (int i = 0; i < NUMBERS.length; i++) { + assertEquals("misconfigured test values", NUMBERS[i], NUMBERS_CLONE[i]); + assertTrue(FastNumber.orNull(NUMBERS[i]).equals(FastNumber.orNull(NUMBERS_CLONE[i]))); + } + } + + @Test + public void equals_returnsFalse_ifNumberIsDifferent() { + for (int i = 0; i < NUMBERS.length; i++) { + for (int j = 0; j < NUMBERS.length; j++) { + if (j == i) { + continue; + } + assertNotEquals("duplicate test values at index " + i + " and " + j, NUMBERS[i], NUMBERS[j]); + assertFalse(FastNumber.orNull(NUMBERS[i]).equals(FastNumber.orNull(NUMBERS[j]))); + } + } + } + + @Test + public void hashCode_isEqual_ifInstanceIsEqual() { + for (Number number : NUMBERS) { + for (Number number2 : NUMBERS) { + FastNumber fastNumber1 = FastNumber.orNull(number); + FastNumber fastNumber2 = FastNumber.orNull(number2); + if (fastNumber1.equals(fastNumber2)) { + assertEquals(fastNumber1.hashCode(), fastNumber2.hashCode()); + } + } + } + } + +} \ No newline at end of file diff --git a/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java b/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java index 3d479495..3dbd6970 100644 --- a/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java +++ b/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java @@ -35,33 +35,32 @@ public void setUp() throws Exception { layerHash.addToBottom(obj2, obj2); layerHash.addToBottom(obj3, obj3); - assertEquals(obj3, layerHash.getKeysAsList().get(0)); - assertEquals(obj2, layerHash.getKeysAsList().get(1)); - assertEquals(obj1, layerHash.getKeysAsList().get(2)); + assertEquals(obj1, layerHash.getTop()); + assertEquals(obj3, layerHash.getBottom()); } // "top" == last element in the list: @Test - public void testMoveUp() throws Exception { + public void moveUp() { layerHash.moveUp(obj3); - assertEquals(obj2, layerHash.getKeysAsList().get(0)); - assertEquals(obj3, layerHash.getKeysAsList().get(1)); - assertEquals(obj1, layerHash.getKeysAsList().get(2)); + assertEquals(obj1, layerHash.getTop()); + assertEquals(obj3, layerHash.getBeneath(obj1)); + assertEquals(obj2, layerHash.getBeneath(obj3)); layerHash.moveUp(obj3); - assertEquals(obj2, layerHash.getKeysAsList().get(0)); - assertEquals(obj1, layerHash.getKeysAsList().get(1)); - assertEquals(obj3, layerHash.getKeysAsList().get(2)); + assertEquals(obj3, layerHash.getTop()); + assertEquals(obj1, layerHash.getBeneath(obj3)); + assertEquals(obj2, layerHash.getBeneath(obj1)); layerHash.moveUp(obj3); - assertEquals(obj2, layerHash.getKeysAsList().get(0)); - assertEquals(obj1, layerHash.getKeysAsList().get(1)); - assertEquals(obj3, layerHash.getKeysAsList().get(2)); + assertEquals(obj3, layerHash.getTop()); + assertEquals(obj1, layerHash.getBeneath(obj3)); + assertEquals(obj2, layerHash.getBeneath(obj1)); } // "bottom" == first element in the list: @Test - public void testMoveDown() throws Exception { + public void moveDown() { layerHash.moveDown(obj1); assertEquals(obj3, layerHash.getKeysAsList().get(0)); assertEquals(obj1, layerHash.getKeysAsList().get(1)); @@ -79,7 +78,7 @@ public void testMoveDown() throws Exception { } @Test - public void testMoveAbove() throws Exception { + public void moveAbove() { layerHash.moveAbove(obj2, obj1); assertEquals(obj3, layerHash.getKeysAsList().get(0)); assertEquals(obj1, layerHash.getKeysAsList().get(1)); @@ -87,11 +86,45 @@ public void testMoveAbove() throws Exception { } @Test - public void testMoveBeneath() throws Exception { + public void moveBeneath() { layerHash.moveBeneath(obj1, obj2); assertEquals(obj3, layerHash.getKeysAsList().get(0)); assertEquals(obj1, layerHash.getKeysAsList().get(1)); assertEquals(obj2, layerHash.getKeysAsList().get(2)); } + @Test + public void addToTop() { + Object obj = new Object(); + layerHash.addToTop(obj, obj); + assertEquals(obj, layerHash.getTop()); + assertEquals(obj3, layerHash.getKeysAsList().get(0)); + assertEquals(obj2, layerHash.getKeysAsList().get(1)); + assertEquals(obj1, layerHash.getKeysAsList().get(2)); + assertEquals(obj, layerHash.getKeysAsList().get(3)); + } + + @Test + public void moveToTop() { + layerHash.moveToTop(obj3); + assertEquals(obj3, layerHash.getTop()); + assertEquals(obj1, layerHash.getBeneath(obj3)); + assertEquals(obj2, layerHash.getBeneath(obj1)); + } + + @Test + public void moveToBottom() { + layerHash.moveToBottom(obj1); + assertEquals(obj1, layerHash.getBottom()); + assertEquals(obj3, layerHash.getAbove(obj1)); + assertEquals(obj2, layerHash.getAbove(obj3)); + } + + @Test + public void remove() { + layerHash.remove(obj2); + assertEquals(2, layerHash.size()); + assertEquals(obj1, layerHash.getAbove(obj3)); + } + } diff --git a/androidplot-core/src/test/java/com/androidplot/util/PlotStatisticsTest.java b/androidplot-core/src/test/java/com/androidplot/util/PlotStatisticsTest.java new file mode 100644 index 00000000..4454c69c --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/util/PlotStatisticsTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot.util; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.androidplot.Plot; +import com.androidplot.test.AndroidplotTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class PlotStatisticsTest extends AndroidplotTest { + + @Mock + Canvas canvas; + + @Mock + Paint paint; + + @Mock + Plot plot; + + @InjectMocks + PlotStatistics ps = new PlotStatistics(1, true); + + @Before + public void before() { + when(plot.getDisplayDimensions()).thenReturn(new DisplayDimensions()); + } + + @Test + public void annotatePlot_annotates_ifEnabled() { + ps.onAfterDraw(plot, canvas); + verify(canvas).drawText(anyString(), anyFloat(), anyFloat(), any(Paint.class)); + } + + @Test + public void annotatePlot_doesNotAnnotate_ifDisabled() { + ps.setEnabled(false); + ps.onAfterDraw(plot, canvas); + verify(canvas, never()).drawText(anyString(), anyFloat(), anyFloat(), any(Paint.class)); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java index 83a7eb81..df9014cd 100644 --- a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java +++ b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java @@ -27,6 +27,7 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.fail; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class SeriesUtilsTest { @@ -50,7 +51,7 @@ public void tearDown() throws Exception { } @Test - public void testSeriesMinMax() { + public void minMax_onSimpleXYSeries_calculatesExpectedRegion() { SimpleXYSeries series = new SimpleXYSeries(LINEAR, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); RectRegion minMax = SeriesUtils.minMax(series); assertEquals(0, minMax.getMinX().doubleValue(), 0); @@ -102,7 +103,68 @@ public void testSeriesMinMax() { } @Test - public void testListMinMax() { + public void minMax_onFastXYSeries_usesSeriesMinMax() { + FastXYSeries series = mock(FastXYSeries.class); + SeriesUtils.minMax(series); + verify(series).minMax(); + } + + @Test + public void minMax_calculatesExpectedRegion_onFastXYSeriesWithLayoutConstraints() { + FastXYSeries series = new FastXYSeries() { + + // create a couple arrays of y-values to plot: + final ArrayList times = new ArrayList<>(); + final ArrayList values = new ArrayList<>(); + + { + for (int i = 0; i < 10; i++) { + times.add(i); + values.add(i); + } + } + + @Override + public int size() { + return times.size(); + } + + @Override + public Number getX(int index) { + return times.get(index); + } + + @Override + public Number getY(int index) { + return values.get(index); + } + + @Override + public String getTitle() { + return "Isaac's crazy thing"; + } + + @Override + public RectRegion minMax() { + return new RectRegion(0, 10, 0, 10); + } + }; + + final XYConstraints constraints = new XYConstraints(); + constraints.setDomainLowerBoundaryMode(BoundaryMode.FIXED); + constraints.setDomainUpperBoundaryMode(BoundaryMode.FIXED); + constraints.setMinX(5); + constraints.setMaxX(9); + + final RectRegion result = SeriesUtils.minMax(constraints, series); + assertEquals(5d, result.getMinX().doubleValue()); + assertEquals(9d, result.getMaxX().doubleValue()); + assertEquals(5d, result.getMinY().doubleValue()); + assertEquals(9d, result.getMaxY().doubleValue()); + } + + @Test + public void minMax_onSeriesList_producesAggregateResult() { Region minMax = SeriesUtils.minMax(LINEAR); assertEquals(1, minMax.getMin().doubleValue(), 0); assertEquals(8, minMax.getMax().doubleValue(), 0); @@ -133,7 +195,7 @@ public void testListMinMax() { } @Test - public void testGetNullRegion() { + public void getNullRegion_producesExpectedResult() { XYSeries s1 = new SimpleXYSeries( SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "s1", 0, 0, // 0 @@ -168,7 +230,7 @@ public void testGetNullRegion() { } @Test - public void testIboundsMin() { + public void iBoundsMin_findsMin() { XYSeries s1 = new SimpleXYSeries( Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), @@ -195,7 +257,7 @@ public void testIboundsMin() { } @Test - public void testIboundsMax() { + public void iBoundsMax_findsMax() { XYSeries s1 = new SimpleXYSeries( Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), @@ -222,7 +284,7 @@ public void testIboundsMax() { } @Test - public void testIbounds() { + public void iBounds_findsMinMax() { FastXYSeries series = mock(FastXYSeries.class); when(series.size()).thenReturn(3); when(series.getX(0)).thenReturn(0); diff --git a/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java index 647882c5..b1600828 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/AdvanceLineAndPointRendererTest.java @@ -24,7 +24,6 @@ import org.junit.*; import org.mockito.*; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; /** diff --git a/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java index 2bb317b5..08cc1480 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java @@ -33,9 +33,9 @@ import java.util.List; import static junit.framework.Assert.assertEquals; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyFloat; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -61,14 +61,14 @@ public class BarRendererTest extends AndroidplotTest { ArgumentCaptor rectCaptor; @Before - public void setUp() throws Exception { + public void setUp() { canvas = spy(new Canvas()); xyPlot = spy(new XYPlot(getContext(), "My Plot")); barFormatter = spy(new BarFormatter(Color.RED, Color.RED)); } @Test - public void testOnRender_handlesNullValues() throws Exception { + public void onRender_handlesNullValues() { XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", null, 5, null); XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", null, 5, null); @@ -83,7 +83,7 @@ public void testOnRender_handlesNullValues() throws Exception { } @Test - public void testOnRender_stacked() throws Exception { + public void onRender_stacked() { XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 2, 5, 7); XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 8, 5, 3); @@ -119,7 +119,7 @@ public void testOnRender_stacked() throws Exception { } @Test - public void testOnRender_sideBySide() throws Exception { + public void onRender_sideBySide() { XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10); @@ -150,7 +150,35 @@ public void testOnRender_sideBySide() throws Exception { } @Test - public void testOnRender_overlaid() throws Exception { + public void onRender_inOrder_drawsFirstAddedSeriesFirst() { + XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 1, 5, 6); + XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 2, 4, 7); + + BarRenderer renderer = setupRendererForTesting(s1, s2); + renderer.setBarOrientation(BarRenderer.BarOrientation.IN_ORDER); + + xyPlot.setUserRangeOrigin(0); + xyPlot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack); + + verify(renderer, times(6)) + .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture()); + + // list of all bars drawn, in the exact order they were drawn. + List bars = barCaptor.getAllValues(); + + assertEquals(bars.get(0).getY(), s1.getY(0)); + assertEquals(bars.get(1).getY(), s2.getY(0)); + + assertEquals(bars.get(2).getY(), s1.getY(1)); + assertEquals(bars.get(3).getY(), s2.getY(1)); + + assertEquals(bars.get(4).getY(), s1.getY(2)); + assertEquals(bars.get(5).getY(), s2.getY(2)); + } + + @Test + public void onRender_overlaid_drawsBarsWithExpectedHeight() { XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10); @@ -183,10 +211,9 @@ public void testOnRender_overlaid() throws Exception { /** * Verify that positive values are drawn in order of highest yVal, while negative values are * drawn in order of lowest yVal. - * @throws Exception */ @Test - public void testOnRender_overlaid_drawsBarsInOrder() throws Exception { + public void onRender_overlaid_drawsSmallestBarsLast() { XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", -1, -2, 1, 2); XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", -2, -1, 2, 1); @@ -211,7 +238,7 @@ public void testOnRender_overlaid_drawsBarsInOrder() throws Exception { } @Test - public void testFixedBarWidth() throws Exception { + public void onRender_fixedBarWidth_rendersAllBarsWithSameWidth() { XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10); @@ -233,7 +260,7 @@ public void testFixedBarWidth() throws Exception { } @Test - public void testFixedGapWidth() throws Exception { + public void onRender_fixedGapWidth_rendersFixedGapBetweenAllBars() { XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10); diff --git a/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java index cd0ff2b1..94a0be50 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/BubbleRendererTest.java @@ -24,7 +24,6 @@ import org.junit.*; import org.mockito.*; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; /** diff --git a/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java index a7fba775..8eda23b9 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/CandlestickRendererTest.java @@ -24,7 +24,6 @@ import org.junit.*; import org.mockito.*; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; public class CandlestickRendererTest extends AndroidplotTest { diff --git a/androidplot-core/src/test/java/com/androidplot/xy/CatmullRomInterpolatorTest.java b/androidplot-core/src/test/java/com/androidplot/xy/CatmullRomInterpolatorTest.java new file mode 100644 index 00000000..1d672daf --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/CatmullRomInterpolatorTest.java @@ -0,0 +1,59 @@ +package com.androidplot.xy; + +import org.junit.Test; + +import java.util.List; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class CatmullRomInterpolatorTest { + + @Test(expected = IllegalArgumentException.class) + public void interpolate_invalidPointsPerSegment_throwsIllegalArgumentException() { + final XYSeries series = mock(XYSeries.class); + final CatmullRomInterpolator.Params params = + new CatmullRomInterpolator.Params(1, CatmullRomInterpolator.Type.Centripetal); + + new CatmullRomInterpolator().interpolate(series, params); + } + + @Test(expected = IllegalArgumentException.class) + public void interpolate_twoElementSeries_throwsIllegalArgumentException() { + final XYSeries series = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "test", 1, 2); + final CatmullRomInterpolator.Params params = + new CatmullRomInterpolator.Params(2, CatmullRomInterpolator.Type.Centripetal); + + new CatmullRomInterpolator().interpolate(series, params); + } + + @Test + public void interpolate_threeElementSeriesAndThreePointsPerSegment_producesFivePoints() { + final XYSeries series = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "test", 1, 2, 3); + final CatmullRomInterpolator.Params params = + new CatmullRomInterpolator.Params(3, CatmullRomInterpolator.Type.Centripetal); + final List interpolated = new CatmullRomInterpolator().interpolate(series, params); + + assertEquals(5, interpolated.size()); + + // control points should exactly match input: + assertEquals(1, interpolated.get(0).y); + assertEquals(2, interpolated.get(2).y); + assertEquals(3, interpolated.get(4).y); + } + + @Test + public void interpolate_threeElementSeriesAndFourPointsPerSegment_producesSavenPoints() { + final XYSeries series = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "test", 1, 2, 3); + final CatmullRomInterpolator.Params params = + new CatmullRomInterpolator.Params(4, CatmullRomInterpolator.Type.Centripetal); + final List interpolated = new CatmullRomInterpolator().interpolate(series, params); + + assertEquals(7, interpolated.size()); + + // control points should exactly match input: + assertEquals(1, interpolated.get(0).y); + assertEquals(2, interpolated.get(3).y); + assertEquals(3, interpolated.get(6).y); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java index de81e70c..cf083d9e 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java @@ -24,8 +24,8 @@ import org.junit.runner.*; import org.mockito.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java index 3269082d..800992f9 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java @@ -27,7 +27,6 @@ import java.util.*; import static junit.framework.Assert.assertEquals; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; public class LTTBSamplerTest extends AndroidplotTest { diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java index ef67bb36..44a7b22d 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java @@ -26,7 +26,6 @@ import java.util.*; import static junit.framework.Assert.assertEquals; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; public class LineAndPointRendererTest extends AndroidplotTest { @@ -217,4 +216,46 @@ public void testCullPointsCache() throws Exception { renderer.cullPointsCache(); assertEquals(0, renderer.pointsCaches.size()); } + + @Test + public void renderPath_rendersRegions() { + LineAndPointFormatter formatter = + new LineAndPointFormatter(0, 0, 0, null); + + XYRegionFormatter r1 = new XYRegionFormatter(Color.RED); + formatter.addRegion(new RectRegion(0, 2, 0, 2, "region1"), r1); + + XYRegionFormatter r2 = new XYRegionFormatter(Color.GREEN); + formatter.addRegion(new RectRegion(0, 2, 0, 2, "region2"), r2); + + SimpleXYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + xyPlot.addSeries(series, formatter); + LineAndPointRenderer renderer = xyPlot.getRenderer(LineAndPointRenderer.class); + + //xyPlot.draw(canvas); + renderer.renderPath(canvas, plotArea, new Path(), mock(PointF.class), mock(PointF.class), formatter); + verify(canvas).drawRect(any(RectF.class), eq(r1.getPaint())); + verify(canvas).drawRect(any(RectF.class), eq(r2.getPaint())); + } + + @Test + public void drawSeries_withPointLabelFormatter_drawsPointLabels() { + LineAndPointFormatter formatter = + new LineAndPointFormatter(0, 0, 0, null); + formatter.setPointLabelFormatter(new PointLabelFormatter(Color.RED)); + SimpleXYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + xyPlot.addSeries(series, formatter); + LineAndPointRenderer renderer = xyPlot.getRenderer(LineAndPointRenderer.class); + renderer.drawSeries(canvas, plotArea, series, formatter); + + verify(canvas, times(series.size())).drawText( + anyString(), + anyFloat(), + anyFloat(), + eq(formatter.getPointLabelFormatter().getTextPaint())); + + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java index 6bb7fb20..50d3c16c 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java @@ -178,6 +178,61 @@ public void testZoom() { } + @Test + public void testLimitZoom() { + double[] inc_domain = new double[]{10,50,100}; + double[] inc_range = new double[]{20,50}; + + xyPlot = spy(new InstrumentedXYPlot(getContext())); + xyPlot.setDomainBoundaries(0, 20, BoundaryMode.FIXED); + xyPlot.setRangeBoundaries(0, 30, BoundaryMode.FIXED); + xyPlot.setDomainStepModel(new StepModelFit(xyPlot.getBounds().getxRegion(), inc_domain, 5)); + xyPlot.setRangeStepModel(new StepModelFit(xyPlot.getBounds().getyRegion(), inc_range, 5)); + xyPlot.redraw(); + + PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE, PanZoom.ZoomLimit.MIN_TICKS)); + + // cap our pan/zoom boundaries: + xyPlot.getOuterLimits().set(0, 20, 0, 30); + + panZoom.setFingersRect(new RectF(0, 0, 20, 20)); + + InOrder inOrder = inOrder(xyPlot); + inOrder.verify(xyPlot).setDomainBoundaries(0, 20, BoundaryMode.FIXED); + + // should NOT result in a 2x zoom on domain centerpoint, but in a zoom to + // the minimum spacing 10 and 20 respectively + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 40, 40)); + inOrder.verify(xyPlot).setDomainBoundaries(5f, 15f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(5f, 25f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // to zoom in beyond min limits + panZoom.setZoomLimit(PanZoom.ZoomLimit.OUTER); + + // should result in another 2x zoom on domain centerpoint: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 80, 80)); + inOrder.verify(xyPlot).setDomainBoundaries(7.5f, 12.5f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(10f, 20f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // back to limited zoom + panZoom.setZoomLimit(PanZoom.ZoomLimit.MIN_TICKS); + + // try to zoom in further, should snap back to min limit: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 90, 90)); + inOrder.verify(xyPlot).setDomainBoundaries(5f, 15f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(5f, 25f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // redraw should not be called again + inOrder.verify(xyPlot, never()).redraw(); + + // make sure no panning took place during these zoom ops: + verify(panZoom, never()).pan(any(MotionEvent.class)); + + } + @Test public void testFingerDistance() { PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE)); diff --git a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java index 70a9bbe1..c6e11ae7 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java @@ -21,7 +21,6 @@ import com.androidplot.test.AndroidplotTest; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import static junit.framework.Assert.assertEquals; @@ -196,55 +195,4 @@ public void testUnion() throws Exception { assertEquals(0, r2.getMinY().doubleValue(), 0); assertEquals(200, r2.getMaxY().doubleValue(), 0); } - - /** - * Currently ignored as the base implementation does not currently pass. - */ - @Ignore - @Test - public void testOverlapsLine() { - RectRegion r1 = new RectRegion(0, 100, 0, 100); - -// assertFalse(r1.intersectsWithLine(200, 200, 400, 400)); -// assertFalse(r1.intersectsWithLine(-1, -1, -1, 100)); -// assertTrue(r1.intersectsWithLine(0, 0, 100, 100)); -// assertTrue(r1.intersectsWithLine(50, 50, 200, 200)); - - // lines running parallel to region edges: - assertTrue(r1.intersectsWithLine(0, 0, 0, 100)); - assertTrue(r1.intersectsWithLine(0, 0, 100, 0)); - assertTrue(r1.intersectsWithLine(0, 100, 0, 0)); - assertTrue(r1.intersectsWithLine(100, 0, 0, 0)); - - assertTrue(r1.intersectsWithLine(100, 0, 0, 0)); - assertTrue(r1.intersectsWithLine(0, 100, 0, 0)); - assertTrue(r1.intersectsWithLine(0, 0, 100, 0)); - assertTrue(r1.intersectsWithLine(0, 0, 0, 100)); - - // lines passing through top & bottom edges only: - assertTrue(r1.intersectsWithLine(50, -1000, 50, 1000)); - assertTrue(r1.intersectsWithLine(50, 1000, 50, -1000)); - - // lines passing through left & right edges only: - assertTrue(r1.intersectsWithLine(-1000, 50, 1000, 50)); - assertTrue(r1.intersectsWithLine(1000, 50, -100, 50)); - - // diagonal passing through bottom-left and top-right corners: - assertTrue(r1.intersectsWithLine(-100, -100, 200, 200)); - - // diagonal passing inside upper-left edge - assertTrue(r1.intersectsWithLine(-20, 80, 20, 120)); - assertTrue(r1.intersectsWithLine(20, 120, -20, 80)); - - // diagonal passing outside upper-left edge - - // diagonal passing inside lower-left edge - // diagonal passing outside lower-left edge - - // diagonal passing inside upper-right edge - // diagonal passing outside upper-right edge - - // diagonal passing inside lower-right edge - // diagonal passing outside lower-right edge - } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java index 397e7700..dc21ab7c 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java @@ -19,13 +19,14 @@ import org.junit.Test; import java.util.Arrays; +import java.util.NoSuchElementException; import static junit.framework.Assert.assertEquals; public class SimpleXYSeriesTest { @Test - public void testYValsOnlyConstructor() throws Exception { + public void constructor_yValsOnly() throws Exception { Number[] yVals = {5, 6, 7, 8, 9}; SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "test"); @@ -43,7 +44,7 @@ public void testYValsOnlyConstructor() throws Exception { } @Test - public void testXYInterleavedConstructor() throws Exception { + public void constructor_xyInterleaved() throws Exception { Number[] yVals = {55, 5, 66, 6, 77, 7, 88, 8, 99, 9}; SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "test"); @@ -61,7 +62,7 @@ public void testXYInterleavedConstructor() throws Exception { } @Test - public void testTwoListConstructor() throws Exception { + public void constructor_xAndYLists() throws Exception { Number[] yVals = {5, 6, 7, 8, 9}; Number[] xVals = {1, 2, 3, 4, 5}; SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(xVals), Arrays.asList(yVals), "test"); @@ -80,7 +81,7 @@ public void testTwoListConstructor() throws Exception { } @Test - public void testPushPopStuff() throws Exception { + public void addRemove_modifiesSeries() throws Exception { Number[] yVals = {5, 6, 7, 8, 9}; Number[] xVals = {1, 2, 3, 4, 5}; SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(xVals), Arrays.asList(yVals), "test"); @@ -107,7 +108,7 @@ public void testPushPopStuff() throws Exception { } @Test - public void testSet() throws Exception { + public void set_setsExpectedValue() throws Exception { Number[] yVals = {5, 6, 7, 8, 9}; Number[] xVals = {1, 2, 3, 4, 5}; SimpleXYSeries series = new SimpleXYSeries(Arrays.asList(xVals), Arrays.asList(yVals), "test"); @@ -128,7 +129,7 @@ public void testSet() throws Exception { } @Test - public void testResize() throws Exception { + public void resize_emptySeries_resizesSeries() { SimpleXYSeries series = new SimpleXYSeries("series"); series.resize(10); assertEquals(10, series.size()); @@ -146,4 +147,59 @@ public void testResize() throws Exception { assertEquals(0, series.size()); } + @Test + public void resize_yValsOnly_resizesSeries() { + SimpleXYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "", 1, 2, 3); + series.resize(10); + assertEquals(10, series.size()); + } + + @Test + public void setXY_modifiesSeries() { + SimpleXYSeries series = new SimpleXYSeries("series"); + series.resize(5); + series.setXY(100, 200, 0); + + assertEquals(100, series.getX(0)); + assertEquals(200, series.getY(0)); + + } + + @Test + public void setX_yValsOnly_changesValue() { + SimpleXYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "", 1, 2, 3); + series.setX(9, 0); + assertEquals(9, series.getX(0)); + } + + @Test(expected = NoSuchElementException.class) + public void removeFirst_throwsNoSuchElementException_ifEmpty() { + new SimpleXYSeries("series").removeFirst(); + } + + @Test(expected = NoSuchElementException.class) + public void removeLast_throwsNoSuchElementException_ifEmpty() { + new SimpleXYSeries("series").removeLast(); + } + + @Test + public void setTitle_changesTitle() { + SimpleXYSeries series = new SimpleXYSeries("series"); + + final String newTitle = "newTitle"; + series.setTitle(newTitle); + assertEquals(newTitle, series.getTitle()); + } + + @Test + public void clear_removesEverything() { + SimpleXYSeries series = new SimpleXYSeries( + Arrays.asList(1, 2, 3, 4, 5), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "series"); + + series.clear(); + assertEquals(0, series.size()); + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/StepModelFitTest.java b/androidplot-core/src/test/java/com/androidplot/xy/StepModelFitTest.java new file mode 100644 index 00000000..5f34fd52 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/StepModelFitTest.java @@ -0,0 +1,61 @@ +package com.androidplot.xy; + +import com.androidplot.Region; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class StepModelFitTest { + + Region regionSmall = new Region(0,11); + Region regionBig = new Region(-111,420); + Region regionZero = new Region(0, 0); + Region regionUndef = new Region(0, null); + + double[] stpSmall = {1,2,5}, stpBig = {1,10,100}, nonsense = {0}; + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void getValue() throws Exception { + + StepModelFit model = new StepModelFit(regionSmall,stpSmall,3); + + assertEquals(5.0, model.getValue(), 0.0); + model.setValue(5.0); + assertEquals(2.0, model.getValue(), 0.0); + model.setValue(7.0); + assertEquals(2.0, model.getValue(), 0.0); + + model.setSteps(stpBig); + assertEquals(1.0, model.getValue(), 0.0); + + model.setScale(regionBig); + assertEquals(100.0, model.getValue(), 0.0); + model.setValue(1000.0); + assertEquals(1.0, model.getValue(), 0.0); + + // bad parameters + model.setSteps(nonsense); + assertArrayEquals(stpBig,model.getSteps(), 0.0); + + model.setScale(regionZero); + assertEquals(stpBig[0], model.getValue(), 0.0); + + model.setScale(regionUndef); + model.setValue(1.1); + assertEquals(1.1, model.getValue(), 0.0); + } + +} \ No newline at end of file diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java index 4b9f156a..a9182501 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java @@ -28,11 +28,11 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; -import static junit.framework.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyFloat; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -67,7 +67,7 @@ public class XYGraphWidgetTest extends AndroidplotTest { @Before - public void setUp() throws Exception { + public void setUp() { size = spy(new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE)); xyPlot = spy(new XYPlot(getContext(), "XYPlot")); when(xyPlot.getRegistry()).thenReturn(seriesRegistry); @@ -79,17 +79,12 @@ public void setUp() throws Exception { xyPlot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1); graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); - graphWidget.setGridRect(new RectF(0, 0, 100, 100)); + graphWidget.setGridRect(new RectF(0, 0, 10, 100)); graphWidget.setLabelRect(new RectF(0, 0, 100, 100)); } - @After - public void tearDown() throws Exception { - - } - @Test - public void testProcessAttrs() throws Exception { + public void processAttrs_withDefaults_disablesGridClipping() { XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); graphWidget.processAttrs(typedArray); @@ -97,13 +92,15 @@ public void testProcessAttrs() throws Exception { } @Test - public void testDoOnDraw_drawGridOnTopFalse() throws Exception { + public void doOnDraw_drawGridOnTopFalse_drawsGridAfterData() throws Exception { XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); graphWidget.setDrawGridOnTop(false); doNothing().when(graphWidget).drawGrid(canvas); doNothing().when(graphWidget).drawData(canvas); - graphWidget.doOnDraw(canvas, new RectF(0, 0, 100, 100)); + final RectF canvasRect = new RectF(0, 0, 100, 100); + graphWidget.onResize(null, canvasRect); + graphWidget.doOnDraw(canvas, canvasRect); InOrder io = inOrder(graphWidget); io.verify(graphWidget).drawGrid(canvas); @@ -111,13 +108,15 @@ public void testDoOnDraw_drawGridOnTopFalse() throws Exception { } @Test - public void testDoOnDraw_drawGridOnTopTrue() throws Exception { + public void doOnDraw_drawGridOnTopTrue_drawsGridBeforeData() throws Exception { XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); graphWidget.setDrawGridOnTop(true); doNothing().when(graphWidget).drawGrid(canvas); doNothing().when(graphWidget).drawData(canvas); - graphWidget.doOnDraw(canvas, new RectF(0, 0, 100, 100)); + final RectF canvasRect = new RectF(0, 0, 100, 100); + graphWidget.onResize(null, canvasRect); + graphWidget.doOnDraw(canvas, canvasRect); InOrder io = inOrder(graphWidget); io.verify(graphWidget).drawData(canvas); @@ -125,7 +124,7 @@ public void testDoOnDraw_drawGridOnTopTrue() throws Exception { } @Test - public void testDrawMarkers() throws Exception { + public void drawMarkers_drawsLineForEachMarker() { xyPlot.addMarker(new XValueMarker(1, "x")); xyPlot.addMarker(new YValueMarker(-1, "y")); @@ -136,42 +135,22 @@ public void testDrawMarkers() throws Exception { .drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), any(Paint.class)); } - protected void runDrawGridTest() { - doNothing().when(graphWidget). - drawDomainLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean()); - - doNothing().when(graphWidget). - drawRangeLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean()); - - xyPlot.setRangeBoundaries(0, 100, BoundaryMode.FIXED); - xyPlot.setDomainBoundaries(0, 100, BoundaryMode.FIXED); - - graphWidget.drawGrid(canvas); - - // expecting a 100x100 grid to be drawn: - verify(graphWidget, times(100)) - .drawDomainLine(eq(canvas), anyFloat(), anyFloat(), any(Paint.class), eq(false)); - - verify(graphWidget, times(100)) - .drawRangeLine(eq(canvas), anyFloat(), anyFloat(), any(Paint.class), eq(false)); - } - @Test - public void testDrawGrid_nullOrigin() throws Exception { + public void drawGrid_nullOrigin_drawsGrid() { when(xyPlot.getDomainOrigin()).thenReturn(null); when(xyPlot.getRangeOrigin()).thenReturn(null); runDrawGridTest(); } @Test - public void testDrawGrid_zeroOrigin() throws Exception { + public void drawGrid_zeroOrigin_drawsGrid() { when(xyPlot.getDomainOrigin()).thenReturn(0); when(xyPlot.getRangeOrigin()).thenReturn(0); runDrawGridTest(); } @Test - public void testDrawGrid_centeredOrigin() throws Exception { + public void drawGrid_centeredOrigin_drawsGrid() { // set origin to midpoint so we exercise // code to draw lines on both sides of the origin: @@ -181,7 +160,7 @@ public void testDrawGrid_centeredOrigin() throws Exception { } @Test - public void testDrawCursors_ifCursorPaintAndPositionAreSet() throws Exception { + public void drawCursors_withCursorPaintAndPosition_drawsCursorLines() { final Paint domainCursorPaint = new Paint(); graphWidget.setDomainCursorPaint(domainCursorPaint); @@ -197,7 +176,7 @@ public void testDrawCursors_ifCursorPaintAndPositionAreSet() throws Exception { } @Test - public void testDrawCursors_ifCursorPaintAndPositionAreNotSet() throws Exception { + public void testDrawCursors_noCursorPaintOrPosition_drawsNoCursorLines() { graphWidget.setDomainCursorPaint(null); graphWidget.setRangeCursorPaint(null); @@ -208,7 +187,7 @@ public void testDrawCursors_ifCursorPaintAndPositionAreNotSet() throws Exception } @Test - public void testDrawCursorLabel() throws Exception { + public void drawCursorLabel_drawsText() { graphWidget.setDomainCursorPosition(0f); graphWidget.setRangeCursorPosition(0f); XYGraphWidget.CursorLabelFormatter clf = mock(XYGraphWidget.CursorLabelFormatter.class); @@ -220,8 +199,7 @@ public void testDrawCursorLabel() throws Exception { } @Test - public void testSetLineLabelEdges() throws Exception { - + public void setLineLabelEdges_setsEdges() { graphWidget.setLineLabelEdges(XYGraphWidget.Edge.LEFT, XYGraphWidget.Edge.BOTTOM); assertTrue(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.LEFT)); assertTrue(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.BOTTOM)); @@ -234,7 +212,7 @@ public void testSetLineLabelEdges() throws Exception { } @Test - public void testSetLineLabelEdges_bitfield() throws Exception { + public void setLineLabelEdges_bitfield_setsEdges() { graphWidget.setLineLabelEdges( XYGraphWidget.Edge.TOP.getValue() | XYGraphWidget.Edge.RIGHT.getValue()); @@ -246,24 +224,24 @@ public void testSetLineLabelEdges_bitfield() throws Exception { } @Test - public void testScreenToSeries() throws Exception { + public void screenToSeries_returnsSeriesCoords() { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); XYCoords coords = graphWidget.screenToSeries(new PointF(0, 0)); assertEquals(-100, coords.x.intValue()); assertEquals(100, coords.y.intValue()); - coords = graphWidget.screenToSeries(new PointF(100, 100)); + coords = graphWidget.screenToSeries(new PointF(10, 100)); assertEquals(100, coords.x.intValue()); assertEquals(-100, coords.y.intValue()); - coords = graphWidget.screenToSeries(new PointF(50, 50)); + coords = graphWidget.screenToSeries(new PointF(5, 50)); assertEquals(0, coords.x.intValue()); assertEquals(0, coords.y.intValue()); } @Test - public void testSeriesToScreen() throws Exception { + public void seriesToScreen_returnsScreenPoint() { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); PointF point = graphWidget.seriesToScreen(new XYCoords(-100, 100)); @@ -271,25 +249,25 @@ public void testSeriesToScreen() throws Exception { assertEquals(0f, point.y); point = graphWidget.seriesToScreen(new XYCoords(100, -100)); - assertEquals(100f, point.x); + assertEquals(10f, point.x); assertEquals(100f, point.y); point = graphWidget.seriesToScreen(new XYCoords(0, 0)); - assertEquals(50f, point.x); + assertEquals(5f, point.x); assertEquals(50f, point.y); } @Test - public void testScreenToSeriesX() throws Exception { + public void screenToSeriesX_returnsSeriesValue() { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); assertEquals(-100, graphWidget.screenToSeriesX(new PointF(0, 0)).intValue()); - assertEquals(100, graphWidget.screenToSeriesX(new PointF(100, 100)).intValue()); - assertEquals(0, graphWidget.screenToSeriesX(new PointF(50, 50)).intValue()); + assertEquals(100, graphWidget.screenToSeriesX(new PointF(10, 100)).intValue()); + assertEquals(0, graphWidget.screenToSeriesX(new PointF(5, 50)).intValue()); } @Test - public void testScreenToSeriesY() throws Exception { + public void screenToSeriesY_returnsSeriesValue() { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); assertEquals(100, graphWidget.screenToSeriesY(new PointF(0, 0)).intValue()); @@ -298,20 +276,54 @@ public void testScreenToSeriesY() throws Exception { } @Test - public void testSeriesToScreenX() throws Exception { + public void seriesToScreenX_returnsScreenValue() { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); assertEquals(0f, graphWidget.seriesToScreenX(-100)); - assertEquals(100f, graphWidget.seriesToScreenX(100)); - assertEquals(50f, graphWidget.seriesToScreenX(0)); + assertEquals(10f, graphWidget.seriesToScreenX(100)); + assertEquals(5f, graphWidget.seriesToScreenX(0)); } @Test - public void testSeriesToScreenY() throws Exception { + public void seriesToScreenY_returnsScreenValue() { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); - assertEquals(0f, graphWidget.seriesToScreenY(100)); - assertEquals(100f, graphWidget.seriesToScreenY(-100)); + assertEquals(100f, graphWidget.seriesToScreenY(100)); + assertEquals(0f, graphWidget.seriesToScreenY(-100)); assertEquals(50f, graphWidget.seriesToScreenY(0)); } + + @Test + public void setGridInsets_updatesGridRect() { + graphWidget.setGridInsets(new com.androidplot.ui.Insets(0, 0, 0, 0)); + final RectF oldRect = graphWidget.getGridRect(); + + graphWidget.setGridInsets(new com.androidplot.ui.Insets(2, 2, 2, 2)); + final RectF newRect = graphWidget.getGridRect(); + + assertEquals(oldRect.left + 2, newRect.left); + assertEquals(oldRect.top + 2, newRect.top); + assertEquals(oldRect.right - 2, newRect.right); + assertEquals(oldRect.bottom -2, newRect.bottom); + } + + private void runDrawGridTest() { + doNothing().when(graphWidget). + drawDomainLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean(), anyBoolean()); + + doNothing().when(graphWidget). + drawRangeLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean(), anyBoolean()); + + xyPlot.setRangeBoundaries(0, 100, BoundaryMode.FIXED); + xyPlot.setDomainBoundaries(0, 100, BoundaryMode.FIXED); + + graphWidget.drawGrid(canvas); + + // expecting a 100x100 grid to be drawn: + verify(graphWidget, times(100)) + .drawDomainLine(eq(canvas), anyFloat(), anyDouble(), any(Paint.class), eq(false), anyBoolean()); + + verify(graphWidget, times(100)) + .drawRangeLine(eq(canvas), anyFloat(), anyDouble(), any(Paint.class), eq(false), anyBoolean()); + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java index bbb2bec5..778fb40a 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java @@ -17,61 +17,100 @@ package com.androidplot.xy; import android.graphics.*; -import com.androidplot.Plot; import com.androidplot.test.AndroidplotTest; -import org.junit.After; +import com.androidplot.ui.DynamicTableModel; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.Size; +import com.androidplot.ui.SizeMode; +import com.google.common.collect.Lists; + +import org.junit.Before; import org.junit.Test; -import org.robolectric.RuntimeEnvironment; -import java.util.Arrays; -import static junit.framework.Assert.assertEquals; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class XYLegendWidgetTest extends AndroidplotTest { - static class MockXYPlot extends XYPlot { + @Mock LayoutManager layoutManager; + @Mock XYPlot xyPlot; + @Mock Canvas canvas; + @Mock XYRegionFormatter xyRegionFormatter; + LineAndPointRenderer lineAndPointRenderer; - public MockXYPlot() { - super(RuntimeEnvironment.application, "Test", - Plot.RenderMode.USE_MAIN_THREAD); - } + Size widgetSize = new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE); + Size iconSize = new Size(10, SizeMode.ABSOLUTE, 10, SizeMode.ABSOLUTE); + XYSeriesRegistry seriesRegistry; - public void exposedOnSizeChanged(int w, int h, int oldw, int oldh) { - this.onSizeChanged(w, h, oldw, oldh); - } + XYLegendWidget legendWidget; - public void exposedOnDraw(Canvas canvas) { - this.onDraw(canvas); - } - } + @Before + public void before() { + seriesRegistry = new XYSeriesRegistry(); + legendWidget = spy(new XYLegendWidget(layoutManager, xyPlot, widgetSize, + new DynamicTableModel(4, 4), iconSize)); - @After - public void tearDown() throws Exception {} + lineAndPointRenderer = new LineAndPointRenderer(xyPlot); - @Test - public void testDoOnDraw() throws Exception { - MockXYPlot plot = new MockXYPlot(); + when(xyPlot.getRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getRendererList()).thenReturn(Lists.newArrayList(lineAndPointRenderer)); + when(xyPlot.getRenderer(any(Class.class))).thenReturn(lineAndPointRenderer); + } - SimpleXYSeries s1 = new SimpleXYSeries((Arrays.asList(1, 2, 3)), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1"); + @Test + public void draw_drawsLegendIcons_forEnabledItemsOnly() throws Exception { + final XYSeries s1 = mock(XYSeries.class); + final XYSeriesFormatter f1 = new LineAndPointFormatter(); + f1.setLegendIconEnabled(true); + + final XYSeries s2 = mock(XYSeries.class); + final XYSeriesFormatter f2 = new LineAndPointFormatter(); + f2.setLegendIconEnabled(false); + + final RectRegion r1 = new RectRegion(0, 0, 10, 10, "r1"); + final RectRegion r2 = new RectRegion(0, 0, 20, 20, "r2"); + f1.addRegion(r1, new XYRegionFormatter(0)); + f2.addRegion(r2, new XYRegionFormatter(0)); + + seriesRegistry.add(s1, f1); + seriesRegistry.add(s2, f2); + legendWidget.draw(canvas); + + verify(legendWidget, times(2)) + .drawRegionLegendIcon(any(Canvas.class), any(RectF.class), any(XYRegionFormatter.class)); + verify(legendWidget, times(3)) + .drawIcon(any(Canvas.class), any(RectF.class), any(XYLegendItem.class)); + } - plot.addSeries(s1, new LineAndPointFormatter( - Color.RED, Color.GREEN, Color.BLUE, null)); + @Test + public void draw_sortsItemsAlphabeticallyByTitle() throws Exception{ + final XYLegendItem i1 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "zoo"); + final XYLegendItem i2 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "apple"); + final XYLegendItem i3 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "boo"); - assertEquals(1, plot.getRegistry().size()); + final List legendItems = Lists.newArrayList(i1, i2, i3); + doReturn(legendItems).when(legendWidget).getLegendItems(); - plot.exposedOnSizeChanged(100, 100, 100, 100); - plot.redraw(); - // have to manually invoke this because the invalidate() - // invoked by redraw() is a stub and will not result in onDraw being called. - plot.exposedOnDraw(new Canvas()); + legendWidget.draw(canvas); - plot.removeSeries(s1); - assertEquals(0, plot.getRegistry().size()); - plot.addSeries(s1, new BarFormatter(Color.RED, Color.GREEN)); - plot.redraw(); + InOrder inOrder = Mockito.inOrder(legendWidget); - // throws NullPointerException before fix - // for ANDROIDPLOT-166 was applied. - plot.exposedOnDraw(new Canvas()); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i2)); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i3)); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i1)); } - } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java index 553314a4..2e8c6094 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java @@ -31,6 +31,7 @@ import java.util.List; import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.mock; public class XYPlotTest extends AndroidplotTest { @@ -38,7 +39,7 @@ public class XYPlotTest extends AndroidplotTest { List numList1; List numList2; - SimpleXYSeries series1; + SimpleXYSeries series0To100; @Before public void setUp() throws Exception { @@ -46,7 +47,7 @@ public void setUp() throws Exception { plot = new XYPlot(getContext(), "test"); numList1 = Arrays.asList(0, 1, 3, 5, 10, 15, 25, 50, 75, 100); // 10 elements numList2 = Arrays.asList(-100, 0, 1, 3, 5, 10, 15, 25, 50, 75, 100, 200); // 12 elements - series1 = new SimpleXYSeries(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, ""); + series0To100 = new SimpleXYSeries(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, ""); } @After @@ -56,7 +57,7 @@ public void tearDown() throws Exception { @Test public void testOriginFixedMode() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.centerOnDomainOrigin(5, 2, BoundaryMode.FIXED); plot.calculateMinMaxVals(); @@ -67,7 +68,7 @@ public void testOriginFixedMode() throws Exception { @Test public void testOriginAutoMode() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.centerOnDomainOrigin(5); plot.calculateMinMaxVals(); @@ -84,7 +85,7 @@ public void testOriginAutoMode() throws Exception { @Test public void testOriginGrowMode() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.centerOnDomainOrigin(5, null, BoundaryMode.GROW); plot.calculateMinMaxVals(); @@ -92,14 +93,14 @@ public void testOriginGrowMode() throws Exception { assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0); // introduce a larger domain set. boundaries should change - series1.setModel(numList2, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); assertEquals(-1.0, plot.getBounds().getMinX().doubleValue(), 0); assertEquals(11.0, plot.getBounds().getMaxX().doubleValue(), 0); // revert series model back to the previous set. boundaries should remain the same - series1.setModel(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); assertEquals(-1.0, plot.getBounds().getMinX().doubleValue(), 0); @@ -108,7 +109,7 @@ public void testOriginGrowMode() throws Exception { @Test public void testOriginShrinkMode() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.centerOnDomainOrigin(5, null, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); @@ -116,18 +117,27 @@ public void testOriginShrinkMode() throws Exception { assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0); // update with more extreme values...nothing should change in shrink mode: - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0); assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0); } + @Test(expected = IllegalArgumentException.class) + public void centerOnRangeOrigin_throwsIllegalArgumentException_ifNullOrigin() { + plot.centerOnRangeOrigin(null); + } + + @Test(expected = IllegalArgumentException.class) + public void centerOnDomainOrigin_throwsIllegalArgumentException_ifNullOrigin() { + plot.centerOnDomainOrigin(null); + } // Ifor not sure about filling in test stubs just going to do my own stuff instead. @Test public void testsetDomainBoundaries() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.calculateMinMaxVals(); // default to auto so run them @@ -159,7 +169,7 @@ public void testsetDomainBoundaries() throws Exception { assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); // update with more extreme values... - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after growing @@ -167,7 +177,7 @@ public void testsetDomainBoundaries() throws Exception { assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // back to previous - series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. @@ -175,7 +185,7 @@ public void testsetDomainBoundaries() throws Exception { assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // back to big - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.setDomainBoundaries(2, BoundaryMode.SHRINK, 8, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); @@ -185,7 +195,7 @@ public void testsetDomainBoundaries() throws Exception { assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // now small - series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after shrinking @@ -193,7 +203,7 @@ public void testsetDomainBoundaries() throws Exception { assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); // back to previous - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. @@ -210,8 +220,8 @@ public void testsetDomainBoundaries() throws Exception { } @Test - public void testsetRangeBoundaries() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + public void setRangeBoundaries_calculatesCorrectMinMaxVals() throws Exception { + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.calculateMinMaxVals(); // default to auto so run them @@ -242,7 +252,7 @@ public void testsetRangeBoundaries() throws Exception { assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); // update with more extreme values... - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after growing @@ -250,7 +260,7 @@ public void testsetRangeBoundaries() throws Exception { assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // back to previous - series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. @@ -258,7 +268,7 @@ public void testsetRangeBoundaries() throws Exception { assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // back to big - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.setRangeBoundaries(2, BoundaryMode.SHRINK, 8, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); @@ -268,7 +278,7 @@ public void testsetRangeBoundaries() throws Exception { assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // now small - series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after shrinking @@ -276,7 +286,7 @@ public void testsetRangeBoundaries() throws Exception { assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); // back to previous - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. @@ -294,7 +304,7 @@ public void testsetRangeBoundaries() throws Exception { @Test public void testSetDomainRightMinMax() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.calculateMinMaxVals(); // default to auto so run them @@ -308,7 +318,7 @@ public void testSetDomainRightMinMax() throws Exception { assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on RightMax @@ -331,7 +341,7 @@ public void testSetDomainRightMinMax() throws Exception { assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // small list - series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on RightMin @@ -349,7 +359,7 @@ public void testSetDomainRightMinMax() throws Exception { @Test public void testSetRangeTopBottomMinMax() throws Exception { - plot.addSeries(series1, new LineAndPointFormatter()); + plot.addSeries(series0To100, new LineAndPointFormatter()); plot.calculateMinMaxVals(); // default to auto so run them @@ -364,7 +374,7 @@ public void testSetRangeTopBottomMinMax() throws Exception { assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); - series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on Limits @@ -389,7 +399,7 @@ public void testSetRangeTopBottomMinMax() throws Exception { assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // small list - series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); + series0To100.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on Limits @@ -407,33 +417,45 @@ public void testSetRangeTopBottomMinMax() throws Exception { } @Test - public void testSetDomainUpperBoundary() throws Exception { - + public void setDomainUpperBoundary_overridesCalculatedBoundary() throws Exception { + plot.addSeries(series0To100, new LineAndPointFormatter()); + plot.setDomainUpperBoundary(350, BoundaryMode.FIXED); + plot.calculateMinMaxVals(); + assertEquals(350, plot.getBounds().getMaxX().intValue()); } @Test public void testSetDomainLowerBoundary() throws Exception { - + plot.addSeries(series0To100, new LineAndPointFormatter()); + plot.setDomainLowerBoundary(-350, BoundaryMode.FIXED); + plot.calculateMinMaxVals(); + assertEquals(-350, plot.getBounds().getMinX().intValue()); } @Test public void testSetRangeUpperBoundary() throws Exception { - + plot.addSeries(series0To100, new LineAndPointFormatter()); + plot.setRangeUpperBoundary(350, BoundaryMode.FIXED); + plot.calculateMinMaxVals(); + assertEquals(350, plot.getBounds().getMaxY().intValue()); } @Test public void testSetRangeLowerBoundary() throws Exception { - + plot.addSeries(series0To100, new LineAndPointFormatter()); + plot.setRangeLowerBoundary(-350, BoundaryMode.FIXED); + plot.calculateMinMaxVals(); + assertEquals(-350, plot.getBounds().getMinY().intValue()); } @Test public void testSetDomainOrigin() throws Exception { - + // TODO } @Test public void testSetRangeOrigin() throws Exception { - + // TODO } @Test @@ -451,4 +473,81 @@ public void testConfigure() throws Exception { assertEquals(Plot.RenderMode.USE_BACKGROUND_THREAD, plot.getRenderMode()); assertEquals(Color.parseColor(param3), plot.getBackgroundPaint().getColor()); } + + @Test + public void removeMarker_withXMarker_removesExpectedXMarkerOnly() { + + XValueMarker xMarker1 = new XValueMarker(1, "x1"); + XValueMarker xMarker2 = new XValueMarker(2, "x2"); + XValueMarker xMarker3 = new XValueMarker(2, "x2"); + XValueMarker xMarker4 = new XValueMarker(2, "x2"); + XValueMarker xMarker5 = new XValueMarker(2, "x2"); + + plot.addMarker(xMarker1); + plot.addMarker(xMarker2); + plot.addMarker(xMarker3); + plot.addMarker(xMarker4); + plot.addMarker(xMarker5); + + assertEquals(5, plot.getXValueMarkers().size()); + + assertEquals(xMarker3, plot.removeMarker(xMarker3)); + assertEquals(4, plot.getXValueMarkers().size()); + } + + @Test + public void removeMarker_withYMarker_removesExpectedYMarkerOnly() { + + YValueMarker YMarker1 = new YValueMarker(1, "Y1"); + YValueMarker YMarker2 = new YValueMarker(2, "Y2"); + YValueMarker YMarker3 = new YValueMarker(2, "Y2"); + YValueMarker YMarker4 = new YValueMarker(2, "Y2"); + YValueMarker YMarker5 = new YValueMarker(2, "Y2"); + + plot.addMarker(YMarker1); + plot.addMarker(YMarker2); + plot.addMarker(YMarker3); + plot.addMarker(YMarker4); + plot.addMarker(YMarker5); + + assertEquals(5, plot.getYValueMarkers().size()); + + assertEquals(YMarker3, plot.removeMarker(YMarker3)); + assertEquals(4, plot.getYValueMarkers().size()); + } + + @Test + public void removeMarkers_removesAllXAndYMarkers() { + + XValueMarker xMarker1 = new XValueMarker(1, "x1"); + XValueMarker xMarker2 = new XValueMarker(2, "x2"); + XValueMarker xMarker3 = new XValueMarker(2, "x2"); + XValueMarker xMarker4 = new XValueMarker(2, "x2"); + XValueMarker xMarker5 = new XValueMarker(2, "x2"); + + plot.addMarker(xMarker1); + plot.addMarker(xMarker2); + plot.addMarker(xMarker3); + plot.addMarker(xMarker4); + plot.addMarker(xMarker5); + + YValueMarker YMarker1 = new YValueMarker(1, "Y1"); + YValueMarker YMarker2 = new YValueMarker(2, "Y2"); + YValueMarker YMarker3 = new YValueMarker(2, "Y2"); + YValueMarker YMarker4 = new YValueMarker(2, "Y2"); + YValueMarker YMarker5 = new YValueMarker(2, "Y2"); + + plot.addMarker(YMarker1); + plot.addMarker(YMarker2); + plot.addMarker(YMarker3); + plot.addMarker(YMarker4); + plot.addMarker(YMarker5); + + assertEquals(5, plot.getXValueMarkers().size()); + assertEquals(5, plot.getYValueMarkers().size()); + + plot.removeMarkers(); + assertEquals(0, plot.getXValueMarkers().size()); + assertEquals(0, plot.getYValueMarkers().size()); + } } diff --git a/build.gradle b/build.gradle index 196377de..4b5e18de 100644 --- a/build.gradle +++ b/build.gradle @@ -14,44 +14,32 @@ * limitations under the License. */ -apply plugin: 'java' - -allprojects { - repositories { - jcenter() - } -} - ext { - theBuildToolsVersion = '24.0.2' - theCompileSdkVersion = 24 - theTargetSdkVersion = 24 + theCompileSdkVersion = 34 + theTargetSdkVersion = 34 theMinSdkVersion = 5 - theVersionName = '1.4.1' - theVersionCode = 0 + theVersionName = '1.5.11' + theVersionCode = 227 + gitUrl = 'https://github.com/halfhp/androidplot.git' } buildscript { repositories { mavenCentral() - jcenter() + maven { url "https://plugins.gradle.org/m2/" } + maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' } + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' - classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.5.0' - classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.6.3' + classpath 'com.android.tools.build:gradle:8.5.2' + classpath 'com.mxalbert.gradle:jacoco-android:0.2.0' } } allprojects { repositories { mavenCentral() + google() } } - -task wrapper(type: Wrapper) { - gradleVersion = '2.14.1' -} diff --git a/circle.yml b/circle.yml deleted file mode 100644 index cb73691c..00000000 --- a/circle.yml +++ /dev/null @@ -1,59 +0,0 @@ - -machine: - environment: - KEYSTORE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/sigining.keystore - PUBLISHER_ACCT_JSON_FILE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/publisher_profile.json - -dependencies: - - pre: - - echo y | android update sdk --no-ui --all --filter "android-24" - - echo y | android update sdk --no-ui --all --filter "build-tools-24.0.2" - - bash ./misc/download_keystore.sh - - bash ./misc/inject_circle_build_number.sh - -test: - - override: - - (./gradlew test assembleRelease javadoc): - timeout: 360 - - post: - - # core lib: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/outputs/aar/ $CIRCLE_ARTIFACTS - - # demo app .apk: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/demoapp/build/outputs/apk/ $CIRCLE_ARTIFACTS - - # javadoc: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/docs/javadoc/ $CIRCLE_ARTIFACTS - - - # junit xml report: - - mkdir -p $CIRCLE_TEST_REPORTS/junit-xml/ - - find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit-xml/ \; - - # junit html report: - # TODO: recursively copy subdirs etc - - mkdir -p $CIRCLE_TEST_REPORTS/junit-html/ - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/tests/release/* $CIRCLE_TEST_REPORTS/junit-html/ - - # lint report: - - mkdir -p $CIRCLE_TEST_REPORTS/lint/ - - find . -type f -regex ".*/build/outputs/.*html" -exec cp {} $CIRCLE_TEST_REPORTS/lint/ \; - - # code coverage: - - ./gradlew jacocoTestReportDebug - - mkdir -p $CIRCLE_TEST_REPORTS/jacoco/ - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/jacoco/debug/. $CIRCLE_TEST_REPORTS/jacoco - - bash <(curl -s https://codecov.io/bash) - -deployment: - master: - branch: master - commands: - - (./gradlew bintrayUpload): - timeout: 360 - - bash ./misc/download_google_publisher_json.sh - - ./gradlew publishApkRelease diff --git a/demoapp-wearable/build.gradle b/demoapp-wearable/build.gradle index 98d5307d..666b353e 100644 --- a/demoapp-wearable/build.gradle +++ b/demoapp-wearable/build.gradle @@ -16,17 +16,14 @@ buildscript { repositories { - jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:3.1.4' } } -apply plugin: 'com.android.application' -repositories { - jcenter() -} +apply plugin: 'com.android.application' android { compileSdkVersion theCompileSdkVersion @@ -41,9 +38,10 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_6 - targetCompatibility JavaVersion.VERSION_1_6 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } + buildTypes { release { minifyEnabled false @@ -54,6 +52,6 @@ android { dependencies { compile project(':androidplot-core') - compile 'com.google.android.support:wearable:1.3.0' + compile 'com.google.android.support:wearable:2.3.0' compile 'com.google.android.gms:play-services-wearable:8.3.0' } diff --git a/demoapp/build.gradle b/demoapp/build.gradle index 141a6f80..2c2fbd52 100644 --- a/demoapp/build.gradle +++ b/demoapp/build.gradle @@ -14,37 +14,54 @@ * limitations under the License. */ -apply plugin: 'com.android.application' -apply plugin: 'com.github.triplet.play' - -dependencies { - compile project(':androidplot-core') - compile 'com.crittercism:crittercism-android-agent:5.4.0' - - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5' - testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5' -} - buildscript { - + ext.kotlin_version = '2.0.0' repositories { mavenCentral() } - dependencies { - classpath 'com.github.triplet.gradle:play-publisher:1.1.5' + classpath 'com.github.triplet.gradle:play-publisher:3.6.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } +plugins { + id 'com.android.application' + id 'com.github.triplet.play' version '3.6.0' +} + +apply plugin: 'kotlin-android' + +dependencies { + implementation "androidx.core:core-ktx:1.13.1" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation project(':androidplot-core') + implementation 'androidx.recyclerview:recyclerview:1.3.2' + + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' +} + +kotlin { + jvmToolchain(17) +} + android { + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + compileSdkVersion theCompileSdkVersion - buildToolsVersion theBuildToolsVersion + + buildFeatures { + viewBinding true + } defaultConfig { versionCode theVersionCode versionName theVersionName - minSdkVersion 11 + minSdkVersion 19 targetSdkVersion theTargetSdkVersion applicationId "com.androidplot.demos" } @@ -68,18 +85,18 @@ android { debuggable true } } - - - /** - * TODO: uncomment this and address all the lint issues. - */ - lintOptions { + lint { abortOnError false } + namespace 'com.androidplot.demos' + } play { // see: https://github.com/Triple-T/gradle-play-publisher - jsonFile = file(System.getenv("PUBLISHER_ACCT_JSON_FILE") ?: "publisher.json") + serviceAccountCredentials.set(file(System.getenv("PUBLISHER_ACCT_JSON_FILE") ?: "publisher.json")) track = 'beta' } +repositories { + mavenCentral() +} diff --git a/demoapp/project.properties b/demoapp/project.properties index 29656f2d..90a2cb18 100644 --- a/demoapp/project.properties +++ b/demoapp/project.properties @@ -39,5 +39,3 @@ #proguard.config=${sdk.dir}\tools\proguard\proguard-android.txt:proguard-project.txt # using config with optimization enabled so that things like log statement removal will work: proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt -# Project target. -target=android-16 diff --git a/demoapp/src/main/AndroidManifest.xml b/demoapp/src/main/AndroidManifest.xml index 89f1428f..5b73a2b7 100644 --- a/demoapp/src/main/AndroidManifest.xml +++ b/demoapp/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + @@ -27,75 +27,88 @@ android:icon="@drawable/ic_launcher"> + android:label="@string/app_name" + android:exported="true"> + android:label="@string/app_name" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> + android:screenOrientation="landscape" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> + android:label="@string/app_name" + android:exported="false"> - + + @@ -111,7 +124,8 @@ + android:name="com.androidplot.demos.widget.DemoAppWidgetProvider" + android:exported="false"> diff --git a/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java index 7c2cdaee..43d3adc0 100644 --- a/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java @@ -21,6 +21,7 @@ import android.app.Activity; import android.graphics.Color; import android.os.Bundle; +import androidx.annotation.NonNull; import android.view.animation.AccelerateDecelerateInterpolator; import com.androidplot.xy.BoundaryMode; @@ -96,12 +97,14 @@ public void onCreate(Bundle savedInstanceState) plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() { @Override - public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + public StringBuffer format(Object obj, + @NonNull StringBuffer toAppendTo, + @NonNull FieldPosition pos) { int i = Math.round(((Number) obj).floatValue()); return toAppendTo.append(domainLabels[i]); } @Override - public Object parseObject(String source, ParsePosition pos) { + public Object parseObject(String source, @NonNull ParsePosition pos) { return null; } }); diff --git a/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java index 8d8720d1..3cc2faa4 100644 --- a/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java @@ -22,6 +22,7 @@ import java.text.ParsePosition; import java.util.Arrays; +import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Color; import android.graphics.Paint; @@ -86,6 +87,7 @@ private enum SeriesSize { private Pair selection; + @SuppressLint("ClickableViewAccessibility") @Override public void onCreate(Bundle savedInstanceState) { diff --git a/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java index 432b16ed..86ae1fbb 100644 --- a/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java @@ -21,15 +21,28 @@ import android.graphics.DashPathEffect; import android.graphics.Paint; import android.os.Bundle; +import androidx.annotation.NonNull; -import com.androidplot.*; -import com.androidplot.util.*; +import com.androidplot.Region; +import com.androidplot.util.PixelUtils; +import com.androidplot.util.SeriesUtils; +import com.androidplot.xy.BoundaryMode; import com.androidplot.xy.CandlestickFormatter; import com.androidplot.xy.CandlestickMaker; import com.androidplot.xy.CandlestickSeries; -import com.androidplot.xy.*; - -import java.text.*; +import com.androidplot.xy.CatmullRomInterpolator; +import com.androidplot.xy.LineAndPointFormatter; +import com.androidplot.xy.PointLabelFormatter; +import com.androidplot.xy.PointLabeler; +import com.androidplot.xy.StepMode; +import com.androidplot.xy.XYGraphWidget; +import com.androidplot.xy.XYPlot; +import com.androidplot.xy.XYSeries; + +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; /** * A simple example of a candlestick chart rendered on an {@link XYPlot}. @@ -98,8 +111,8 @@ public void onCreate(Bundle savedInstanceState) { plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1); plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() { @Override - public StringBuffer format(Object object, StringBuffer buffer, - FieldPosition field) { + public StringBuffer format(Object object, @NonNull StringBuffer buffer, + @NonNull FieldPosition field) { int day = ((Number) object).intValue() % 7; switch (day) { case 0: @@ -130,7 +143,7 @@ public StringBuffer format(Object object, StringBuffer buffer, } @Override - public Object parseObject(String string, ParsePosition position) { + public Object parseObject(String string, @NonNull ParsePosition position) { return null; } }); diff --git a/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java b/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java index f8c00eae..df3d6117 100644 --- a/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java +++ b/demoapp/src/main/java/com/androidplot/demos/DemoApplication.java @@ -2,23 +2,10 @@ import android.app.*; -import com.squareup.leakcanary.*; - -/** - * Created by halfhp on 10/1/16. - */ public class DemoApplication extends Application { @Override public void onCreate() { super.onCreate(); - initLeakCanary(); - } - - protected void initLeakCanary() { - if (LeakCanary.isInAnalyzerProcess(this)) { - return; - } - LeakCanary.install(this); } } diff --git a/demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java b/demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java index 492fe332..f0116121 100644 --- a/demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java @@ -20,6 +20,7 @@ import android.graphics.Color; import android.graphics.DashPathEffect; import android.os.Bundle; +import androidx.annotation.NonNull; import com.androidplot.util.PixelUtils; import com.androidplot.xy.BoundaryMode; @@ -30,7 +31,6 @@ import com.androidplot.xy.StepMode; import com.androidplot.xy.XYGraphWidget; import com.androidplot.xy.XYPlot; -import com.androidplot.xy.XYSeries; import java.text.DecimalFormat; import java.text.FieldPosition; @@ -105,15 +105,15 @@ public void onCreate(Bundle savedInstanceState) final DecimalFormat df = new DecimalFormat("$#,###"); @Override - public StringBuffer format(Object o, StringBuffer stringBuffer, - FieldPosition fieldPosition) { + public StringBuffer format(Object o, @NonNull StringBuffer stringBuffer, + @NonNull FieldPosition fieldPosition) { final Number cost = costsSeries.denormalizeYVal((Number) o); stringBuffer.append(df.format(cost.doubleValue())); return stringBuffer; } @Override - public Object parseObject(String s, ParsePosition parsePosition) { + public Object parseObject(String s, @NonNull ParsePosition parsePosition) { return null; } }); @@ -124,15 +124,15 @@ public Object parseObject(String s, ParsePosition parsePosition) { final DecimalFormat df = new DecimalFormat("$#.##"); @Override - public StringBuffer format(Object o, StringBuffer stringBuffer, - FieldPosition fieldPosition) { + public StringBuffer format(Object o, @NonNull StringBuffer stringBuffer, + @NonNull FieldPosition fieldPosition) { Number minWage = minWageSeries.denormalizeYVal((Number) o); stringBuffer.append(df.format(minWage.doubleValue())); return stringBuffer; } @Override - public Object parseObject(String s, ParsePosition parsePosition) { + public Object parseObject(String s, @NonNull ParsePosition parsePosition) { return null; } }); @@ -141,8 +141,8 @@ public Object parseObject(String s, ParsePosition parsePosition) { plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() { @Override - public StringBuffer format(Object o, StringBuffer stringBuffer, - FieldPosition fieldPosition) { + public StringBuffer format(Object o, @NonNull StringBuffer stringBuffer, + @NonNull FieldPosition fieldPosition) { Number year = ((Number) o).intValue() + 2012; stringBuffer.append(year); @@ -150,7 +150,7 @@ public StringBuffer format(Object o, StringBuffer stringBuffer, } @Override - public Object parseObject(String s, ParsePosition parsePosition) { + public Object parseObject(String s, @NonNull ParsePosition parsePosition) { return null; } }); diff --git a/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java index f76dca01..f18b518b 100644 --- a/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java @@ -138,8 +138,8 @@ public void notifyObservers() { private static final int MAX_AMP_SEED = 100; private static final int MIN_AMP_SEED = 10; private static final int AMP_STEP = 1; - public static final int SINE1 = 0; - public static final int SINE2 = 1; + static final int SINE1 = 0; + static final int SINE2 = 1; private static final int SAMPLE_SIZE = 31; private int phase = 0; private int sinAmp = 1; @@ -150,7 +150,7 @@ public void notifyObservers() { notifier = new MyObservable(); } - public void stopThread() { + void stopThread() { keepRunning = false; } @@ -181,18 +181,18 @@ public void run() { } } - public int getItemCount(int series) { + int getItemCount(int series) { return SAMPLE_SIZE; } - public Number getX(int series, int index) { + Number getX(int series, int index) { if (index >= SAMPLE_SIZE) { throw new IllegalArgumentException(); } return index; } - public Number getY(int series, int index) { + Number getY(int series, int index) { if (index >= SAMPLE_SIZE) { throw new IllegalArgumentException(); } @@ -208,7 +208,7 @@ public Number getY(int series, int index) { } } - public void addObserver(Observer observer) { + void addObserver(Observer observer) { notifier.addObserver(observer); } @@ -223,7 +223,7 @@ class SampleDynamicSeries implements XYSeries { private int seriesIndex; private String title; - public SampleDynamicSeries(SampleDynamicXYDatasource datasource, int seriesIndex, String title) { + SampleDynamicSeries(SampleDynamicXYDatasource datasource, int seriesIndex, String title) { this.datasource = datasource; this.seriesIndex = seriesIndex; this.title = title; diff --git a/demoapp/src/main/java/com/androidplot/demos/ECGExample.java b/demoapp/src/main/java/com/androidplot/demos/ECGExample.java index 31d71e87..2bd7dec5 100644 --- a/demoapp/src/main/java/com/androidplot/demos/ECGExample.java +++ b/demoapp/src/main/java/com/androidplot/demos/ECGExample.java @@ -82,7 +82,7 @@ public static class MyFadeFormatter extends AdvancedLineAndPointRenderer.Formatt private int trailSize; - public MyFadeFormatter(int trailSize) { + MyFadeFormatter(int trailSize) { this.trailSize = trailSize; } @@ -125,7 +125,7 @@ public static class ECGModel implements XYSeries { * @param size Sample size contained within this model * @param updateFreqHz Frequency at which new samples are added to the model */ - public ECGModel(int size, int updateFreqHz) { + ECGModel(int size, int updateFreqHz) { data = new Number[size]; for(int i = 0; i < data.length; i++) { data[i] = 0; @@ -176,7 +176,7 @@ public void run() { }); } - public void start(final WeakReference rendererRef) { + void start(final WeakReference rendererRef) { this.rendererRef = rendererRef; keepRunning = true; thread.start(); diff --git a/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java b/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java index 47acf1cb..290b7802 100644 --- a/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java @@ -20,15 +20,21 @@ import android.content.Context; import android.graphics.Color; import android.os.Bundle; +import androidx.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; + import com.androidplot.Plot; import com.androidplot.ui.SeriesBundle; import com.androidplot.util.PixelUtils; -import com.androidplot.xy.*; +import com.androidplot.xy.CatmullRomInterpolator; +import com.androidplot.xy.LineAndPointFormatter; +import com.androidplot.xy.SimpleXYSeries; +import com.androidplot.xy.XYPlot; +import com.androidplot.xy.XYSeries; import java.util.ArrayList; import java.util.List; @@ -72,10 +78,10 @@ protected void generateData() { double bp = Math.random(); LineAndPointFormatter lpf = new LineAndPointFormatter( - Color.rgb(new Double(rl * 255).intValue(), - new Double(gl * 255).intValue(), new Double(bl * 255).intValue()), - Color.rgb(new Double(rp * 255).intValue(), - new Double(gp * 255).intValue(), new Double(bp * 255).intValue()), + Color.rgb(Double.valueOf(rl * 255).intValue(), + Double.valueOf(gl * 255).intValue(), Double.valueOf(bl * 255).intValue()), + Color.rgb(Double.valueOf(rp * 255).intValue(), + Double.valueOf(gp * 255).intValue(), Double.valueOf(bp * 255).intValue()), null, null); // for fun, configure interpolation on the formatter: @@ -100,8 +106,9 @@ public int getCount() { return NUM_PLOTS; } + @NonNull @Override - public View getView(int pos, View convertView, ViewGroup parent) { + public View getView(int pos, View convertView, @NonNull ViewGroup parent) { LayoutInflater inf = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = convertView; diff --git a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java b/demoapp/src/main/java/com/androidplot/demos/MainActivity.java deleted file mode 100644 index 86dadb68..00000000 --- a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot.demos; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.widget.Button; -import com.crittercism.app.Crittercism; - -public class MainActivity extends Activity { - - private static final String TAG = MainActivity.class.getName(); - - // DO *NOT* CHANGE THIS LINE! (CI-MATCH-POPULATE) - private static final String CRITTERCISM_APP_ID = null; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (CRITTERCISM_APP_ID != null) { - Log.d(TAG, "Crittercism initialized."); - Crittercism.initialize(getApplicationContext(), CRITTERCISM_APP_ID); - } - - setContentView(R.layout.main); - - Button startAnimatedXYPlotExButton = (Button) findViewById(R.id.animatedXYPlotExButton); - startAnimatedXYPlotExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, AnimatedXYPlotActivity.class)); - } - }); - - Button startScatterPlotExButton = (Button) findViewById(R.id.startScatterExButton); - startScatterPlotExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, ScatterPlotActivity.class)); - } - }); - - Button startSimplePieExButton = (Button) findViewById(R.id.startSimplePieExButton); - startSimplePieExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, SimplePieChartActivity.class)); - } - }); - - Button startDynamicXYExButton = (Button) findViewById(R.id.startDynamicXYExButton); - startDynamicXYExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, DynamicXYPlotActivity.class)); - } - }); - - Button startCandlestickExButton = (Button) findViewById(R.id.startCandlestickExButton); - startCandlestickExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, CandlestickChartActivity.class)); - } - }); - - Button startSimpleXYExButton = (Button) findViewById(R.id.startSimpleXYExButton); - startSimpleXYExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, SimpleXYPlotActivity.class)); - } - }); - - Button startBarPlotExButton = (Button) findViewById(R.id.startBarPlotExButton); - startBarPlotExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, BarPlotExampleActivity.class)); - } - }); - - Button startOrSensorExButton = (Button) findViewById(R.id.startOrSensorExButton); - startOrSensorExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, OrientationSensorExampleActivity.class)); - } - }); - - Button startDualScaleExButton = (Button) findViewById(R.id.startDualScaleExButton); - startDualScaleExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, DualScaleActivity.class)); - } - }); - - Button startTimeSeriesExButon = (Button) findViewById(R.id.startTimeSeriesExButton); - startTimeSeriesExButon.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, TimeSeriesActivity.class)); - } - }); - - Button startStepChartExButton = (Button) findViewById(R.id.startStepChartExButton); - startStepChartExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, StepChartExampleActivity.class)); - } - }); - - Button startScrollZoomExButton = (Button) findViewById(R.id.startScrollZoomButton); - startScrollZoomExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, TouchZoomExampleActivity.class)); - } - }); - - Button startXyRegionExampleButton = (Button) findViewById(R.id.startXyRegionExampleButton); - startXyRegionExampleButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, XYRegionExampleActivity.class)); - } - }); - - - Button listViewExButton = (Button) findViewById(R.id.startXyListViewExButton); - listViewExButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, ListViewActivity.class)); - } - }); - - Button startXYPlotWithBgImgExampleButton = (Button) findViewById(R.id.startXYPlotWithBgImgExample); - startXYPlotWithBgImgExampleButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, XYPlotWithBgImgActivity.class)); - } - }); - - // ECG - Button startECGExampleButton = (Button) findViewById(R.id.startECGExample); - startECGExampleButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, ECGExample.class)); - } - }); - - // f(x) plot - Button fxPlotExampleButton = (Button) findViewById(R.id.fxPlotExample); - fxPlotExampleButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, FXPlotExampleActivity.class)); - } - }); - - // bubble chart - Button bubbleChartButton = (Button) findViewById(R.id.bubbleChartExample); - bubbleChartButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(MainActivity.this, BubbleChartActivity.class)); - } - }); - } -} diff --git a/demoapp/src/main/java/com/androidplot/demos/MainActivity.kt b/demoapp/src/main/java/com/androidplot/demos/MainActivity.kt new file mode 100644 index 00000000..6632af40 --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/MainActivity.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2021 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.androidplot.demos + +import android.app.Activity +import android.os.Bundle +import android.content.Intent +import com.androidplot.demos.databinding.MainBinding + +class MainActivity : Activity() { + + private lateinit var binding: MainBinding + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = MainBinding.inflate(layoutInflater) + + binding.animatedXYPlotExButton.setOnClickListener { + startActivity(Intent(this, AnimatedXYPlotActivity::class.java)) + } + + binding.startScatterExButton.setOnClickListener { + startActivity(Intent(this, ScatterPlotActivity::class.java)) + } + + binding.startSimplePieExButton.setOnClickListener { + startActivity(Intent(this, SimplePieChartActivity::class.java)) + } + + binding.startDynamicXYExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, DynamicXYPlotActivity::class.java)) + } + + binding.startCandlestickExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, CandlestickChartActivity::class.java)) + } + + binding.startSimpleXYExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, SimpleXYPlotActivity::class.java)) + } + + binding.startBarPlotExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, BarPlotExampleActivity::class.java)) + } + + binding.startOrSensorExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, OrientationSensorExampleActivity::class.java)) + } + + binding.startDualScaleExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, DualScaleActivity::class.java)) + } + + binding.startTimeSeriesExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, TimeSeriesActivity::class.java)) + } + + binding.startStepChartExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, StepChartExampleActivity::class.java)) + } + + binding.startScrollZoomButton.setOnClickListener { + startActivity(Intent(this@MainActivity, TouchZoomExampleActivity::class.java)) + } + + binding.startXyRegionExampleButton.setOnClickListener { + startActivity(Intent(this@MainActivity, XYRegionExampleActivity::class.java)) + } + + binding.startXyListViewExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, ListViewActivity::class.java)) + } + + binding.startXyRecyclerViewExButton.setOnClickListener { + startActivity(Intent(this@MainActivity, RecyclerViewActivity::class.java)) + } + + binding.startXYPlotWithBgImgExample.setOnClickListener { + startActivity(Intent(this@MainActivity, XYPlotWithBgImgActivity::class.java)) + } + + binding.startECGExample.setOnClickListener { + startActivity(Intent(this@MainActivity, ECGExample::class.java)) + } + + binding.fxPlotExample.setOnClickListener { + startActivity(Intent(this@MainActivity, FXPlotExampleActivity::class.java)) + } + + binding.bubbleChartExample.setOnClickListener { + startActivity(Intent(this@MainActivity, BubbleChartActivity::class.java)) + } + + setContentView(binding.root) + } +} \ No newline at end of file diff --git a/demoapp/src/main/java/com/androidplot/demos/RecyclerViewActivity.kt b/demoapp/src/main/java/com/androidplot/demos/RecyclerViewActivity.kt new file mode 100644 index 00000000..194ee6e0 --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/RecyclerViewActivity.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2021 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.androidplot.demos + +import android.app.Activity +import android.graphics.Color +import com.androidplot.ui.SeriesBundle +import android.os.Bundle +import android.view.ViewGroup +import android.view.LayoutInflater +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.androidplot.demos.databinding.RecyclerviewExampleBinding +import com.androidplot.demos.databinding.RecyclerviewExampleItemBinding +import com.androidplot.util.PixelUtils +import com.androidplot.xy.* +import java.util.* + +class RecyclerViewActivity : Activity() { + private lateinit var binding: RecyclerviewExampleBinding + + companion object { + private const val NUM_PLOTS = 10 + private const val NUM_POINTS_PER_SERIES = 10 + private const val NUM_SERIES_PER_PLOT = 5 + } + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + PixelUtils.init(this) + + binding = RecyclerviewExampleBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = MyRecyclerViewAdapter() + } + + class MyRecyclerViewHolder( + private val binding: RecyclerviewExampleItemBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: List>, title: String) { + val plot = binding.plot + plot.clear() + plot.title.text = title + data.map { plot.addSeries(it.series, it.formatter) } + plot.redraw() + } + } + + class MyRecyclerViewAdapter : RecyclerView.Adapter() { + private val seriesData = generateData() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyRecyclerViewHolder { + val itemBinding = RecyclerviewExampleItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return MyRecyclerViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: MyRecyclerViewHolder, position: Int) { + holder.bind(seriesData[position], "Series $position") + } + + override fun getItemCount() = seriesData.size + + private fun generateData(): List>> { + val theData = mutableListOf>>() + fun generateBundle(seriesLabel: String): SeriesBundle { + val generator = Random() + val nums = ArrayList() + for (j in 0 until NUM_POINTS_PER_SERIES) { + nums.add(generator.nextFloat()) + } + + val formatter = LineAndPointFormatter( + Color.rgb( + java.lang.Double.valueOf(Math.random() * 255).toInt(), + java.lang.Double.valueOf(Math.random() * 255).toInt(), + java.lang.Double.valueOf(Math.random() * 255).toInt() + ), + Color.rgb( + java.lang.Double.valueOf(Math.random() * 255).toInt(), + java.lang.Double.valueOf(Math.random() * 255).toInt(), + java.lang.Double.valueOf(Math.random() * 255).toInt() + ), + null, null + ) + + // for fun, configure interpolation on the formatter: + formatter.interpolationParams = CatmullRomInterpolator.Params( + 20, + CatmullRomInterpolator.Type.Centripetal + ) + + return SeriesBundle( + SimpleXYSeries( + nums, + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, + seriesLabel + ), + formatter + ) + } + + for (i in 0 until NUM_PLOTS) { + val seriesList: MutableList> = + ArrayList(NUM_SERIES_PER_PLOT) + + for (k in 0 until NUM_SERIES_PER_PLOT) { + seriesList.add(generateBundle("S$k")) + } + theData.add(seriesList) + } + return theData + } + } +} \ No newline at end of file diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java index 850bc4d8..cf419083 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java @@ -18,6 +18,7 @@ import android.animation.Animator; import android.animation.ValueAnimator; +import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.*; import android.os.Bundle; @@ -52,6 +53,7 @@ public class SimplePieChartActivity extends Activity private Segment s3; private Segment s4; + @SuppressLint("ClickableViewAccessibility") @Override public void onCreate(Bundle savedInstanceState) { @@ -62,6 +64,9 @@ public void onCreate(Bundle savedInstanceState) // initialize our XYPlot reference: pie = (PieChart) findViewById(R.id.mySimplePieChart); + // enable the legend: + pie.getLegend().setVisible(true); + final float padding = PixelUtils.dpToPix(30); pie.getPie().setPadding(padding, padding, padding, padding); @@ -73,9 +78,9 @@ public boolean onTouch(View view, MotionEvent motionEvent) { if(pie.getPie().containsPoint(click)) { Segment segment = pie.getRenderer(PieRenderer.class).getContainingSegment(click); - deselectAll(); if(segment != null) { final boolean isSelected = getFormatter(segment).getOffset() != 0; + deselectAll(); setSelected(segment, !isSelected); pie.redraw(); } @@ -183,36 +188,10 @@ protected void setupIntroAnimation() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float scale = valueAnimator.getAnimatedFraction(); -// scalingSeries1.setScale(scale); -// scalingSeries2.setScale(scale); renderer.setExtentDegs(360 * scale); pie.redraw(); } }); -// animator.addListener(new Animator.AnimatorListener() { -// @Override -// public void onAnimationStart(Animator animator) { -// -// } -// -// @Override -// public void onAnimationEnd(Animator animator) { -// // the animation is over, so show point labels: -// series1Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); -// series2Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); -// plot.redraw(); -// } -// -// @Override -// public void onAnimationCancel(Animator animator) { -// -// } -// -// @Override -// public void onAnimationRepeat(Animator animator) { -// -// } -// }); // the animation will run for 1.5 seconds: animator.setDuration(1500); diff --git a/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java index 904f02c7..db3a5a28 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 AndroidPlot.com + * Copyright 2018 AndroidPlot.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,19 +15,24 @@ */ package com.androidplot.demos; + import android.app.Activity; -import android.graphics.*; +import android.graphics.DashPathEffect; import android.os.Bundle; +import androidx.annotation.NonNull; import com.androidplot.util.PixelUtils; +import com.androidplot.xy.CatmullRomInterpolator; +import com.androidplot.xy.LineAndPointFormatter; import com.androidplot.xy.SimpleXYSeries; +import com.androidplot.xy.XYGraphWidget; +import com.androidplot.xy.XYPlot; import com.androidplot.xy.XYSeries; -import com.androidplot.xy.*; import java.text.FieldPosition; import java.text.Format; import java.text.ParsePosition; -import java.util.*; +import java.util.Arrays; /** * A simple XYPlot @@ -43,7 +48,7 @@ public void onCreate(Bundle savedInstanceState) setContentView(R.layout.simple_xy_plot_example); // initialize our XYPlot reference: - plot = (XYPlot) findViewById(R.id.plot); + plot = findViewById(R.id.plot); // create a couple arrays of y-values to plot: final Number[] domainLabels = {1, 2, 3, 6, 7, 8, 9, 10, 13, 14}; @@ -72,7 +77,7 @@ public void onCreate(Bundle savedInstanceState) PixelUtils.dpToPix(20), PixelUtils.dpToPix(15)}, 0)); - // just for fun, add some smoothing to the lines: + // (optional) add some smoothing to the lines: // see: http://androidplot.com/smooth-curves-and-androidplot/ series1Format.setInterpolationParams( new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal)); @@ -86,12 +91,12 @@ public void onCreate(Bundle savedInstanceState) plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() { @Override - public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + public StringBuffer format(Object obj, @NonNull StringBuffer toAppendTo, @NonNull FieldPosition pos) { int i = Math.round(((Number) obj).floatValue()); return toAppendTo.append(domainLabels[i]); } @Override - public Object parseObject(String source, ParsePosition pos) { + public Object parseObject(String source, @NonNull ParsePosition pos) { return null; } }); diff --git a/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java index 5caf8651..4b1fdc35 100644 --- a/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/StepChartExampleActivity.java @@ -18,12 +18,20 @@ import android.app.Activity; -import android.graphics.*; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Shader; import android.os.Bundle; +import androidx.annotation.NonNull; + import com.androidplot.util.PixelUtils; import com.androidplot.xy.SimpleXYSeries; +import com.androidplot.xy.StepFormatter; +import com.androidplot.xy.StepMode; +import com.androidplot.xy.XYGraphWidget; +import com.androidplot.xy.XYPlot; import com.androidplot.xy.XYSeries; -import com.androidplot.xy.*; import java.text.DecimalFormat; import java.text.FieldPosition; @@ -82,8 +90,8 @@ public void onCreate(Bundle savedInstanceState) // create a custom getFormatter to draw our state names as range tick labels: mySimpleXYPlot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).setFormat(new Format() { @Override - public StringBuffer format(Object obj, StringBuffer toAppendTo, - FieldPosition pos) { + public StringBuffer format(Object obj, @NonNull StringBuffer toAppendTo, + @NonNull FieldPosition pos) { Number num = (Number) obj; switch (num.intValue()) { case 1: @@ -106,7 +114,7 @@ public StringBuffer format(Object obj, StringBuffer toAppendTo, } @Override - public Object parseObject(String source, ParsePosition pos) { + public Object parseObject(String source, @NonNull ParsePosition pos) { return null; } }); diff --git a/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java b/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java index e457ad0f..12930bd7 100644 --- a/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/TimeSeriesActivity.java @@ -16,21 +16,38 @@ package com.androidplot.demos; +import android.annotation.SuppressLint; import android.app.Activity; -import android.graphics.*; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; import android.os.Bundle; +import androidx.annotation.NonNull; +import com.androidplot.util.PixelUtils; +import com.androidplot.xy.BoundaryMode; +import com.androidplot.xy.LineAndPointFormatter; import com.androidplot.xy.SimpleXYSeries; -import com.androidplot.xy.XYSeries; -import com.androidplot.xy.*; - -import java.text.*; +import com.androidplot.xy.StepMode; +import com.androidplot.xy.XYGraphWidget; +import com.androidplot.xy.XYPlot; + +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.Calendar; import java.util.Date; +import java.util.GregorianCalendar; public class TimeSeriesActivity extends Activity { + private static final String SERIES_TITLE = "Signthings in USA"; + private XYPlot plot1; + private SimpleXYSeries series; @Override public void onCreate(Bundle savedInstanceState) { @@ -38,43 +55,36 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.time_series_example); plot1 = (XYPlot) findViewById(R.id.plot1); - Number[] numSightings = {5, 8, 6, 9, 3, 8, 5}; - - // an array of years in milliseconds: - Number[] years = { - 978307200, // 2001 - 998309300, - 1009843200, // 2002 - 1041379200, // 2003 - 1052012100, - 1072915200, // 2004 - 1104537600 // 2005 + + // these will be our domain index labels: + final Date[] years = { + new GregorianCalendar(2001, Calendar.JANUARY, 1).getTime(), + new GregorianCalendar(2001, Calendar.JULY, 1).getTime(), + new GregorianCalendar(2002, Calendar.JANUARY, 1).getTime(), + new GregorianCalendar(2002, Calendar.JULY, 1).getTime(), + new GregorianCalendar(2003, Calendar.JANUARY, 1).getTime(), + new GregorianCalendar(2003, Calendar.JULY, 1).getTime(), + new GregorianCalendar(2004, Calendar.JANUARY, 1).getTime(), + new GregorianCalendar(2004, Calendar.JULY, 1).getTime(), + new GregorianCalendar(2005, Calendar.JANUARY, 1).getTime(), + new GregorianCalendar(2005, Calendar.JULY, 1).getTime() }; - // create our series from our array of nums: - XYSeries series2 = new SimpleXYSeries( - Arrays.asList(years), - Arrays.asList(numSightings), - "Sightings in USA"); + + addSeries(savedInstanceState); + + plot1.setRangeBoundaries(0, 10, BoundaryMode.FIXED); plot1.getGraph().getGridBackgroundPaint().setColor(Color.WHITE); plot1.getGraph().getDomainGridLinePaint().setColor(Color.BLACK); plot1.getGraph().getDomainGridLinePaint(). - setPathEffect(new DashPathEffect(new float[] {1, 1}, 1)); + setPathEffect(new DashPathEffect(new float[]{1, 1}, 1)); plot1.getGraph().getRangeGridLinePaint().setColor(Color.BLACK); plot1.getGraph().getRangeGridLinePaint(). - setPathEffect(new DashPathEffect(new float[] {1, 1}, 1)); + setPathEffect(new DashPathEffect(new float[]{1, 1}, 1)); plot1.getGraph().getDomainOriginLinePaint().setColor(Color.BLACK); plot1.getGraph().getRangeOriginLinePaint().setColor(Color.BLACK); - // setup our line fill paint to be a slightly transparent gradient: - Paint lineFill = new Paint(); - lineFill.setAlpha(200); - - LineAndPointFormatter formatter = - new LineAndPointFormatter(Color.rgb(0, 0, 0), Color.BLUE, Color.RED, null); - formatter.setFillPaint(lineFill); plot1.getGraph().setPaddingRight(2); - plot1.addSeries(series2, formatter); // draw a domain tick for each year: plot1.setDomainStep(StepMode.SUBDIVIDE, years.length); @@ -93,24 +103,65 @@ public void onCreate(Bundle savedInstanceState) { // create a simple date format that draws on the year portion of our timestamp. // see http://download.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html // for a full description of SimpleDateFormat. - private SimpleDateFormat dateFormat = new SimpleDateFormat("MM-yyyy"); + @SuppressLint("SimpleDateFormat") + private final SimpleDateFormat dateFormat = new SimpleDateFormat("MMM yyyy"); @Override - public StringBuffer format(Object obj, StringBuffer toAppendTo, - FieldPosition pos) { - - // because our timestamps are in seconds and SimpleDateFormat expects milliseconds - // we multiply our timestamp by 1000: - long timestamp = ((Number) obj).longValue() * 1000; - Date date = new Date(timestamp); - return dateFormat.format(date, toAppendTo, pos); + public StringBuffer format(Object obj, + @NonNull StringBuffer toAppendTo, + @NonNull FieldPosition pos) { + + // this rounding is necessary to avoid precision loss when converting from + // double back to int: + int yearIndex = (int) Math.round(((Number) obj).doubleValue()); + return dateFormat.format(years[yearIndex], toAppendTo, pos); } @Override - public Object parseObject(String source, ParsePosition pos) { + public Object parseObject(String source, @NonNull ParsePosition pos) { return null; } }); } + + /** + * Instantiates our XYSeries, checking the current savedInstanceState for existing series data + * to avoid having to regenerate on each resume. If your series data is small and easy to + * regenerate (as it is here) then you can skip saving/restoring your series data to + * savedInstanceState. + * @param savedInstanceState Current saved instance state, if any; may be null. + */ + private void addSeries(Bundle savedInstanceState) { + Number[] yVals; + + if(savedInstanceState != null) { + yVals = (Number[]) savedInstanceState.getSerializable(SERIES_TITLE); + } else { + yVals = new Number[]{5, 8, 6, 9, 3, 8, 5, 4, 7, 4}; + } + + // create our series from our array of nums: + series = new SimpleXYSeries(Arrays.asList(yVals), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, SERIES_TITLE); + + LineAndPointFormatter formatter = + new LineAndPointFormatter(Color.rgb(0, 0, 0), Color.RED, Color.RED, null); + formatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(10)); + formatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(5)); + + // setup our line fill paint to be a slightly transparent gradient: + Paint lineFill = new Paint(); + lineFill.setAlpha(200); + + formatter.setFillPaint(lineFill); + + plot1.addSeries(series, formatter); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + // persist our series data so we don't have to regenerate each time: + bundle.putSerializable(SERIES_TITLE, series.getyVals().toArray(new Number[]{})); + } } \ No newline at end of file diff --git a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java index ebf493d3..dfae19d9 100644 --- a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java @@ -31,6 +31,7 @@ public class TouchZoomExampleActivity extends Activity { private static final int SERIES_SIZE = 3000; private static final int SERIES_ALPHA = 255; + private static final int NUM_GRIDLINES = 5; private XYPlot plot; private PanZoom panZoom; private Button resetButton; @@ -40,24 +41,30 @@ public class TouchZoomExampleActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.touch_zoom_example); - resetButton = (Button) findViewById(R.id.resetButton); + resetButton = findViewById(R.id.resetButton); resetButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { reset(); } }); - plot = (XYPlot) findViewById(R.id.plot); + plot = findViewById(R.id.plot); // set a fixed origin and a "by-value" step mode so that grid lines will // move dynamically with the data when the users pans or zooms: plot.setUserDomainOrigin(0); plot.setUserRangeOrigin(0); - plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 500); - plot.setRangeStep(StepMode.INCREMENT_BY_VAL, 100); - panSpinner = (Spinner) findViewById(R.id.pan_spinner); - zoomSpinner = (Spinner) findViewById(R.id.zoom_spinner); + // predefine the stepping of both axis + // increment will be chosen from list to best fit NUM_GRIDLINES grid lines + double[] inc_domain = new double[]{10,50,100,500}; + double[] inc_range = new double[]{1,5,10,20,50,100}; + plot.setDomainStepModel(new StepModelFit(plot.getBounds().getxRegion(),inc_domain,NUM_GRIDLINES)); + plot.setRangeStepModel( new StepModelFit(plot.getBounds().getyRegion(),inc_range,NUM_GRIDLINES)); + + + panSpinner = findViewById(R.id.pan_spinner); + zoomSpinner = findViewById(R.id.zoom_spinner); plot.getGraph().setLinesPerRangeLabel(2); plot.getGraph().setLinesPerDomainLabel(2); plot.getGraph().getBackgroundPaint().setColor(Color.TRANSPARENT); @@ -71,27 +78,17 @@ public void onClick(View view) { plot.setBorderStyle(Plot.BorderStyle.NONE, null, null); - panZoom = PanZoom.attach(plot); + panZoom = PanZoom.attach(plot, PanZoom.Pan.BOTH, PanZoom.Zoom.STRETCH_BOTH, PanZoom.ZoomLimit.MIN_TICKS); plot.getOuterLimits().set(0, 3000, 0, 1000); initSpinners(); // enable autoselect of sampling level based on visible boundaries: plot.getRegistry().setEstimator(new ZoomEstimator()); - if(savedInstanceState != null && savedInstanceState.containsKey("seriesRegistry")) { - XYSeriesRegistry registry = (XYSeriesRegistry) savedInstanceState.getSerializable("seriesRegistry"); - plot.setRegistry(registry); - } else { - generateSeriesData(); - } + generateSeriesData(); reset(); } - @Override - public void onSaveInstanceState(Bundle bundle) { - bundle.putSerializable("seriesRegistry", plot.getRegistry()); - } - private void reset() { plot.setDomainBoundaries(0, 10000, BoundaryMode.FIXED); plot.setRangeBoundaries(0, 1000, BoundaryMode.FIXED); @@ -155,6 +152,7 @@ public void onNothingSelected(AdapterView parent) { } }); + zoomSpinner.setAdapter( new ArrayAdapter<>(this, R.layout.spinner_item, PanZoom.Zoom.values())); zoomSpinner.setSelection(panZoom.getZoom().ordinal()); @@ -170,5 +168,19 @@ public void onNothingSelected(AdapterView parent) { } }); } + + // (optional) save the current pan/zoom state + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putSerializable("pan-zoom-state", panZoom.getState()); + } + + // (optional) restore the previously saved pan/zoom state + @Override + public void onRestoreInstanceState(Bundle bundle) { + PanZoom.State state = (PanZoom.State) bundle.getSerializable("pan-zoom-state"); + panZoom.setState(state); + plot.redraw(); + } } diff --git a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java index c9cc953d..0f48c3c2 100644 --- a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java @@ -69,8 +69,8 @@ public class XYRegionExampleActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.xyregion_example); - plot = (XYPlot) findViewById(R.id.xyRegionExamplePlot); - timCB = (CheckBox) findViewById(R.id.s1CheckBox); + plot = findViewById(R.id.xyRegionExamplePlot); + timCB = findViewById(R.id.s1CheckBox); timCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { @@ -78,7 +78,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { } }); - nickCB = (CheckBox) findViewById(R.id.s2CheckBox); + nickCB = findViewById(R.id.s2CheckBox); nickCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { @@ -86,7 +86,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { } }); - r2CheckBox = (CheckBox) findViewById(R.id.r2CheckBox); + r2CheckBox = findViewById(R.id.r2CheckBox); r2CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { @@ -94,7 +94,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { } }); - r3CheckBox = (CheckBox) findViewById(R.id.r3CheckBox); + r3CheckBox = findViewById(R.id.r3CheckBox); r3CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { @@ -102,7 +102,7 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { } }); - r4CheckBox = (CheckBox) findViewById(R.id.r4CheckBox); + r4CheckBox = findViewById(R.id.r4CheckBox); r4CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { diff --git a/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java b/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java index fe3627b3..592fbce3 100644 --- a/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java +++ b/demoapp/src/main/java/com/androidplot/demos/widget/DemoAppWidgetProvider.java @@ -25,6 +25,8 @@ import android.widget.RemoteViews; import com.androidplot.demos.R; import com.androidplot.ui.*; +import com.androidplot.util.PixelUtils; +import com.androidplot.xy.XYGraphWidget; import com.androidplot.xy.XYSeries; import com.androidplot.xy.LineAndPointFormatter; import com.androidplot.xy.SimpleXYSeries; @@ -43,10 +45,6 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a plot.getGraph().setMargins(0, 0, 0 , 0); plot.getGraph().setPadding(0, 0, 0, 0); -// plot.getGraphWidget().getDomainLineLabelPaint().setTextSize(PixelUtils.spToPix(10)); -// plot.getGraphWidget().getRangeLineLabelPaint().setTextSize(PixelUtils.spToPix(10)); -// plot.getGraphWidget().getDomainOriginLineLabelPaint().setTextSize(PixelUtils.spToPix(10)); -// plot.getGraphWidget().getRangeOriginLineLabelPaint().setTextSize(PixelUtils.spToPix(10)); plot.getGraph().position(0, HorizontalPositioning.ABSOLUTE_FROM_LEFT, 0, VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.LEFT_TOP); @@ -55,6 +53,15 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a plot.getLayoutManager().moveToTop(plot.getTitle()); + plot.getGraph().setLineLabelEdges(XYGraphWidget.Edge.LEFT, XYGraphWidget.Edge.BOTTOM); + plot.getGraph().getLineLabelInsets().setLeft(PixelUtils.dpToPix(16)); + plot.getGraph().getLineLabelInsets().setBottom(PixelUtils.dpToPix(4)); + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).getPaint().setColor(Color.RED); + plot.getGraph().getGridInsets().setTop(PixelUtils.dpToPix(12)); + plot.getGraph().getGridInsets().setRight(PixelUtils.dpToPix(12)); + plot.getGraph().getGridInsets().setLeft(PixelUtils.dpToPix(36)); + plot.getGraph().getGridInsets().setBottom(PixelUtils.dpToPix(16)); + plot.measure(w, h); plot.layout(0, 0, w, h); diff --git a/demoapp/src/main/res/layout/bar_plot_example.xml b/demoapp/src/main/res/layout/bar_plot_example.xml index e607f973..3182342d 100644 --- a/demoapp/src/main/res/layout/bar_plot_example.xml +++ b/demoapp/src/main/res/layout/bar_plot_example.xml @@ -1,5 +1,4 @@ - - + xmlns:ap="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + ap:title="Growth" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:progress="10" /> + android:progress="1" /> + android:checked="true" + android:text="Series 1" /> + android:checked="true" + android:text="Series 2" /> \ No newline at end of file diff --git a/demoapp/src/main/res/layout/demo_app_widget.xml b/demoapp/src/main/res/layout/demo_app_widget.xml index 85b5fe6b..81977453 100644 --- a/demoapp/src/main/res/layout/demo_app_widget.xml +++ b/demoapp/src/main/res/layout/demo_app_widget.xml @@ -17,14 +17,13 @@ --> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - - + \ No newline at end of file diff --git a/demoapp/src/main/res/layout/dynamic_xyplot_example.xml b/demoapp/src/main/res/layout/dynamic_xyplot_example.xml index ddfd0cba..5b0c5644 100644 --- a/demoapp/src/main/res/layout/dynamic_xyplot_example.xml +++ b/demoapp/src/main/res/layout/dynamic_xyplot_example.xml @@ -1,5 +1,4 @@ - - + xmlns:ap="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + ap:legendTextSize="15sp" + ap:rangeTitle="Range" + ap:title="A Dynamic XY Plot" /> diff --git a/demoapp/src/main/res/layout/main.xml b/demoapp/src/main/res/layout/main.xml index 65a43ef9..3a2d873f 100644 --- a/demoapp/src/main/res/layout/main.xml +++ b/demoapp/src/main/res/layout/main.xml @@ -32,99 +32,124 @@