This commit is contained in:
coco
2026-07-03 15:56:07 +08:00
commit caef23209c
5767 changed files with 1004268 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
*.ipr
*.iml
.gradle
local.properties
build
.metadata
AndroidPlot-Core/target
AndroidPlot-Core/.settings
DemoApp/.settings
DemoApp/bin
DemoApp/gen
DemoApp/target
.idea
**/R.java
+201
View File
@@ -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.
+57
View File
@@ -0,0 +1,57 @@
# ![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.
Androidplot is compatible with all versions of Android from 1.6 onward, works equally with Kotlin and Java codebases
and is compatible with Jetpack compose.
If you enjoy the lib, please [rate us on codix.io](http://codix.io/gh/repo/halfhp/androidplot)!
<img src="docs/images/screens/fx_vert.png" width="115"> <img src="docs/images/screens/candlestick_vert.png" width="115"> <img src="docs/images/screens/pie_vert.png" width="115"> <img src="docs/images/screens/scatter_vert.png" width="115"> <img src="docs/images/screens/step_vert.png" width="115"> <img src="docs/images/screens/bubble_vert.png" width="115"> <img src="docs/images/screens/bar_vert.png" width="115">
**Features:**
* Line Charts
* Scatter Charts
* Bar Charts
* Pie Charts
* Step Charts
* Candlestick Charts
* Bubble Charts
* Dynamic plots
* Pan & Zoom
# Usage
* **[Quickstart](docs/quickstart.md)** :star:
* [Full Documentation](docs/index.md)
# Links
* [Website](http://androidplot.com)
* [Github Repo](https://github.com/halfhp/androidplot)
* [Bitbucket Repo](https://bitbucket.org/androidplot/androidplot)
* [Demo App (Google Play Store)](https://play.google.com/store/apps/details?id=com.androidplot.demos&hl=en)
* [Demo App Soure Code](https://bitbucket.org/androidplot/androidplot/src/1538c5dfa56aed0d2cfdcbc7cdc6173e605543cd/demoapp/?at=master)
* [Bugs](https://github.com/halfhp/androidplot/issues) :ant:
* [Contributing Source Code](docs/contributing.md)
# Help
Technical questions should be posted using the [androidplot tag](http://stackoverflow.com/questions/tagged/androidplot) on Stack Overflow. For everything else use the [Google Groups forum](https://groups.google.com/d/forum/androidplot).
# License
Androidplot has been made available under the Apache 2.0 license:
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.
+1
View File
@@ -0,0 +1 @@
theme: jekyll-theme-minimal
+190
View File
@@ -0,0 +1,190 @@
/*
* 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.
*/
plugins {
id 'com.android.library'
id 'com.dicedmelon.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(/<!--\n?([\s\S]*?)\n?-->/) { 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
defaultConfig {
minSdkVersion theMinSdkVersion
targetSdkVersion theTargetSdkVersion
testApplicationId "com.androidplot.test"
}
testOptions {
unitTests.all {
jacoco {
includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}
}
}
lint {
abortOnError false
}
}
group = 'com.androidplot'
version = theVersionName
def siteUrl = 'http://androidplot.com'
def gitUrl = 'https://github.com/halfhp/androidplot.git'
dependencies {
implementation 'com.halfhp.fig:figlib:1.0.11'
implementation 'com.android.support:support-annotations:28.0.0'
testImplementation "org.mockito:mockito-core:4.0.0"
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
testImplementation "org.robolectric:robolectric:4.7.3"
}
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'
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
javadoc {
options.overview = "src/main/java/overview.html"
}
afterEvaluate {
publishing {
repositories {
maven {
name = "Maven Central"
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username = System.getenv("OSSRH_ACTOR")
password = System.getenv("OSSRH_TOKEN")
}
}
}
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'
}
}
scm {
connection = gitUrl
developerConnection = gitUrl
url = gitUrl
}
}
}
}
}
}
afterEvaluate {
signing {
def signingKey = System.getenv("SIGNING_KEY")
def signingPassword = System.getenv("SIGNING_PASSWORD")
useInMemoryPgpKeys(signingKey, signingPassword)
sign publishing.publications.release
}
}
artifacts {
archives javadocJar
archives sourcesJar
}
@@ -0,0 +1,20 @@
<!--
~ 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.
-->
<!-- This file has been intentionally left empty; values should be
set in android.defaultConfig in build.gradle wherever possible.-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.androidplot">
</manifest>
@@ -0,0 +1,30 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot;
import android.graphics.Paint;
public interface LineLabelFormatter {
/**
*
* @param value The value being rendered by this formatter.
* @return Paint instance that should be used to render the specified value.
*/
Paint getPaint(Number value);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
/*
* 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.graphics.Canvas;
/**
* Defines methods used for monitoring events generated by a Plot.
*/
public interface PlotListener {
/**
* Fired immediately before the Plot "source" is drawn onto canvas.
* Commonly used by implementing Series instances to activate a read
* lock on it's self in preparation for the Plot's imminent reading
* of that series.
* @param source
* @param canvas
*/
void onBeforeDraw(Plot source, Canvas canvas);
/**
* Fired immediately after the Plot "source" is drawn onto canvas.
* Just as onBeforeDraw(...) is commonly used by Series implementations
* to activate a read lock, this method is commonly used to release that
* same lock.
* @param source
* @param canvas
*/
void onAfterDraw(Plot source, Canvas canvas);
}
@@ -0,0 +1,268 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot;
import com.androidplot.util.FastNumber;
/**
* A one dimensional region represented by a starting and ending value.
*/
public class Region {
private FastNumber min;
private FastNumber max;
private FastNumber cachedLength;
private Region defaults = this;
public Region() {}
public static Region withDefaults(Region defaults) {
if(defaults == null || !defaults.isDefined()) {
throw new IllegalArgumentException("When specifying default min and max must both be non-null values");
}
Region r = new Region();
r.defaults = defaults;
return r;
}
public Region(Number v1, Number v2) {
if (v1 != null && v2 != null && v1.doubleValue() < v2.doubleValue()) {
this.setMin(v1);
this.setMax(v2);
} else {
this.setMin(v2);
this.setMax(v1);
}
}
public void setMinMax(Region region) {
setMin(region.getMin());
setMax(region.getMax());
}
/**
*
* @param v1
* @param v2
* @return The distance between val1 and val2 or null if either parameters are null.
* @since 0.9.7
*/
public static Number measure(Number v1, Number v2) {
return new Region(v1, v2).length();
}
public Number length() {
if(cachedLength == null) {
Number l = getMax() == null || getMin() == null ?
null : getMax().doubleValue() - getMin().doubleValue();
if(l != null) {
cachedLength = FastNumber.orNull(l);
}
}
return cachedLength;
}
/**
* Tests whether a value is within the given range
* @param value
* @return
*/
public boolean contains(Number value) {
return value.doubleValue() >= getMin().doubleValue() && value.doubleValue() <= getMax().doubleValue();
}
public boolean intersects(Region region) {
return intersects(region.getMin(), region.getMax());
}
/**
*
* @return Middle value within this region
*/
public Number center() {
return getMax().doubleValue() - (length().doubleValue() / 2);
}
/**
* Transform a value relative to this region into it's corresponding value relative to the
* specified region.
* @param value
* @param region2
* @return
*/
public Number transform(double value, Region region2) {
return transform(value, region2, false);
}
public Number transform(double value, Region region2, boolean flip) {
return transform(value, region2.getMin().doubleValue(), region2.getMax().doubleValue(), flip);
}
public double transform(double value, double min, double max, boolean flip) {
double range = length().doubleValue();
final double r2 = max - min;
// TODO: refactor to use ratio here
final double scale = r2 / range;
if(!flip) {
return min + (scale * (value - this.getMin().doubleValue()));
} else {
return max - (scale * (value - this.getMin().doubleValue()));
}
}
public Number ratio(Region r2) {
return ratio(r2.getMin().doubleValue(), r2.getMax().doubleValue());
}
/**
*
* @param min
* @param max
* @return length of this series divided by the length of the distance between min and max.
*/
public double ratio(double min, double max) {
return length().doubleValue() / (max - min);
}
public void union(Number value) {
if(value == null) {
return;
}
double val = value.doubleValue();
if(getMin() == null ||
val < getMin().doubleValue()) {
setMin(value);
}
if(getMax() == null || val >
getMax().doubleValue()) {
setMax(value);
}
}
/**
* Compares the input bounds min/max against this instance's current min/max.
* If the input.min is less than this.min then this.min will be set to input.min.
* If the input.max is greater than this.max then this.max will be set to input.max
*
* The result of a union will always be an equal or larger size region.
* @param input
*/
public void union(Region input) {
union(input.getMin());
union(input.getMax());
}
/**
* The result of an intersect will always be an equal or smaller size region.
* @param input
*/
public void intersect(Region input) {
if(getMin().doubleValue() < input.getMin().doubleValue()) {
setMin(input.getMin());
}
if(getMax().doubleValue() > input.getMax().doubleValue()) {
setMax(input.getMax());
}
}
/**
* Tests whether this segment intersects another
* @param line2Min
* @param line2Max
* @return
*/
public boolean intersects(Number line2Min, Number line2Max) {
// is this line completely within line2?
if(line2Min.doubleValue() <= getMin().doubleValue() && line2Max.doubleValue() >= getMax().doubleValue()) {
return true;
// is line1 partially within line2
} else return contains(line2Min) || contains(line2Max);
}
public boolean isMinSet() {
return min != null;
}
public Number getMin() {
return isMinSet() ? min : defaults.min;
}
public void setMin(Number min) {
cachedLength = null;
if(min == null) {
if(defaults == null) {
throw new NullPointerException(
"Region values cannot be null unless defaults have been set.");
} else {
this.min = null;
}
} else if (this.min == null || !this.min.equals(min)) {
this.min = FastNumber.orNull(min);
}
}
public boolean isMaxSet() {
return max != null;
}
public Number getMax() {
return isMaxSet() ? max : defaults.max;
}
public void setMax(Number max) {
cachedLength = null;
if(max == null) {
if(defaults == null) {
throw new NullPointerException(
"Region values can never be null unless defaults have been set.");
} else {
this.max = null;
}
} else if (this.max == null || !this.max.equals(max)) {
this.max = FastNumber.orNull(max);
}
}
/**
*
* @return True if both min and max values are non-null, false otherwise. Does *not* consider defaults.
*/
public boolean isDefined() {
return min != null && max != null;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Region{");
sb.append("min=").append(min);
sb.append(", max=").append(max);
sb.append(", cachedLength=").append(cachedLength);
sb.append(", defaults=");
if (defaults != this) {
sb.append(defaults);
} else {
sb.append("this");
}
sb.append('}');
return sb.toString();
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot;
/**
* Base interface for all Series implementations
*/
public interface Series {
/**
*
* @return The title of this Series.
*/
String getTitle();
}
@@ -0,0 +1,137 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot;
import com.androidplot.ui.Formatter;
import com.androidplot.ui.SeriesBundle;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Manages a list of {@link Series} and their associated {@link Formatter} in the context of a {@link Plot}.
* @since 0.9.7
*/
public abstract class SeriesRegistry
<BundleType extends SeriesBundle<SeriesType, FormatterType>,
SeriesType extends Series, FormatterType extends Formatter> implements Serializable {
private ArrayList<BundleType> registry = new ArrayList<>();
public List<BundleType> getSeriesAndFormatterList() {
return registry;
}
public List<SeriesType> getSeriesList() {
List<SeriesType> result = new ArrayList<>(registry.size());
for(SeriesBundle<SeriesType, FormatterType> sfPair : registry) {
result.add(sfPair.getSeries());
}
return result;
}
public int size() {
return registry.size();
}
public boolean isEmpty() {
return registry.isEmpty();
}
public boolean add(SeriesType series, FormatterType formatter) {
if(series == null || formatter == null) {
throw new IllegalArgumentException("Neither series nor formatter param may be null.");
}
return registry.add(newSeriesBundle(series, formatter));
}
protected abstract BundleType newSeriesBundle(SeriesType series, FormatterType formatter);
/**
*
* @param series
* @return A List of {@link SeriesBundle} instances that reference series.
*/
protected List<SeriesBundle<SeriesType, FormatterType>> get(SeriesType series) {
List<SeriesBundle<SeriesType, FormatterType>> results =
new ArrayList<>();
for(SeriesBundle<SeriesType, FormatterType> thisPair : registry) {
if(thisPair.getSeries() == series) {
results.add(thisPair);
}
}
return results;
}
public synchronized List<BundleType> remove(SeriesType series, Class rendererClass) {
ArrayList<BundleType> removedItems = new ArrayList<>();
for(Iterator<BundleType> it = registry.iterator(); it.hasNext();) {
BundleType b = it.next();
if(b.getSeries() == series && b.getFormatter().getRendererClass() == rendererClass) {
it.remove();
removedItems.add(b);
}
}
return removedItems;
}
/**
* Remove all occurrences of series regardless of the associated Renderer.
* @param series
*/
public synchronized boolean remove(SeriesType series) {
boolean result = false;
for(Iterator<BundleType> it = registry.iterator(); it.hasNext();) {
if(it.next().getSeries() == series) {
it.remove();
result = true;
}
}
return result;
}
/**
* Remove all series from the plot.
*/
public void clear() {
for(Iterator<BundleType> it
= registry.iterator(); it.hasNext();) {
it.next();
it.remove();
}
}
public List<SeriesBundle<SeriesType, FormatterType>> getLegendEnabledItems() {
List<SeriesBundle<SeriesType, FormatterType>> sfList = new ArrayList<>();
for(SeriesBundle<SeriesType, FormatterType> sf : registry) {
if(sf.getFormatter().isLegendIconEnabled()) {
sfList.add(sf);
}
}
return sfList;
}
public boolean contains(SeriesType series, Class<? extends FormatterType> formatterClass) {
for(BundleType b : registry) {
if(b.getFormatter().getClass() == formatterClass && b.getSeries() == series) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot;
import android.graphics.Color;
import android.graphics.Paint;
import com.androidplot.util.PixelUtils;
/**
* A basic implementation of a {@link LineLabelFormatter}.
*/
public class SimpleLineLabelFormatter implements LineLabelFormatter {
private static final int DEFAULT_TEXT_SIZE_SP = 12;
private static final int DEFAULT_STROKE_SIZE_DP = 2;
private Paint paint;
public SimpleLineLabelFormatter() {
this(new Paint());
getPaint().setColor(Color.WHITE);
getPaint().setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_SP));
getPaint().setStrokeWidth(PixelUtils.dpToPix(DEFAULT_STROKE_SIZE_DP));
}
public SimpleLineLabelFormatter(int color) {
this();
getPaint().setColor(color);
}
public SimpleLineLabelFormatter(Paint paint) {
this.paint = paint;
}
public Paint getPaint() {
return paint;
}
public void setPaint(Paint paint) {
this.paint = paint;
}
@Override
public Paint getPaint(Number value) {
return getPaint();
}
}
@@ -0,0 +1,144 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.pie;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import com.androidplot.*;
import com.androidplot.ui.*;
import com.androidplot.util.AttrUtils;
import com.androidplot.util.PixelUtils;
/**
* Basic representation of a Pie Chart that displays a title and pie widget.
*/
public class PieChart extends Plot<Segment, SegmentFormatter, PieRenderer,
SegmentBundle, SegmentRegistry> {
private static final int DEFAULT_PIE_WIDGET_H_DP = 18;
private static final int DEFAULT_PIE_WIDGET_W_DP = 10;
private static final int DEFAULT_PIE_WIDGET_Y_OFFSET_DP = 0;
private static final int DEFAULT_PIE_WIDGET_X_OFFSET_DP = 0;
private static final int DEFAULT_LEGEND_WIDGET_H_DP = 30;
private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 18;
private static final int DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP = 0;
private static final int DEFAULT_LEGEND_WIDGET_X_OFFSET_DP = 40;
private static final int DEFAULT_PADDING_DP = 5;
@SuppressWarnings("FieldCanBeLocal")
private PieWidget pie;
private PieLegendWidget legend;
@Override
protected SegmentRegistry getRegistryInstance() {
return new SegmentRegistry();
}
public PieChart(Context context, String title) {
super(context, title);
}
public PieChart(Context context, String title, RenderMode mode) {
super(context, title, mode);
}
public PieChart(Context context, AttributeSet attributes) {
super(context, attributes);
}
@Override
protected void onPreInit() {
pie = new PieWidget(
getLayoutManager(),
this,
new Size(
PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_H_DP),
SizeMode.FILL,
PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_W_DP),
SizeMode.FILL));
pie.position(
PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_X_OFFSET_DP),
HorizontalPositioning.ABSOLUTE_FROM_CENTER,
PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_Y_OFFSET_DP),
VerticalPositioning.ABSOLUTE_FROM_CENTER,
Anchor.CENTER);
legend = new PieLegendWidget(
getLayoutManager(),
this,
new Size(
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_H_DP),
SizeMode.ABSOLUTE, 0.5f, SizeMode.RELATIVE),
new DynamicTableModel(0, 1),
new Size(
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP),
SizeMode.ABSOLUTE,
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP),
SizeMode.ABSOLUTE));
legend.position(
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_X_OFFSET_DP),
HorizontalPositioning.ABSOLUTE_FROM_RIGHT,
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP),
VerticalPositioning.ABSOLUTE_FROM_BOTTOM,
Anchor.RIGHT_BOTTOM);
legend.setVisible(false);
final float padding = PixelUtils.dpToPix(DEFAULT_PADDING_DP);
pie.setPadding(padding, padding, padding, padding);
}
@Override
protected void processAttrs(TypedArray attrs) {
// borderPaint
AttrUtils.configureLinePaint(attrs, getBorderPaint(),
R.styleable.pie_PieChart_pieBorderColor, R.styleable.pie_PieChart_pieBorderThickness);
}
public void setPie(PieWidget pie) {
this.pie = pie;
}
public PieWidget getPie() {
return pie;
}
public void addSegment(Segment segment, SegmentFormatter formatter) {
addSeries(segment, formatter);
}
public void removeSegment(Segment segment) {
removeSeries(segment);
}
public PieLegendWidget getLegend() {
return legend;
}
public void setLegend(PieLegendWidget legend) {
this.legend = legend;
}
}
@@ -0,0 +1,26 @@
package com.androidplot.pie;
import androidx.annotation.NonNull;
import com.androidplot.ui.widget.LegendItem;
/**
* An item in a {@link PieLegendWidget} corresponding to a {@link Segment} in a {@link PieChart}.
*/
public class PieLegendItem implements LegendItem {
public SegmentFormatter formatter;
public Segment segment;
public PieLegendItem(@NonNull Segment segment, @NonNull SegmentFormatter formatter) {
this.segment = segment;
this.formatter = formatter;
}
@Override
public String getTitle() {
return segment.getTitle();
}
}
@@ -0,0 +1,41 @@
package com.androidplot.pie;
import android.graphics.Canvas;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import com.androidplot.ui.LayoutManager;
import com.androidplot.ui.SeriesBundle;
import com.androidplot.ui.Size;
import com.androidplot.ui.TableModel;
import com.androidplot.ui.widget.LegendWidget;
import java.util.ArrayList;
import java.util.List;
public class PieLegendWidget extends LegendWidget<PieLegendItem> {
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<PieLegendItem> getLegendItems() {
final List<PieLegendItem> legendItems = new ArrayList<>();
for(SeriesBundle<Segment, SegmentFormatter> item : pieChart.getRegistry().getLegendEnabledItems()) {
legendItems.add(new PieLegendItem(item.getSeries(), item.getFormatter()));
}
return legendItems;
}
}
@@ -0,0 +1,402 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.pie;
import android.graphics.*;
import com.androidplot.ui.SeriesBundle;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.ui.RenderStack;
import java.util.List;
/**
* Basic renderer for drawing pie charts.
*/
public class PieRenderer extends SeriesRenderer<PieChart, Segment, SegmentFormatter> {
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 = FULL_PIE_DEGS;
// TODO: express donut in units other than px.
private float donutSize = 0.5f;
private DonutMode donutMode = DonutMode.PERCENT;
public enum DonutMode {
PERCENT,
PIXELS
}
public PieRenderer(PieChart plot) {
super(plot);
}
public float getRadius(RectF rect) {
return rect.width() < rect.height() ? rect.width() / 2 : rect.height() / 2;
}
@Override
public void onRender(Canvas canvas, RectF plotArea, Segment series, SegmentFormatter formatter,
RenderStack stack) {
// This renderer renders all series in one shot, so exclude any remaining series
// from causing subsequent invocations of onRender:
stack.disable(getClass());
float radius = getRadius(plotArea);
PointF origin = new PointF(plotArea.centerX(), plotArea.centerY());
double[] values = getValues();
double scale = calculateScale(values);
float offset = degsToScreenDegs(startDegs);
RectF rec = new RectF(origin.x - radius, origin.y - radius, origin.x + radius,
origin.y + radius);
int i = 0;
for (SeriesBundle<Segment, ? extends SegmentFormatter> sfPair : getSeriesAndFormatterList()) {
float lastOffset = offset;
float sweep = (float) (scale * (values[i]) * extentDegs);
offset += sweep;
drawSegment(canvas, rec, sfPair.getSeries(), sfPair.getFormatter(), radius, lastOffset,
sweep);
i++;
}
}
protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f,
float rad, float startAngle, float sweep) {
canvas.save();
startAngle = startAngle + f.getRadialInset();
sweep = sweep - (f.getRadialInset() * 2);
// midpoint angle between startAngle and endAngle
final float halfSweepEndAngle = startAngle + (sweep / 2);
PointF translated = calculateLineEnd(
bounds.centerX(), bounds.centerY(), f.getOffset(), halfSweepEndAngle);
final float cx = translated.x;
final float cy = translated.y;
float donutSizePx;
switch (donutMode) {
case PERCENT:
donutSizePx = donutSize * rad;
break;
case PIXELS:
donutSizePx = (donutSize > 0) ? donutSize : (rad + donutSize);
break;
default:
throw new UnsupportedOperationException("Unsupported DonutMde: " + donutMode);
}
final float outerRad = rad - f.getOuterInset();
final float innerRad = donutSizePx == 0 ? 0 : donutSizePx + f.getInnerInset();
// do we have a segment of less than 100%
if (Math.abs(sweep - extentDegs) > Float.MIN_VALUE) {
// vertices of the first radial:
PointF r1Outer = calculateLineEnd(cx, cy, outerRad, startAngle);
PointF r1Inner = calculateLineEnd(cx, cy, innerRad, startAngle);
// vertices of the second radial:
PointF r2Outer = calculateLineEnd(cx, cy, outerRad, startAngle + sweep);
PointF r2Inner = calculateLineEnd(cx, cy, innerRad, startAngle + sweep);
Path clip = new Path();
// outer arc:
// leave plenty of room on the outside for stroked borders;
// necessary because the clipping border is ugly
// and cannot be easily anti aliased. Really we only care about masking off the
// radial edges.
clip.arcTo(new RectF(bounds.left - outerRad,
bounds.top - outerRad,
bounds.right + outerRad,
bounds.bottom + outerRad),
startAngle, sweep);
clip.lineTo(cx, cy);
clip.close();
canvas.clipPath(clip);
Path p = new Path();
// outer arc:
p.arcTo(new RectF(
cx - outerRad,
cy - outerRad,
cx + outerRad,
cy + outerRad)
, startAngle, sweep);
p.lineTo(r2Inner.x, r2Inner.y);
// inner arc:
// sweep back to original angle:
p.arcTo(new RectF(
cx - innerRad,
cy - innerRad,
cx + innerRad,
cy + innerRad),
startAngle + sweep, -sweep);
p.close();
// fill segment:
canvas.drawPath(p, f.getFillPaint());
// draw radial lines
canvas.drawLine(r1Inner.x, r1Inner.y, r1Outer.x, r1Outer.y, f.getRadialEdgePaint());
canvas.drawLine(r2Inner.x, r2Inner.y, r2Outer.x, r2Outer.y, f.getRadialEdgePaint());
} else {
canvas.save();
Path chart = new Path();
chart.addCircle(cx, cy, outerRad, Path.Direction.CW);
Path inside = new Path();
inside.addCircle(cx, cy, innerRad, Path.Direction.CW);
canvas.clipPath(inside, Region.Op.DIFFERENCE);
canvas.drawPath(chart, f.getFillPaint());
canvas.restore();
}
// draw inner line:
canvas.drawCircle(cx, cy, innerRad, f.getInnerEdgePaint());
// draw outer line:
canvas.drawCircle(cx, cy, outerRad, f.getOuterEdgePaint());
canvas.restore();
PointF labelOrigin = calculateLineEnd(cx, cy,
(outerRad - ((outerRad - innerRad) / 2)), halfSweepEndAngle);
// TODO: move segment labelling outside the segment drawing loop
// TODO: so that the labels will not be clipped by the edge of the next
// TODO: segment being drawn.
if (f.getLabelPaint() != null) {
drawSegmentLabel(canvas, labelOrigin, seg, f);
}
}
protected void drawSegmentLabel(Canvas canvas, PointF origin,
Segment seg, SegmentFormatter f) {
canvas.drawText(seg.getTitle(), origin.x, origin.y, f.getLabelPaint());
}
@Override
protected void doDrawLegendIcon(Canvas canvas, RectF rect, SegmentFormatter formatter) {
throw new UnsupportedOperationException("Not yet implemented.");
}
/**
* Determines how many counts there are per cent of whatever the
* pie chart is displaying as a fraction, 1 being 100%.
*/
protected double calculateScale(double[] values) {
double total = 0;
for (int i = 0; i < values.length; i++) {
total += values[i];
}
return (1d / total);
}
/**
* Retreive the raw values being rendered from each {@link Segment}.
* @return
*/
protected double[] getValues() {
List<SeriesBundle<Segment, ? extends SegmentFormatter>> seriesList = getSeriesAndFormatterList();
double[] result = new double[seriesList.size()];
int i = 0;
for (SeriesBundle<Segment, ? extends SegmentFormatter> sfPair : seriesList) {
result[i] = sfPair.getSeries().getValue().doubleValue();
i++;
}
return result;
}
protected PointF calculateLineEnd(float x, float y, float rad, float deg) {
return calculateLineEnd(new PointF(x, y), rad, deg);
}
protected PointF calculateLineEnd(PointF origin, float rad, float deg) {
double radians = deg * Math.PI / HALF_PIE_DEGS;
double x = rad * Math.cos(radians);
double y = rad * Math.sin(radians);
// convert to screen space:
return new PointF(origin.x + (float) x, origin.y + (float) y);
}
/**
* Set the size of the pie's empty inner space. May be specified as either pixels or as a percentage.
* If using {@link DonutMode#PIXELS}, the best practice is to specify values in dp using
* {@link com.androidplot.util.PixelUtils#dpToPix(float)}.
*
* If using {@link DonutMode#PERCENT} the value must be within the range 0 - 1. The value being
* set corresponds to the size of the donut radius relative to the pie's total radius.
* @param size
* @param mode
*/
public void setDonutSize(float size, DonutMode mode) {
switch (mode) {
case PERCENT:
if (size < 0 || size > 1) {
throw new IllegalArgumentException(
"Size parameter must be between 0 and 1 when operating in PERCENT mode.");
}
break;
case PIXELS:
break;
default:
throw new UnsupportedOperationException("Not yet implemented.");
}
donutMode = mode;
donutSize = size;
}
/**
* Retrieve the segment containing the specified point. This current implementation
* only matches against angle; clicks outside of the pie/donut inner/outer boundaries
* will still trigger a match on the segment whose beginning and ending angle contains
* the angle of the line drawn between the pie chart's center point and the clicked point.
* @param point The clicked point
* @return Segment containing the clicked point.
*/
public Segment getContainingSegment(PointF point) {
RectF plotArea = getPlot().getPie().getWidgetDimensions().marginatedRect;
// figure out the angle in degrees of the line between the clicked point
// and the origin of the plotArea:
PointF origin = new PointF(plotArea.centerX(), plotArea.centerY());
float dx = point.x - origin.x;
float dy = point.y - origin.y;
double theta = Math.atan2(dy, dx);
double angle = (theta * (HALF_PIE_DEGS / Math.PI));
if (angle < 0) {
// bring into 0-360 range
angle += FULL_PIE_DEGS;
}
// find the segment whose starting and ending angle (degs) contains
// the angle calculated above
List<SeriesBundle<Segment, ? extends SegmentFormatter>> seriesList = getSeriesAndFormatterList();
int i = 0;
double[] values = getValues();
double scale = calculateScale(values);
float offset = degsToScreenDegs(startDegs);
for (SeriesBundle<Segment, ? extends SegmentFormatter> sfPair : seriesList) {
float lastOffset = offset;
float sweep = (float) (scale * (values[i]) * extentDegs);
offset += sweep;
offset = offset % FULL_PIE_DEGS;
final double dist = signedDistance(offset, angle);
double endDist = signedDistance(offset, lastOffset);
if(endDist < 0) {
// segment accounts for more than 50% of the pie and wrapped around
// need to correct:
endDist = FULL_PIE_DEGS + endDist;
}
if(dist > 0 && dist <= endDist) {
return sfPair.getSeries();
}
i++;
}
return null;
}
/**
* convert conventional degrees (90 degrees is north) to screen degrees (90 degrees is south)
* Values >= 369 will be converted back into the range of 0 - 359.999...
* @param degs
* @return
*/
protected static float degsToScreenDegs(float degs) {
degs = degs % FULL_PIE_DEGS;
if (degs > 0) {
return FULL_PIE_DEGS - degs;
} else {
return degs;
}
}
/**
* Compute the signed shortest angular distance between two angles
* @param angle1
* @param angle2
* @return
*/
protected static double signedDistance(double angle1, double angle2) {
double d = Math.abs(angle1 - angle2) % FULL_PIE_DEGS;
double r = d > HALF_PIE_DEGS ? FULL_PIE_DEGS - d : d;
//calculate sign
int sign = (angle1 - angle2 >= 0 && angle1 - angle2 <= HALF_PIE_DEGS)
|| (angle1 - angle2 <= -HALF_PIE_DEGS && angle1 - angle2 >= -FULL_PIE_DEGS) ? 1 : -1;
r *= sign;
return r;
}
/**
* Throws an IllegalArgumentException if the input value is outside of the range of 0.0 to 360.0
* @param degs
*/
protected static void validateInputDegs(float degs) {
if(degs < 0 || degs > FULL_PIE_DEGS) {
throw new IllegalArgumentException("Degrees values must be between 0.0 and 360.");
}
}
/**
* Set the starting point in degrees from which series will be drawn in order.
* The input value must be within the range 0 - 360.
* @param degs
*/
public void setStartDegs(float degs) {
validateInputDegs(degs);
startDegs = degs;
}
public float getStartDegs() {
return startDegs;
}
/**
* Set the size in degrees of the pie chart, extending from the startDegs. The input value
* must be within the range 0 - 360. An input value would represent 100% of the pie as a half
* circle while an input value of 360 would be a full circle.
* @param degs
*/
public void setExtentDegs(float degs) {
validateInputDegs(degs);
extentDegs = degs;
}
public float getExtentDegs() {
return extentDegs;
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.pie;
import android.graphics.*;
import com.androidplot.ui.LayoutManager;
import com.androidplot.ui.Size;
import com.androidplot.ui.widget.Widget;
import com.androidplot.ui.RenderStack;
/**
* Visualizes data as a pie chart.
*/
public class PieWidget extends Widget {
private PieChart pieChart;
private RenderStack<? extends Segment, ? extends SegmentFormatter> renderStack;
public PieWidget(LayoutManager layoutManager, PieChart pieChart, Size metrics) {
super(layoutManager, metrics);
this.pieChart = pieChart;
renderStack = new RenderStack(pieChart);
}
@Override
protected void doOnDraw(Canvas canvas, RectF widgetRect) {
renderStack.sync();
for(RenderStack.StackElement thisElement : renderStack.getElements()) {
if(thisElement.isEnabled()) {
pieChart.getRenderer(thisElement.get().getFormatter().getRendererClass()).
render(canvas, widgetRect, thisElement.get(), renderStack);
}
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.pie;
import com.androidplot.Series;
/**
* An implementation of Series representing a segment in a pie chart.
*/
public class Segment implements Series {
private String title;
private Number value;
public Segment(String title, Number value) {
this.title = title;
this.setValue(value);
}
@Override
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Number getValue() {
return value;
}
public void setValue(Number value) {
this.value = value;
}
}
@@ -0,0 +1,14 @@
package com.androidplot.pie;
import com.androidplot.ui.*;
/**
* Manages the association between a given {@link Segment} and the {@link SegmentFormatter} that
* will be used to render it.
*/
public class SegmentBundle extends SeriesBundle<Segment, SegmentFormatter> {
public SegmentBundle(Segment series, SegmentFormatter formatter) {
super(series, formatter);
}
}
@@ -0,0 +1,223 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.pie;
import android.content.*;
import android.graphics.Color;
import android.graphics.Paint;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.ui.Formatter;
public class SegmentFormatter extends Formatter<PieChart> {
private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT;
private static final int DEFAULT_EDGE_COLOR = Color.BLACK;
private static final int DEFAULT_LABEL_COLOR = Color.WHITE;
private static final float DEFAULT_EDGE_THICKNESS = 3;
private static final float DEFAULT_LABEL_MARKER_THICKNESS = 3;
private static final float DEFAULT_LABEL_FONT_SIZE = 18;
private Paint innerEdgePaint;
private Paint outerEdgePaint;
private Paint radialEdgePaint;
private Paint fillPaint;
private Paint labelPaint;
private Paint labelMarkerPaint;
private float offset;
private float radialInset;
private float innerInset;
private float outerInset;
{
setFillPaint(new Paint());
// outer edge:
setOuterEdgePaint(new Paint());
getOuterEdgePaint().setStyle(Paint.Style.STROKE);
getOuterEdgePaint().setStrokeWidth(DEFAULT_EDGE_THICKNESS);
getOuterEdgePaint().setAntiAlias(true);
// inner edge:
setInnerEdgePaint(new Paint());
getInnerEdgePaint().setStyle(Paint.Style.STROKE);
getInnerEdgePaint().setStrokeWidth(DEFAULT_EDGE_THICKNESS);
getInnerEdgePaint().setAntiAlias(true);
// radial edge:
setRadialEdgePaint(new Paint());
getRadialEdgePaint().setStyle(Paint.Style.STROKE);
getRadialEdgePaint().setStrokeWidth(DEFAULT_EDGE_THICKNESS);
getRadialEdgePaint().setAntiAlias(true);
// label paint:
setLabelPaint(new Paint());
getLabelPaint().setColor(DEFAULT_LABEL_COLOR);
getLabelPaint().setTextSize(DEFAULT_LABEL_FONT_SIZE);
getLabelPaint().setAntiAlias(true);
getLabelPaint().setTextAlign(Paint.Align.CENTER);
// label marker paint:
setLabelMarkerPaint(new Paint());
getLabelMarkerPaint().setColor(DEFAULT_LABEL_COLOR);
getLabelMarkerPaint().setStrokeWidth(DEFAULT_LABEL_MARKER_THICKNESS);
}
public SegmentFormatter(Integer fillColor) {
if(fillColor != null) {
getFillPaint().setColor(fillColor);
} else {
getFillPaint().setColor(DEFAULT_FILL_COLOR);
}
}
public SegmentFormatter(Context context, int xmlCfgId) {
configure(context, xmlCfgId);
}
public SegmentFormatter(Integer fillColor, Integer borderColor) {
this(fillColor);
getInnerEdgePaint().setColor(borderColor);
getOuterEdgePaint().setColor(borderColor);
getRadialEdgePaint().setColor(borderColor);
}
public SegmentFormatter(Integer fillColor, Integer outerEdgeColor,
Integer innerEdgeColor, Integer radialEdgeColor) {
this(fillColor);
if(getOuterEdgePaint() != null) {
getOuterEdgePaint().setColor(outerEdgeColor);
} else {
outerEdgePaint = new Paint();
getOuterEdgePaint().setColor(DEFAULT_EDGE_COLOR);
}
if (getInnerEdgePaint() != null) {
getInnerEdgePaint().setColor(innerEdgeColor);
} else {
outerEdgePaint = new Paint();
getInnerEdgePaint().setColor(DEFAULT_EDGE_COLOR);
}
if (getRadialEdgePaint() != null) {
getRadialEdgePaint().setColor(radialEdgeColor);
} else {
radialEdgePaint = new Paint();
getRadialEdgePaint().setColor(DEFAULT_EDGE_COLOR);
}
}
@Override
public Class<? extends SeriesRenderer> getRendererClass() {
return PieRenderer.class;
}
@Override
public SeriesRenderer doGetRendererInstance(PieChart plot) {
return new PieRenderer(plot);
}
public Paint getInnerEdgePaint() {
return innerEdgePaint;
}
public void setInnerEdgePaint(Paint innerEdgePaint) {
this.innerEdgePaint = innerEdgePaint;
}
public Paint getOuterEdgePaint() {
return outerEdgePaint;
}
public void setOuterEdgePaint(Paint outerEdgePaint) {
this.outerEdgePaint = outerEdgePaint;
}
public Paint getRadialEdgePaint() {
return radialEdgePaint;
}
public void setRadialEdgePaint(Paint radialEdgePaint) {
this.radialEdgePaint = radialEdgePaint;
}
public Paint getFillPaint() {
return fillPaint;
}
public void setFillPaint(Paint fillPaint) {
this.fillPaint = fillPaint;
}
public Paint getLabelPaint() {
return labelPaint;
}
public void setLabelPaint(Paint labelPaint) {
this.labelPaint = labelPaint;
}
public Paint getLabelMarkerPaint() {
return labelMarkerPaint;
}
public void setLabelMarkerPaint(Paint labelMarkerPaint) {
this.labelMarkerPaint = labelMarkerPaint;
}
public float getOffset() {
return offset;
}
/**
* Set an offset relative to the center of the pie chart at which this segment should be drawn;
* generally used to highlight specific segments.
* @param offset
*/
public void setOffset(float offset) {
this.offset = offset;
}
public float getRadialInset() {
return radialInset;
}
/**
* Set an inset in degrees for the radial edges of this segment.
* generally used to highlight specific segments.
* @param radialInset
*/
public void setRadialInset(float radialInset) {
this.radialInset = radialInset;
}
public float getInnerInset() {
return innerInset;
}
public void setInnerInset(float innerInset) {
this.innerInset = innerInset;
}
public float getOuterInset() {
return outerInset;
}
public void setOuterInset(float outerInset) {
this.outerInset = outerInset;
}
}
@@ -0,0 +1,14 @@
package com.androidplot.pie;
import com.androidplot.*;
/**
* SeriesRegistry implementation to be used in a {@link PieChart}.
*/
public class SegmentRegistry extends SeriesRegistry<SegmentBundle, Segment, SegmentFormatter> {
@Override
protected SegmentBundle newSeriesBundle(Segment series, SegmentFormatter formatter) {
return new SegmentBundle(series, formatter);
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
/**
* Enumeration of possible anchor positions that a {@link com.androidplot.ui.widget.Widget} can use. There are a total
* 8 possible anchor positions representing each corner of the Widget and the point exactly between each corner.
*/
public enum Anchor {
TOP_MIDDLE,
LEFT_TOP, // default
LEFT_MIDDLE,
LEFT_BOTTOM,
RIGHT_TOP,
RIGHT_MIDDLE,
RIGHT_BOTTOM,
BOTTOM_MIDDLE,
CENTER
}
@@ -0,0 +1,162 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import android.graphics.RectF;
/**
* Convenience implementation of {@link BoxModelable}.
*/
public class BoxModel implements BoxModelable{
private float marginLeft;
private float marginTop;
private float marginRight;
private float marginBottom;
private float paddingLeft;
private float paddingTop;
private float paddingRight;
private float paddingBottom;
/**
* Default with 0 for all padding / margin values
*/
public BoxModel() {
// nothing to do
}
@SuppressWarnings("SameParameterValue")
public BoxModel(float marginLeft, float marginTop, float marginRight, float marginBottom,
float paddingLeft, float paddingTop, float paddingRight, float paddingBottom) {
this.marginLeft = marginLeft;
this.marginTop = marginTop;
this.marginRight = marginRight;
this.marginBottom = marginBottom;
this.paddingLeft = paddingLeft;
this.paddingTop = paddingTop;
this.paddingRight = paddingRight;
this.paddingBottom = paddingBottom;
}
/**
* Returns a RectF instance describing the inner edge of the margin layer.
* @param boundsRect
* @return
*/
public RectF getMarginatedRect(RectF boundsRect) {
return new RectF( boundsRect.left + getMarginLeft(),
boundsRect.top + getMarginTop(),
boundsRect.right - getMarginRight(),
boundsRect.bottom - getMarginBottom());
}
/**
* Returns a RectF instance describing the inner edge of the padding layer.
* @param marginRect
* @return
*/
public RectF getPaddedRect(RectF marginRect) {
return new RectF(marginRect.left + getPaddingLeft(),
marginRect.top + getPaddingTop(),
marginRect.right - getPaddingRight(),
marginRect.bottom - getPaddingBottom());
}
@Override
public void setMargins(float left, float top, float right, float bottom) {
setMarginLeft(left);
setMarginTop(top);
setMarginRight(right);
setMarginBottom(bottom);
}
@Override
public void setPadding(float left, float top, float right, float bottom) {
setPaddingLeft(left);
setPaddingTop(top);
setPaddingRight(right);
setPaddingBottom(bottom);
}
public float getMarginLeft() {
return marginLeft;
}
public void setMarginLeft(float marginLeft) {
this.marginLeft = marginLeft;
}
public float getMarginTop() {
return marginTop;
}
public void setMarginTop(float marginTop) {
this.marginTop = marginTop;
}
public float getMarginRight() {
return marginRight;
}
public void setMarginRight(float marginRight) {
this.marginRight = marginRight;
}
public float getMarginBottom() {
return marginBottom;
}
public void setMarginBottom(float marginBottom) {
this.marginBottom = marginBottom;
}
public float getPaddingLeft() {
return paddingLeft;
}
public void setPaddingLeft(float paddingLeft) {
this.paddingLeft = paddingLeft;
}
public float getPaddingTop() {
return paddingTop;
}
public void setPaddingTop(float paddingTop) {
this.paddingTop = paddingTop;
}
public float getPaddingRight() {
return paddingRight;
}
public void setPaddingRight(float paddingRight) {
this.paddingRight = paddingRight;
}
public float getPaddingBottom() {
return paddingBottom;
}
public void setPaddingBottom(float paddingBottom) {
this.paddingBottom = paddingBottom;
}
}
@@ -0,0 +1,79 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import android.graphics.RectF;
/**
* Defines the properties of a <a href="http://www.w3.org/TR/CSS21/box.html">BoxModel</a> as used
* by Androidplot. Essentially, the BoxModel composes three nested (but not necessarily concentric) rectangles:
* * The bounding box, which is the outer-most box.
* * The marginated box, which is calculated by applying the margin insets to the bounding box.
* * The padded box, which is calculated by applying the padding insets to the marginated box.
*/
public interface BoxModelable {
/**
* Returns a RectF instance describing the inner edge of the margin layer.
* @param boundsRect
* @return
*/
RectF getMarginatedRect(RectF boundsRect);
/**
* Returns a RectF instance describing the inner edge of the padding layer.
* @param marginRect
* @return
*/
RectF getPaddedRect(RectF marginRect);
void setMargins(float left, float top, float right, float bottom);
void setPadding(float left, float top, float right, float bottom);
float getMarginLeft();
void setMarginLeft(float marginLeft);
float getMarginTop();
void setMarginTop(float marginTop);
float getMarginRight();
void setMarginRight(float marginRight);
float getMarginBottom();
void setMarginBottom(float marginBottm);
float getPaddingLeft();
void setPaddingLeft(float paddingLeft);
float getPaddingTop();
void setPaddingTop(float paddingTop);
float getPaddingRight();
void setPaddingRight(float paddingRight);
float getPaddingBottom();
void setPaddingBottom(float paddingBottom);
}
@@ -0,0 +1,228 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import android.graphics.RectF;
import java.util.Iterator;
/**
* Encapsulates the visual aspects of a table; number of rows and columns
* and the height and width in pixels of each element within the table.
* There is no support (yet) for variable size cells within a table; all
* cells within a table share the same dimensions.
*
* The DynamicTableModel provides an Iterator implementation which returns a RectF
* of each subsequent cell, based on the order of the plot. Tables with
* an order of COLUMN_MAJOR are traversed left to right column by column until
* the end of the row is reached, then proceeding to the next row.
* Tables with an order of ROW_MAJOR are traversed top to bottom row by row
* until the end of the row is reached, then proceeding to the next column.
*/
public class DynamicTableModel extends TableModel {
private int numRows;
private int numColumns;
/**
* Convenience method. Sets order to ROW_MAJOR.
* @param numColumns
* @param numRows
*/
public DynamicTableModel(int numColumns, int numRows) {
this(numColumns, numRows, TableOrder.ROW_MAJOR);
}
public DynamicTableModel(int numColumns, int numRows, TableOrder order) {
super(order);
this.numColumns = numColumns;
this.numRows = numRows;
}
@Override
public TableModelIterator getIterator(RectF tableRect, int totalElements) {
return new TableModelIterator(this, tableRect, totalElements);
}
/**
* Calculates the dimensions of a single element of this table with
* tableRect representing the overall dimensions of the table.
* @param tableRect Dimensions/position of the table
* @return a RectF representing the first (top-left) element in
* the tableRect passed in.
*/
public RectF getCellRect(RectF tableRect, int numElements) {
RectF cellRect = new RectF();
cellRect.left = tableRect.left;
cellRect.top = tableRect.top;
cellRect.bottom = tableRect.top + calculateCellSize(tableRect, TableModel.Axis.ROW, numElements);
cellRect.right = tableRect.left + calculateCellSize(tableRect, TableModel.Axis.COLUMN, numElements);
return cellRect;
}
/**
* Figure out the size of a single cell across the specified axis.
* @param tableRect
* @param axis
* @param numElementsInTable
* @return
*/
private float calculateCellSize(RectF tableRect,
Axis axis,
int numElementsInTable) {
int axisElements = 0;
float axisSizePix = 0;
switch (axis) {
case ROW:
axisElements = numRows;
axisSizePix = tableRect.height();
break;
case COLUMN:
axisElements = numColumns;
axisSizePix = tableRect.width();
break;
}
if(axisElements != 0) {
return axisSizePix / axisElements;
} else {
return axisSizePix / numElementsInTable;
}
}
public int getNumRows() {
return numRows;
}
public void setNumRows(int numRows) {
this.numRows = numRows;
}
public int getNumColumns() {
return numColumns;
}
public void setNumColumns(int numColumns) {
this.numColumns = numColumns;
}
private class TableModelIterator implements Iterator<RectF> {
private boolean isOk = true;
int lastColumn = 0; // most recent column iterated
int lastRow = 0; // most recent row iterated
int lastElement = 0; // last element index iterated
private DynamicTableModel dynamicTableModel;
private RectF tableRect;
private RectF lastElementRect;
private int totalElements;
private TableOrder order;
private int calculatedNumElements;
private int calculatedRows; // number of rows to be iterated
private int calculatedColumns; // number of columns to be iterated
public TableModelIterator(DynamicTableModel dynamicTableModel, RectF tableRect, int totalElements) {
this.dynamicTableModel = dynamicTableModel;
this.tableRect = tableRect;
this.totalElements = totalElements;
order = dynamicTableModel.getOrder();
// unlimited columns:
if(dynamicTableModel.getNumColumns() == 0 && dynamicTableModel.getNumRows() >= 1) {
calculatedRows = dynamicTableModel.getNumRows();
// round up:
calculatedColumns = Float.valueOf((totalElements / (float) calculatedRows) + 0.5f).intValue();
} else if(dynamicTableModel.getNumRows() == 0 && dynamicTableModel.getNumColumns() >= 1) {
calculatedColumns = dynamicTableModel.getNumColumns();
calculatedRows = Float.valueOf((totalElements / (float) calculatedColumns) + 0.5f).intValue();
// unlimited rows and columns (impossible) so default a single row with n columns:
}else if(dynamicTableModel.getNumColumns() == 0 && dynamicTableModel.getNumRows() == 0) {
calculatedRows = 1;
calculatedColumns = totalElements;
} else {
calculatedRows = dynamicTableModel.getNumRows();
calculatedColumns = dynamicTableModel.getNumColumns();
}
calculatedNumElements = calculatedRows * calculatedColumns;
lastElementRect = dynamicTableModel.getCellRect(tableRect, totalElements);
}
@Override
public boolean hasNext() {
return isOk && lastElement < calculatedNumElements;
}
@Override
public RectF next() {
if(!hasNext()) {
isOk = false;
throw new IndexOutOfBoundsException();
}
if (lastElement == 0) {
lastElement++;
return lastElementRect;
}
RectF nextElementRect = new RectF(lastElementRect);
switch (order) {
case ROW_MAJOR:
if (dynamicTableModel.getNumColumns() > 0 && lastColumn >= (dynamicTableModel.getNumColumns() - 1)) {
// move to the begining of the next row down:// move to the begining of the next row down:
nextElementRect.offsetTo(tableRect.left, lastElementRect.bottom);
lastColumn = 0;
lastRow++;
} else {
// move to the next column over:
nextElementRect.offsetTo(lastElementRect.right, lastElementRect.top);
lastColumn++;
}
break;
case COLUMN_MAJOR:
if (dynamicTableModel.getNumRows() > 0 && lastRow >= (dynamicTableModel.getNumRows() - 1)) {
// move to the top of the next column over:
nextElementRect.offsetTo(lastElementRect.right, tableRect.top);
lastRow = 0;
lastColumn++;
} else {
// move to the next row down:
nextElementRect.offsetTo(lastElementRect.left, lastElementRect.bottom);
lastRow++;
}
break;
// unknown/unsupported enum val:
default:
isOk = false;
throw new IllegalArgumentException();
}
lastElement++;
lastElementRect = nextElementRect;
return nextElementRect;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,149 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import android.graphics.RectF;
import java.util.Iterator;
public class FixedTableModel extends TableModel {
private float cellWidth;
private float cellHeight;
public FixedTableModel(float cellWidth, float cellHeight, TableOrder order) {
super(order);
setCellWidth(cellWidth);
setCellHeight(cellHeight);
}
@Override
public Iterator<RectF> getIterator(RectF tableRect, int totalElements) {
return new FixedTableModelIterator(this, tableRect, totalElements);
}
public float getCellWidth() {
return cellWidth;
}
public void setCellWidth(float cellWidth) {
this.cellWidth = cellWidth;
}
public float getCellHeight() {
return cellHeight;
}
public void setCellHeight(float cellHeight) {
this.cellHeight = cellHeight;
}
private class FixedTableModelIterator implements Iterator<RectF> {
private FixedTableModel model;
private RectF tableRect;
private RectF lastRect;
private int numElements;
private int lastElement;
protected FixedTableModelIterator(FixedTableModel model, RectF tableRect, int numElements) {
this.model = model;
this.tableRect = tableRect;
this.numElements = numElements;
lastRect = new RectF(
tableRect.left,
tableRect.top,
tableRect.left + model.getCellWidth(),
tableRect.top + model.getCellHeight());
}
@Override
public boolean hasNext() {
// was this the last element or is there no room in either axis for another cell?
return !(lastElement >= numElements || (isColumnFinished() && isRowFinished()));
}
private boolean isColumnFinished() {
return lastRect.bottom + model.getCellHeight() > tableRect.height();
}
private boolean isRowFinished() {
return lastRect.right + model.getCellWidth() > tableRect.width();
}
@Override
public RectF next() {
try {
if (lastElement == 0) {
return lastRect;
}
if (lastElement >= numElements) {
throw new IndexOutOfBoundsException();
}
switch (model.getOrder()) {
case ROW_MAJOR:
if (isColumnFinished()) {
moveOverAndUp();
} else {
moveDown();
}
break;
case COLUMN_MAJOR:
if (isRowFinished()) {
moveDownAndBack();
} else {
moveOver();
}
break;
default:
throw new UnsupportedOperationException();
}
return lastRect;
} finally {
lastElement++;
}
}
private void moveDownAndBack() {
//RectF rect = new RectF(lastRect);
lastRect.offsetTo(tableRect.left, lastRect.bottom);
//return rect;
}
private void moveOverAndUp() {
//RectF rect = new RectF(lastRect);
lastRect.offsetTo(lastRect.right, tableRect.top);
//return rect;
}
private void moveOver() {
//RectF rect = new RectF(lastRect);
lastRect.offsetTo(lastRect.right, lastRect.top);
//return rect;
}
private void moveDown() {
//RectF rect = new RectF(lastRect);
lastRect.offsetTo(lastRect.left, lastRect.bottom);
//return rect;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import android.content.Context;
import com.androidplot.Plot;
import com.halfhp.fig.*;
/**
* Base class of all Formatters. Encapsulates visual elements of a series; line style, color etc.
* Implementors of this class should include both a default constructor and a one argument
* constructor in the following form:
*
* <pre>
* {@code
* // provided as a convenience to users; allows instantiation and
* // xml configuration in a single line.
* public MyFormatter(Context ctx, int xmlCfgId) {
* // prevent configuration of classes derived from this one:
* if (getClass().equals(MyFormatter.class)) {
* Configurator.configure(ctx, this, xmlCfgId);
* }
* }
* </pre>
*/
public abstract class Formatter<PlotType extends Plot> {
private boolean isLegendIconEnabled = true;
public Formatter() {}
public Formatter(Context ctx, int xmlCfgId) {
configure(ctx, xmlCfgId);
}
public void configure(Context ctx, int xmlCfgId) {
try {
Fig.configure(ctx, this, xmlCfgId);
} catch (FigException e) {
throw new RuntimeException(e);
}
}
/**
*
* @param plot
* @param <T>
* @return @return An instance of SeriesRenderer constructed with the specified plot.
*/
public <T extends SeriesRenderer> T getRendererInstance(PlotType plot) {
return (T) doGetRendererInstance(plot);
}
/**
*
* @return The Class of SeriesRenderer that should be used when rendering series associated
* with instances of this formatter.
*/
public abstract Class<? extends SeriesRenderer> getRendererClass();
/**
*
* @return An instance of SeriesRenderer constructed with the specified plot.
*/
protected abstract SeriesRenderer doGetRendererInstance(PlotType plot);
public boolean isLegendIconEnabled() {
return isLegendIconEnabled;
}
/**
* Sets whether or not a legend icon should be drawn for the series associated with this formatter.
* @param legendIconEnabled
*/
public void setLegendIconEnabled(boolean legendIconEnabled) {
this.isLegendIconEnabled = legendIconEnabled;
}
}
@@ -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.ui;
public class HorizontalPosition extends PositionMetric<HorizontalPositioning> {
public HorizontalPosition(float value, HorizontalPositioning layoutStyle) {
super(value, layoutStyle);
validatePair(value, layoutStyle);
}
/**
* Throws IllegalArgumentException if there is a problem.
* @param value
*/
protected void validatePair(float value, HorizontalPositioning layoutStyle) {
switch(layoutStyle) {
case ABSOLUTE_FROM_LEFT:
case ABSOLUTE_FROM_RIGHT:
case ABSOLUTE_FROM_CENTER:
validateValue(value, PositionMetric.LayoutMode.ABSOLUTE);
break;
case RELATIVE_TO_LEFT:
case RELATIVE_TO_RIGHT:
case RELATIVE_TO_CENTER:
validateValue(value, PositionMetric.LayoutMode.RELATIVE);
}
}
@Override
public float getPixelValue(float size) {
switch(getLayoutType()) {
case ABSOLUTE_FROM_LEFT:
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_BEGINING);
case ABSOLUTE_FROM_RIGHT:
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_END);
case ABSOLUTE_FROM_CENTER:
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_CENTER);
case RELATIVE_TO_LEFT:
return this.getRelativePosition(size, PositionMetric.Origin.FROM_BEGINING);
case RELATIVE_TO_RIGHT:
return this.getRelativePosition(size, PositionMetric.Origin.FROM_END);
case RELATIVE_TO_CENTER:
return this.getRelativePosition(size, PositionMetric.Origin.FROM_CENTER);
default:
throw new IllegalArgumentException("Unsupported LayoutType: " + this.getLayoutType());
}
}
@Override
public void setLayoutType(HorizontalPositioning layoutType) {
super.setLayoutType(layoutType);
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
public enum HorizontalPositioning {
ABSOLUTE_FROM_LEFT,
ABSOLUTE_FROM_RIGHT,
ABSOLUTE_FROM_CENTER,
RELATIVE_TO_LEFT,
RELATIVE_TO_RIGHT,
RELATIVE_TO_CENTER
}
@@ -0,0 +1,69 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
/**
* A set of insets for a rect space.
*/
public class Insets {
private float top;
private float bottom;
private float left;
private float right;
public Insets() {}
public Insets(float top, float bottom, float left, float right) {
this.top = top;
this.bottom = bottom;
this.left = left;
this.right = right;
}
public float getTop() {
return top;
}
public void setTop(float top) {
this.top = top;
}
public float getBottom() {
return bottom;
}
public void setBottom(float bottom) {
this.bottom = bottom;
}
public float getLeft() {
return left;
}
public void setLeft(float left) {
this.left = left;
}
public float getRight() {
return right;
}
public void setRight(float right) {
this.right = right;
}
}
@@ -0,0 +1,257 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
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.ui.widget.Widget;
import com.androidplot.util.DisplayDimensions;
import com.androidplot.util.LinkedLayerList;
public class LayoutManager extends LinkedLayerList<Widget>
implements View.OnTouchListener, Resizable {
private boolean drawAnchorsEnabled = false;
private Paint anchorPaint;
private boolean drawOutlinesEnabled = false;
private Paint outlinePaint;
private boolean drawOutlineShadowsEnabled = false;
private Paint outlineShadowPaint;
private boolean drawMarginsEnabled = false;
private Paint marginPaint;
private boolean drawPaddingEnabled = false;
private Paint paddingPaint;
private DisplayDimensions displayDims = new DisplayDimensions();
{
anchorPaint = new Paint();
anchorPaint.setStyle(Paint.Style.FILL);
anchorPaint.setColor(Color.GREEN);
outlinePaint = new Paint();
outlinePaint.setColor(Color.GREEN);
outlinePaint.setStyle(Paint.Style.STROKE);
outlinePaint.setAntiAlias(true);
outlinePaint.setStrokeWidth(2);
marginPaint = new Paint();
marginPaint.setColor(Color.YELLOW);
marginPaint.setStyle(Paint.Style.FILL);
marginPaint.setAlpha(200);
paddingPaint= new Paint();
paddingPaint.setColor(Color.BLUE);
paddingPaint.setStyle(Paint.Style.FILL);
paddingPaint.setAlpha(200);
}
/**
* Invoked immediately following XML configuration.
*/
public synchronized void onPostInit() {
for(Widget w : elements()) {
w.onPostInit();
}
}
public LayoutManager() {
}
public void setMarkupEnabled(boolean enabled) {
setDrawOutlinesEnabled(enabled);
setDrawAnchorsEnabled(enabled);
setDrawMarginsEnabled(enabled);
setDrawPaddingEnabled(enabled);
setDrawOutlineShadowsEnabled(enabled);
}
public void draw(Canvas canvas) {
if(isDrawMarginsEnabled()) {
drawSpacing(canvas, displayDims.canvasRect, displayDims.marginatedRect, marginPaint);
}
if (isDrawPaddingEnabled()) {
drawSpacing(canvas, displayDims.marginatedRect, displayDims.paddedRect, paddingPaint);
}
for (Widget widget : elements()) {
try {
canvas.save();
PositionMetrics metrics = widget.getPositionMetrics();
float elementWidth = widget.getWidthPix(displayDims.paddedRect.width());
float elementHeight = widget.getHeightPix(displayDims.paddedRect.height());
PointF coords = Widget.calculateCoordinates(elementHeight,
elementWidth, displayDims.paddedRect, metrics);
DisplayDimensions dims = widget.getWidgetDimensions();
if (drawOutlineShadowsEnabled) {
canvas.drawRect(dims.canvasRect, outlineShadowPaint);
}
// not positive why this is, but the rect clipped by clipRect is 1 less than the one drawn by drawRect.
// so this is necessary to avoid clipping borders. I suspect that its a floating point
// jitter issue.
if (widget.isClippingEnabled()) {
canvas.clipRect(dims.canvasRect, Region.Op.INTERSECT);
}
widget.draw(canvas);
if (drawMarginsEnabled) {
drawSpacing(canvas, dims.canvasRect, dims.marginatedRect, getMarginPaint());
}
if (drawPaddingEnabled) {
drawSpacing(canvas, dims.marginatedRect, dims.paddedRect, getPaddingPaint());
}
if (drawAnchorsEnabled) {
PointF anchorCoords =
Widget.getAnchorCoordinates(coords.x, coords.y, elementWidth,
elementHeight, metrics.getAnchor());
drawAnchor(canvas, anchorCoords);
}
if (drawOutlinesEnabled) {
canvas.drawRect(dims.canvasRect, outlinePaint);
}
} finally {
canvas.restore();
}
}
}
private static void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) {
try {
canvas.save();
canvas.clipRect(inner, Region.Op.DIFFERENCE);
canvas.drawRect(outer, paint);
} finally {
canvas.restore();
}
}
protected void drawAnchor(Canvas canvas, PointF coords) {
float anchorSize = 4;
canvas.drawRect(coords.x-anchorSize, coords.y-anchorSize, coords.x+anchorSize, coords.y+anchorSize, anchorPaint);
}
public boolean isDrawOutlinesEnabled() {
return drawOutlinesEnabled;
}
public void setDrawOutlinesEnabled(boolean drawOutlinesEnabled) {
this.drawOutlinesEnabled = drawOutlinesEnabled;
}
public Paint getOutlinePaint() {
return outlinePaint;
}
public void setOutlinePaint(Paint outlinePaint) {
this.outlinePaint = outlinePaint;
}
public boolean isDrawAnchorsEnabled() {
return drawAnchorsEnabled;
}
public void setDrawAnchorsEnabled(boolean drawAnchorsEnabled) {
this.drawAnchorsEnabled = drawAnchorsEnabled;
}
public boolean isDrawMarginsEnabled() {
return drawMarginsEnabled;
}
public void setDrawMarginsEnabled(boolean drawMarginsEnabled) {
this.drawMarginsEnabled = drawMarginsEnabled;
}
public Paint getMarginPaint() {
return marginPaint;
}
public void setMarginPaint(Paint marginPaint) {
this.marginPaint = marginPaint;
}
public boolean isDrawPaddingEnabled() {
return drawPaddingEnabled;
}
public void setDrawPaddingEnabled(boolean drawPaddingEnabled) {
this.drawPaddingEnabled = drawPaddingEnabled;
}
public Paint getPaddingPaint() {
return paddingPaint;
}
public void setPaddingPaint(Paint paddingPaint) {
this.paddingPaint = paddingPaint;
}
public boolean isDrawOutlineShadowsEnabled() {
return drawOutlineShadowsEnabled;
}
public void setDrawOutlineShadowsEnabled(boolean drawOutlineShadowsEnabled) {
this.drawOutlineShadowsEnabled = drawOutlineShadowsEnabled;
if(drawOutlineShadowsEnabled && outlineShadowPaint == null) {
// use a default shadow effect in the case where none has been set:
outlineShadowPaint = new Paint();
outlineShadowPaint.setColor(Color.DKGRAY);
outlineShadowPaint.setStyle(Paint.Style.FILL);
outlineShadowPaint.setShadowLayer(3, 5, 5, Color.BLACK);
}
}
public Paint getOutlineShadowPaint() {
return outlineShadowPaint;
}
public void setOutlineShadowPaint(Paint outlineShadowPaint) {
this.outlineShadowPaint = outlineShadowPaint;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
/**
* Recalculates layouts for all widgets using last set
* DisplayDimensions. Care should be excersized when choosing when
* to call this method as it is a relatively slow operation.
*/
public void refreshLayout() {
for (Widget widget : elements()) {
widget.layout(displayDims);
}
}
@Override
public void layout(final DisplayDimensions dims) {
this.displayDims = dims;
refreshLayout();
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
abstract class LayoutMetric<LayoutType extends Enum> {
private LayoutType layoutType;
//private LayoutType layoutType;
private float value;
//private float lastRow;
public LayoutMetric(float value, LayoutType layoutType) {
validatePair(value, layoutType);
set(value, layoutType);
//setLayoutType(layoutType);
//setValue(value);
//setLayoutType(layoutType);
}
/**
* Verifies that the values passed in are valid for the layout algorithm being used.
* @param value
* @param layoutType
*/
protected abstract void validatePair(float value, LayoutType layoutType);
public void set(float value, LayoutType layoutType) {
validatePair(value, layoutType);
this.value = value;
this.layoutType = layoutType;
}
public float getValue() {
return value;
}
public void setValue(float value) {
validatePair(value, layoutType);
this.value = value;
}
public abstract float getPixelValue(float size);
public LayoutType getLayoutType() {
return layoutType;
}
public void setLayoutType(LayoutType layoutType) {
validatePair(value, layoutType);
this.layoutType = layoutType;
}
}
@@ -0,0 +1,87 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
public abstract class PositionMetric<LayoutType extends Enum> extends LayoutMetric<LayoutType> {
protected enum Origin {
FROM_BEGINING,
FROM_CENTER,
FROM_END
}
protected enum LayoutMode {
ABSOLUTE,
RELATIVE
}
public PositionMetric(float value, LayoutType layoutType) {
super(value, layoutType);
}
/**
* Throws IllegalArgumentException if there is a problem.
* @param value
* @param layoutMode
* @throws IllegalArgumentException
*/
protected static void validateValue(float value, LayoutMode layoutMode) throws IllegalArgumentException {
switch(layoutMode) {
case ABSOLUTE:
break;
case RELATIVE:
if(value < -1 || value > 1) {
throw new IllegalArgumentException("Relative layout values must be within the range of -1 to 1.");
}
break;
default:
throw new IllegalArgumentException("Unknown LayoutMode: " + layoutMode);
}
}
protected float getAbsolutePosition(float size, Origin origin) {
switch(origin) {
case FROM_BEGINING:
return getValue();
case FROM_CENTER:
return (size/2f) + getValue();
case FROM_END:
return size - getValue();
default:
throw new IllegalArgumentException("Unsupported Origin: " + origin);
}
}
protected float getRelativePosition(float size, Origin origin) {
//throw new UnsupportedOperationException("Not yet implemented.");
switch(origin) {
case FROM_BEGINING:
return size * getValue();
case FROM_CENTER:
return (size/2f) + ((size/2f) * getValue());
case FROM_END:
return size + (size*getValue());
default:
throw new IllegalArgumentException("Unsupported Origin: " + origin);
}
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import androidx.annotation.NonNull;
public class PositionMetrics implements Comparable<PositionMetrics> {
private HorizontalPosition horizontalPosition;
private VerticalPosition verticalPosition;
private Anchor anchor;
private float layerDepth;
public PositionMetrics(float x, HorizontalPositioning horizontalPositioning, float y, VerticalPositioning verticalPositioning, Anchor anchor) {
setXPositionMetric(new HorizontalPosition(x, horizontalPositioning));
setYPositionMetric(new VerticalPosition(y, verticalPositioning));
setAnchor(anchor);
}
public VerticalPosition getYPositionMetric() {
return verticalPosition;
}
public void setYPositionMetric(VerticalPosition verticalPosition) {
this.verticalPosition = verticalPosition;
}
public Anchor getAnchor() {
return anchor;
}
public void setAnchor(Anchor anchor) {
this.anchor = anchor;
}
@Override
public int compareTo(@NonNull PositionMetrics o) {
if(this.layerDepth < o.layerDepth) {
return -1;
} else if(this.layerDepth == o.layerDepth) {
return 0;
} else {
return 1;
}
}
public HorizontalPosition getXPositionMetric() {
return horizontalPosition;
}
public void setXPositionMetric(HorizontalPosition horizontalPosition) {
this.horizontalPosition = horizontalPosition;
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import com.androidplot.Series;
import com.androidplot.xy.XYSeriesFormatter;
public abstract class RenderBundle<RenderBundleType extends RenderBundle, SeriesType extends Series, SeriesFormatterType extends XYSeriesFormatter> {
//private XYDataset series;
private Series series;
private SeriesFormatterType formatter;
public RenderBundle(SeriesType series, SeriesFormatterType formatter) {
this.formatter = formatter;
this.series = series;
}
public Series getSeries() {
return series;
}
public void setSeries(Series series) {
this.series = series;
}
public SeriesFormatterType getFormatter() {
return formatter;
}
public void setFormatter(SeriesFormatterType formatter) {
this.formatter = formatter;
}
}
@@ -0,0 +1,103 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import com.androidplot.Plot;
import com.androidplot.Series;
import java.util.ArrayList;
import java.util.List;
/**
* 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<SeriesType extends Series, FormatterType extends Formatter> {
private final Plot plot;
private final ArrayList<StackElement<SeriesType, FormatterType>> elements;
public ArrayList<StackElement<SeriesType, FormatterType>> getElements() {
return elements;
}
/**
* An element on the render stack.
*/
public class StackElement<SeriesType extends Series, FormatterType extends Formatter> {
private SeriesBundle<SeriesType, FormatterType> seriesBundle;
private boolean isEnabled = true;
public StackElement(SeriesBundle<SeriesType, FormatterType> seriesBundle) {
set(seriesBundle);
}
public SeriesBundle<SeriesType, FormatterType> get() {
return seriesBundle;
}
public void set(SeriesBundle<SeriesType, FormatterType> seriesBundle) {
this.seriesBundle = seriesBundle;
}
public boolean isEnabled() {
return isEnabled;
}
/**
* Enable or disable a stack element for rendering. Has no effect on StackElements that
* have already been rendered.
* @param isEnabled
*/
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
}
}
public RenderStack(Plot plot) {
this.plot = plot;
elements = new ArrayList<>(plot.getRegistry().size());
}
/**
* Syncs the stack structure with plot's current state. Should be called before
* rendering series data to an XYGraphWidget.
*/
public void sync() {
getElements().clear();
List<SeriesBundle<SeriesType, FormatterType>> pairList
= plot.getRegistry().getSeriesAndFormatterList();
for(SeriesBundle<SeriesType, FormatterType> thisPair: pairList) {
getElements().add(new StackElement<>(thisPair));
}
}
/**
* Invokes {@link RenderStack.StackElement#setEnabled(boolean)} with a value
* of false on all stack elements associated with the specified renderer.
* @param rendererClass
*/
public void disable(Class<? extends SeriesRenderer> rendererClass) {
for(RenderStack.StackElement element : getElements()) {
if(element.get().getFormatter().getRendererClass() == rendererClass) {
element.setEnabled(false);
}
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import com.androidplot.util.DisplayDimensions;
/**
* Used by classes that depend on dimensional values to lay themselves out and draw.
* Consideration should be given to synchronizing with any draw routines that also
* exist within the class.
*/
public interface Resizable {
/**
* Called when a change to the class' dimensions is made. This method is responsible
* for cascading calls to update for any logical children of this class, for example
* the Plot class is responsible for updating the LayoutManager. Note that while dims
* is marked final in this interface, the compiler will not enforce it. Implementors of
* this method should take care not to make changes to dims as this will affect parent
* Resizables in likely undesired ways.
* @param dims
*/
void layout(final DisplayDimensions dims);
}
@@ -0,0 +1,46 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import com.androidplot.Series;
/**
* Defines a relationship between a Series instance and other elements needed to unique render that instance
* such as a Formatter etc.
*/
public class SeriesBundle<SeriesType extends Series, FormatterType extends Formatter> {
private final SeriesType series;
private final FormatterType formatter;
public SeriesBundle(SeriesType series, FormatterType formatter) {
this.series = series;
this.formatter = formatter;
}
public SeriesType getSeries() {
return series;
}
public FormatterType getFormatter() {
return formatter;
}
public boolean rendersWith(SeriesRenderer renderer) {
return getFormatter().getRendererClass() == renderer.getClass();
}
}
@@ -0,0 +1,121 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import android.graphics.Canvas;
import android.graphics.RectF;
import android.graphics.Region;
import com.androidplot.Series;
import com.androidplot.Plot;
import java.util.ArrayList;
import java.util.List;
public abstract class SeriesRenderer
<PlotType extends Plot, SeriesType extends Series, SeriesFormatterType extends Formatter> {
private PlotType plot;
public SeriesRenderer(PlotType plot) {
this.plot = plot;
}
public PlotType getPlot() {
return plot;
}
public void setPlot(PlotType plot) {
this.plot = plot;
}
public SeriesFormatterType getFormatter(SeriesType series) {
return (SeriesFormatterType) plot.getFormatter(series, getClass());
}
/**
*
* @param canvas
* @param plotArea
* @param sfPair The series / formatter pair to be rendered
*/
public void render(Canvas canvas, RectF plotArea, SeriesBundle<SeriesType,
SeriesFormatterType> sfPair, RenderStack stack) {
onRender(canvas, plotArea, sfPair.getSeries(), sfPair.getFormatter(), stack);
}
/**
*
* @param canvas
* @param plotArea
* @param series The series to be rendered
* @param formatter The getFormatter that should be used to render the series
* @param stack Ordered list of all series being renderered. May be manipulated by the Renderer
* to gain effect.
*/
protected abstract void onRender(Canvas canvas, RectF plotArea, SeriesType series,
SeriesFormatterType formatter, RenderStack stack);
/**
* Draw the legend icon in the rect passed in.
* @param canvas
* @param rect
*/
protected abstract void doDrawLegendIcon(Canvas canvas, RectF rect, SeriesFormatterType formatter);
public void drawSeriesLegendIcon(Canvas canvas, RectF rect, SeriesFormatterType formatter) {
try {
canvas.save();
canvas.clipRect(rect, Region.Op.INTERSECT);
doDrawLegendIcon(canvas, rect, formatter);
} finally {
canvas.restore();
}
}
/**
*
* @return A List of all {@link SeriesBundle} instances currently associated
* with this Renderer.
*/
public List<SeriesBundle<SeriesType, ? extends SeriesFormatterType>> getSeriesAndFormatterList() {
List<SeriesBundle<SeriesType, ? extends SeriesFormatterType>> results = new ArrayList<>();
List<SeriesBundle> sfList = getPlot().getRegistry().getSeriesAndFormatterList();
for(SeriesBundle<SeriesType, ? extends SeriesFormatterType> thisPair : sfList) {
if(thisPair.rendersWith(this)) {
results.add(thisPair);
}
}
return results;
}
/**
*
* @return
* @since 0.9.7
*/
public List<SeriesType> getSeriesList() {
List<SeriesType> results = new ArrayList<>();
List<SeriesBundle> sfList = getPlot().getRegistry().getSeriesAndFormatterList();
for(SeriesBundle<SeriesType, ? extends SeriesFormatterType> thisPair : sfList) {
if(thisPair.rendersWith(this)) {
results.add(thisPair.getSeries());
}
}
return results;
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import android.graphics.RectF;
import com.androidplot.util.PixelUtils;
/**
* Defines physical dimensions & scaling characteristics
*/
public class Size {
// convenience value; sets size to 100% width and height of the widget container.
public static Size FILL = new Size(0, SizeMode.FILL, 0, SizeMode.FILL);
private SizeMetric height;
private SizeMetric width;
/**
* Convenience constructor. Wraps {@link #Size(SizeMetric, SizeMetric)}.
* @param height Height value used algorithm to calculate the height of the associated widget(s).
* @param heightLayoutType Algorithm used to calculate the height of the associated widget(s).
* @param width Width value used algorithm to calculate the width of the associated widget(s).
* @param widthLayoutType Algorithm used to calculate the width of the associated widget(s).
*/
public Size(float height, SizeMode heightLayoutType, float width, SizeMode widthLayoutType) {
this.height = new SizeMetric(height, heightLayoutType);
this.width = new SizeMetric(width, widthLayoutType);
}
/**
* Creates a new SizeMetrics instance using the specified size layout algorithm and value.
* See {@link SizeMetric} for details on what can be passed in.
* @param height
* @param width
*/
public Size(SizeMetric height, SizeMetric width) {
this.height = height;
this.width = width;
}
public SizeMetric getHeight() {
return height;
}
public void setHeight(SizeMetric height) {
this.height = height;
}
public SizeMetric getWidth() {
return width;
}
/**
* Calculates a RectF with calculated width and height. The top-left corner is set to 0,0.
* @param canvasRect
* @return
*/
public RectF getRectF(RectF canvasRect) {
return new RectF(
0,
0,
width.getPixelValue(canvasRect.width()),
height.getPixelValue(canvasRect.height()));
}
public void setWidth(SizeMetric width) {
this.width = width;
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
/**
* Encapsulates a sizing algorithm and an associated value.
*
* The available algorithms list are stored in the {@link SizeMode} enumeration.
*
*/
public class SizeMetric extends LayoutMetric<SizeMode> {
public SizeMetric(float value, SizeMode layoutType) {
super(value, layoutType);
}
protected void validatePair(float value, SizeMode layoutType) {
switch(layoutType) {
case RELATIVE:
if(value < 0 || value > 1) {
throw new IllegalArgumentException("SizeMetric Relative and Hybrid layout values must be within the range of 0 to 1.");
}
case ABSOLUTE:
case FILL:
default:
break;
}
}
@Override
public float getPixelValue(float size) {
switch(getLayoutType()) {
case ABSOLUTE:
return getValue();
case RELATIVE:
return getValue() * size;
case FILL:
return size - getValue();
default:
throw new IllegalArgumentException("Unsupported LayoutType: " + this.getLayoutType());
}
}
@Override
public void setLayoutType(SizeMode layoutType) {
super.setLayoutType(layoutType);
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
/**
* Algorithms available for calculating an arbitrary dimension of a widget.
* Each algorithm also takes a single value called "val" in this doc.
* ABSOLUTE - Val is treated as absolute. If val is 5 then the size of the widget along the associated axis is 5 pixels.
*
* RELATIVE - Val represents the percentage of the display that the widget should fill along the associated axis. For example,
* if the total size of the owning plot is 120 pixels and val is set to 50 then the size of the widget along the associated axis
* is 60; 50% of 120 = 60.
*
* FILL - Widget completely fills along the associated axis, minus the input size value
*/
public enum SizeMode {
ABSOLUTE,
RELATIVE,
FILL
}
@@ -0,0 +1,51 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
import android.graphics.RectF;
import java.util.Iterator;
public abstract class TableModel {
private TableOrder order;
protected TableModel(TableOrder order) {
setOrder(order);
}
public abstract Iterator<RectF> getIterator(RectF tableRect, int totalElements);
//public abstract RectF getCellRect(RectF tableRect, int numElements);
public TableOrder getOrder() {
return order;
}
public void setOrder(TableOrder order) {
this.order = order;
}
public enum Axis {
ROW,
COLUMN
}
public enum CellSizingMethod {
FIXED,
FILL
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
public enum TableOrder {
ROW_MAJOR, // standard c-style
COLUMN_MAJOR
}
@@ -0,0 +1,28 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
/**
* The sizing methods available to a table.
* AUTO: The table is divided evenly int tableSize/numElements sections.
* FIXED: Each element in the table has a predefined number of pixels
* regardless of what the table's dimensions actually are.
*/
public enum TableSizingMethod {
AUTO,
FIXED
}
@@ -0,0 +1,23 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
public enum TextOrientation {
HORIZONTAL,
VERTICAL_ASCENDING,
VERTICAL_DESCENDING
}
@@ -0,0 +1,67 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
public class VerticalPosition extends PositionMetric<VerticalPositioning> {
public VerticalPosition(float value, VerticalPositioning layoutStyle) {
super(value, layoutStyle);
}
/**
* Throws IllegalArgumentException if there is a problem.
* @param value
*/
protected void validatePair(float value, VerticalPositioning layoutStyle) {
switch(layoutStyle) {
case ABSOLUTE_FROM_TOP:
case ABSOLUTE_FROM_BOTTOM:
case ABSOLUTE_FROM_CENTER:
validateValue(value, PositionMetric.LayoutMode.ABSOLUTE);
break;
case RELATIVE_TO_TOP:
case RELATIVE_TO_BOTTOM:
case RELATIVE_TO_CENTER:
validateValue(value, PositionMetric.LayoutMode.RELATIVE);
}
}
@Override
public float getPixelValue(float size) {
switch(getLayoutType()) {
case ABSOLUTE_FROM_TOP:
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_BEGINING);
case ABSOLUTE_FROM_BOTTOM:
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_END);
case ABSOLUTE_FROM_CENTER:
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_CENTER);
case RELATIVE_TO_TOP:
return this.getRelativePosition(size, PositionMetric.Origin.FROM_BEGINING);
case RELATIVE_TO_BOTTOM:
return this.getRelativePosition(size, PositionMetric.Origin.FROM_END);
case RELATIVE_TO_CENTER:
return this.getRelativePosition(size, PositionMetric.Origin.FROM_CENTER);
default:
throw new IllegalArgumentException("Unsupported LayoutType: " + this.getLayoutType());
}
}
@Override
public void setLayoutType(VerticalPositioning layoutType) {
super.setLayoutType(layoutType);
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui;
public enum VerticalPositioning {
ABSOLUTE_FROM_TOP,
ABSOLUTE_FROM_BOTTOM,
ABSOLUTE_FROM_CENTER,
RELATIVE_TO_TOP,
RELATIVE_TO_BOTTOM,
RELATIVE_TO_CENTER
}
@@ -0,0 +1,13 @@
package com.androidplot.ui.widget;
/**
* An item to be displayed by {@link LegendWidget}.
*/
public interface LegendItem {
/**
*
* @return The user facing label for this item.
*/
String getTitle();
}
@@ -0,0 +1,194 @@
package com.androidplot.ui.widget;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import com.androidplot.ui.LayoutManager;
import com.androidplot.ui.Size;
import com.androidplot.ui.TableModel;
import com.androidplot.util.FontUtils;
import com.androidplot.util.PixelUtils;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
/**
* Provides core functionality for displaying a legend widget within a {@link com.androidplot.Plot}.
* @param <ItemT>
*/
public abstract class LegendWidget<ItemT extends LegendItem> 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<ItemT> 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<ItemT> items = getLegendItems();
if(legendItemComparator != null) {
Collections.sort(items, legendItemComparator);
}
final Iterator<RectF> 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<ItemT> 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<ItemT> 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<ItemT> legendItemComparator) {
this.legendItemComparator = legendItemComparator;
}
}
@@ -0,0 +1,171 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui.widget;
import android.graphics.*;
import com.androidplot.ui.*;
import com.androidplot.util.FontUtils;
public class TextLabelWidget extends Widget {
private String text;
private Paint labelPaint;
private TextOrientation orientation;
private boolean autoPackEnabled = true;
{
labelPaint = new Paint();
labelPaint.setColor(Color.WHITE);
labelPaint.setAntiAlias(true);
labelPaint.setTextAlign(Paint.Align.CENTER);
setClippingEnabled(false);
}
public TextLabelWidget(LayoutManager layoutManager, Size size) {
this(layoutManager, size, TextOrientation.HORIZONTAL);
}
public TextLabelWidget(LayoutManager layoutManager, String title, Size size, TextOrientation orientation) {
this(layoutManager, size, orientation);
setText(title);
}
public TextLabelWidget(LayoutManager layoutManager, Size size, TextOrientation orientation) {
super(layoutManager, new Size(0, SizeMode.ABSOLUTE, 0, SizeMode.ABSOLUTE));
setSize(size);
this.orientation = orientation;
}
@Override
protected void onMetricsChanged(Size olds, Size news) {
if(autoPackEnabled) {
pack();
}
}
@Override
public void onPostInit() {
if(autoPackEnabled) {
pack();
}
}
/**
* Sets the dimensions of the widget to exactly contain the text contents
*/
public void pack() {
Rect size = FontUtils.getStringDimensions(text, getLabelPaint());
if(size == null) {
return;
}
switch(orientation) {
case HORIZONTAL:
setSize(new Size(size.height(), SizeMode.ABSOLUTE, size.width()+2, SizeMode.ABSOLUTE));
break;
case VERTICAL_ASCENDING:
case VERTICAL_DESCENDING:
setSize(new Size(size.width(), SizeMode.ABSOLUTE, size.height()+2, SizeMode.ABSOLUTE));
break;
}
refreshLayout();
}
/**
* Do not call this method directly. It is indirectly invoked every time a plot is
* redrawn.
* @param canvas The Canvas to draw onto
* @param widgetRect the size and coordinates of this widget
*/
@Override
public void doOnDraw(Canvas canvas, RectF widgetRect) {
if(text == null || text.length() == 0) {
return;
}
float vOffset = labelPaint.getFontMetrics().descent;
PointF start = getAnchorCoordinates(widgetRect,
Anchor.CENTER);
try {
canvas.save();
canvas.translate(start.x, start.y);
switch (orientation) {
case HORIZONTAL:
break;
case VERTICAL_ASCENDING:
canvas.rotate(-90);
break;
case VERTICAL_DESCENDING:
canvas.rotate(90);
break;
default:
throw new UnsupportedOperationException("Orientation " + orientation + " not yet implemented for TextLabelWidget.");
}
canvas.drawText(text, 0, vOffset, labelPaint);
} finally {
canvas.restore();
}
}
public Paint getLabelPaint() {
return labelPaint;
}
public void setLabelPaint(Paint labelPaint) {
this.labelPaint = labelPaint;
// when paint changes, packing params change too so run
// to see if we need to resize:
if(autoPackEnabled) {
pack();
}
}
public TextOrientation getOrientation() {
return orientation;
}
public void setOrientation(TextOrientation orientation) {
this.orientation = orientation;
if(autoPackEnabled) {
pack();
}
}
public boolean isAutoPackEnabled() {
return autoPackEnabled;
}
public void setAutoPackEnabled(boolean autoPackEnabled) {
this.autoPackEnabled = autoPackEnabled;
if(autoPackEnabled) {
pack();
}
}
public void setText(String text) {
this.text = text;
if(autoPackEnabled) {
pack();
}
}
public String getText() {
return text;
}
}
@@ -0,0 +1,490 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.ui.widget;
import android.graphics.*;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import com.androidplot.ui.*;
import com.androidplot.util.DisplayDimensions;
import com.androidplot.ui.HorizontalPositioning;
import com.androidplot.ui.VerticalPositioning;
import com.androidplot.util.PixelUtils;
/**
* A Widget is a graphical sub-element of a Plot that can be positioned relative
* to the bounds of the Plot.
*/
public abstract class Widget implements BoxModelable, Resizable {
private Paint borderPaint;
private Paint backgroundPaint;
private boolean clippingEnabled = false;
private BoxModel boxModel = new BoxModel();
private Size size;
private DisplayDimensions plotDimensions = new DisplayDimensions();
private DisplayDimensions widgetDimensions = new DisplayDimensions();
private boolean isVisible = true;
private PositionMetrics positionMetrics;
private LayoutManager layoutManager;
private Rotation rotation = Rotation.NONE;
private RectF lastWidgetRect = null;
public enum Rotation {
NINETY_DEGREES,
NEGATIVE_NINETY_DEGREES,
ONE_HUNDRED_EIGHTY_DEGREES,
NONE,
}
public Widget(LayoutManager layoutManager, SizeMetric heightMetric, SizeMetric widthMetric) {
this(layoutManager, new Size(heightMetric, widthMetric));
}
public Widget(LayoutManager layoutManager, Size size) {
this.layoutManager = layoutManager;
Size oldSize = this.size;
setSize(size);
onMetricsChanged(oldSize, size);
}
public DisplayDimensions getWidgetDimensions() {
return widgetDimensions;
}
public Anchor getAnchor() {
return getPositionMetrics().getAnchor();
}
public void setAnchor(Anchor anchor) {
getPositionMetrics().setAnchor(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
* @param verticalPositioning
*/
public void position(float x, HorizontalPositioning horizontalPositioning, float y, VerticalPositioning verticalPositioning) {
position(x, horizontalPositioning, y, verticalPositioning, Anchor.LEFT_TOP);
}
/**
* @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1.
* @param horizontalPositioning LayoutType to use when orienting this element's X-Coordinate.
* @param y Y_VALS_ONLY-Coordinate of the top-left corner of element. When using RELATIVE, must be a value between 0 and 1.
* @param verticalPositioning LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate.
* @param anchor The point of reference used by this positioning call.
*/
public void position(float x, HorizontalPositioning horizontalPositioning, float y,
VerticalPositioning verticalPositioning, Anchor anchor) {
setPositionMetrics(new PositionMetrics(x, horizontalPositioning, y, verticalPositioning, anchor));
layoutManager.addToTop(this);
}
/**
* Can be overridden by subclasses to respond to resizing events.
*
* @param oldSize
* @param newSize
*/
protected void onMetricsChanged(Size oldSize, Size newSize) {
}
/**
* Can be overridden by subclasses to handle any final resizing etc. that
* can only be done after XML configuration etc. has completed.
*/
public void onPostInit() {
}
/**
* Determines whether or not point lies within this Widget.
*
* @param point
* @return
*/
public boolean containsPoint(PointF point) {
return widgetDimensions.canvasRect.contains(point.x, point.y);
}
public void setSize(Size size) {
this.size = size;
}
public Size getSize() {
return this.size;
}
public void setWidth(float width) {
size.getWidth().setValue(width);
}
public void setWidth(float width, SizeMode layoutType) {
size.getWidth().set(width, layoutType);
}
public void setHeight(float height) {
size.getHeight().setValue(height);
}
public void setHeight(float height, SizeMode layoutType) {
size.getHeight().set(height, layoutType);
}
public SizeMetric getWidthMetric() {
return size.getWidth();
}
public SizeMetric getHeightMetric() {
return size.getHeight();
}
public float getWidthPix(float size) {
return this.size.getWidth().getPixelValue(size);
}
public float getHeightPix(float size) {
return this.size.getHeight().getPixelValue(size);
}
public RectF getMarginatedRect(RectF widgetRect) {
return boxModel.getMarginatedRect(widgetRect);
}
public RectF getPaddedRect(RectF widgetMarginRect) {
return boxModel.getPaddedRect(widgetMarginRect);
}
@Override
public void setMarginRight(float marginRight) {
boxModel.setMarginRight(marginRight);
}
@Override
public void setMargins(float left, float top, float right, float bottom) {
boxModel.setMargins(left, top, right, bottom);
}
@Override
public void setPadding(float left, float top, float right, float bottom) {
boxModel.setPadding(left, top, right, bottom);
}
@Override
public float getMarginTop() {
return boxModel.getMarginTop();
}
@Override
public void setMarginTop(float marginTop) {
boxModel.setMarginTop(marginTop);
}
@Override
public float getMarginBottom() {
return boxModel.getMarginBottom();
}
@Override
public float getPaddingLeft() {
return boxModel.getPaddingLeft();
}
@Override
public void setPaddingLeft(float paddingLeft) {
boxModel.setPaddingLeft(paddingLeft);
}
@Override
public float getPaddingTop() {
return boxModel.getPaddingTop();
}
@Override
public void setPaddingTop(float paddingTop) {
boxModel.setPaddingTop(paddingTop);
}
@Override
public float getPaddingRight() {
return boxModel.getPaddingRight();
}
@Override
public void setPaddingRight(float paddingRight) {
boxModel.setPaddingRight(paddingRight);
}
@Override
public float getPaddingBottom() {
return boxModel.getPaddingBottom();
}
@Override
public void setPaddingBottom(float paddingBottom) {
boxModel.setPaddingBottom(paddingBottom);
}
@Override
@SuppressWarnings("SameParameterValue")
public void setMarginBottom(float marginBottom) {
boxModel.setMarginBottom(marginBottom);
}
@Override
public float getMarginLeft() {
return boxModel.getMarginLeft();
}
@Override
public void setMarginLeft(float marginLeft) {
boxModel.setMarginLeft(marginLeft);
}
@Override
public float getMarginRight() {
return boxModel.getMarginRight();
}
/**
* Causes the pixel dimensions used for rendering this Widget
* to be recalculated. Should be called any time a parameter that factors
* into this Widget's size or position is altered.
*/
public synchronized void refreshLayout() {
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.
return;
}
float elementWidth = getWidthPix(plotDimensions.paddedRect.width());
float elementHeight = getHeightPix(plotDimensions.paddedRect.height());
PointF coords = calculateCoordinates(elementHeight,
elementWidth, plotDimensions.paddedRect, positionMetrics);
RectF widgetRect = new RectF(coords.x, coords.y,
coords.x + elementWidth, coords.y + elementHeight);
RectF marginatedWidgetRect = getMarginatedRect(widgetRect);
RectF paddedWidgetRect = getPaddedRect(marginatedWidgetRect);
widgetDimensions = new DisplayDimensions(widgetRect,
marginatedWidgetRect, paddedWidgetRect);
}
@Override
public synchronized void layout(final DisplayDimensions plotDimensions) {
this.plotDimensions = plotDimensions;
refreshLayout();
}
public static PointF calculateCoordinates(float height, float width, RectF viewRect, PositionMetrics metrics) {
float x = metrics.getXPositionMetric().getPixelValue(viewRect.width()) + viewRect.left;
float y = metrics.getYPositionMetric().getPixelValue(viewRect.height()) + viewRect.top;
PointF point = new PointF(x, y);
return PixelUtils.sub(point, getAnchorOffset(width, height, metrics.getAnchor()));
}
public static PointF getAnchorOffset(float width, float height, Anchor anchor) {
PointF point = new PointF();
switch (anchor) {
case LEFT_TOP:
break;
case LEFT_MIDDLE:
point.set(0, height / 2);
break;
case LEFT_BOTTOM:
point.set(0, height);
break;
case RIGHT_TOP:
point.set(width, 0);
break;
case RIGHT_BOTTOM:
point.set(width, height);
break;
case RIGHT_MIDDLE:
point.set(width, height / 2);
break;
case TOP_MIDDLE:
point.set(width / 2, 0);
break;
case BOTTOM_MIDDLE:
point.set(width / 2, height);
break;
case CENTER:
point.set(width / 2, height / 2);
break;
default:
throw new IllegalArgumentException("Unsupported anchor location: " + anchor);
}
return point;
}
public static PointF getAnchorCoordinates(RectF widgetRect, Anchor anchor) {
return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top),
getAnchorOffset(widgetRect.width(), widgetRect.height(), anchor));
}
public static PointF getAnchorCoordinates(float x, float y, float width, float height, Anchor anchor) {
return getAnchorCoordinates(new RectF(x, y, x + width, y + height), anchor);
}
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) {
if (isVisible()) {
if (backgroundPaint != null) {
drawBackground(canvas, widgetDimensions.canvasRect);
}
canvas.save();
final RectF widgetRect = applyRotation(canvas, widgetDimensions.paddedRect);
checkSize(widgetRect);
doOnDraw(canvas, widgetRect);
canvas.restore();
if (borderPaint != null) {
drawBorder(canvas, widgetRect);
}
}
}
protected RectF applyRotation(Canvas canvas, RectF rect) {
float rotationDegs = 0;
final float cx = widgetDimensions.paddedRect.centerX();
final float cy = widgetDimensions.paddedRect.centerY();
final float halfHeight = widgetDimensions.paddedRect.height() / 2;
final float halfWidth = widgetDimensions.paddedRect.width() / 2;
switch (rotation) {
case NINETY_DEGREES:
rotationDegs = 90;
rect = new RectF(
cx - halfHeight,
cy - halfWidth,
cx + halfHeight,
cy + halfWidth);
break;
case NEGATIVE_NINETY_DEGREES:
rotationDegs = -90;
rect = new RectF(
cx - halfHeight,
cy - halfWidth,
cx + halfHeight,
cy + halfWidth);
break;
case ONE_HUNDRED_EIGHTY_DEGREES:
rotationDegs = 180;
// fall through
case NONE:
break;
default:
throw new UnsupportedOperationException("Not yet implemented.");
}
if (rotation != Rotation.NONE) {
canvas.rotate(rotationDegs, cx, cy);
}
return rect;
}
protected void drawBorder(Canvas canvas, RectF paddedRect) {
canvas.drawRect(paddedRect, borderPaint);
}
protected void drawBackground(Canvas canvas, RectF widgetRect) {
canvas.drawRect(widgetRect, backgroundPaint);
}
/**
* @param canvas The Canvas to draw onto
* @param widgetRect the size and coordinates of this widget
*/
protected abstract void doOnDraw(Canvas canvas, RectF widgetRect);
public Paint getBorderPaint() {
return borderPaint;
}
public void setBorderPaint(Paint borderPaint) {
this.borderPaint = borderPaint;
}
public Paint getBackgroundPaint() {
return backgroundPaint;
}
public void setBackgroundPaint(Paint backgroundPaint) {
this.backgroundPaint = backgroundPaint;
}
public boolean isClippingEnabled() {
return clippingEnabled;
}
public void setClippingEnabled(boolean clippingEnabled) {
this.clippingEnabled = clippingEnabled;
}
public boolean isVisible() {
return isVisible;
}
public void setVisible(boolean visible) {
isVisible = visible;
}
public PositionMetrics getPositionMetrics() {
return positionMetrics;
}
public void setPositionMetrics(PositionMetrics positionMetrics) {
this.positionMetrics = positionMetrics;
}
public Rotation getRotation() {
return rotation;
}
public void setRotation(Rotation rotation) {
this.rotation = rotation;
}
}
@@ -0,0 +1,26 @@
package com.androidplot.util;
import android.os.*;
/**
* Wraps {@link Trace} to provide API-safe methods as well as an easy target for runtime removal
* via obfuscation.
*/
public abstract class APTrace {
public static void begin(final String sectionName) {
if(Build.VERSION.SDK_INT >= 18) {
Trace.beginSection(sectionName);
} else {
// TODO: alternate impl?
}
}
public static void end() {
if(Build.VERSION.SDK_INT >= 18) {
Trace.endSection();
} else {
// TODO: alternate impl?
}
}
}
@@ -0,0 +1,268 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.util;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.util.*;
import com.androidplot.ui.*;
import com.androidplot.ui.Size;
import com.androidplot.ui.widget.Widget;
import com.androidplot.xy.StepMode;
import com.androidplot.xy.StepModel;
/**
* Methods for applying styleable attributes.
*
*/
public class AttrUtils {
private static final String TAG = AttrUtils.class.getName();
public static void configureInsets(TypedArray attrs, Insets insets,
int topAttr, int bottomAttr, int leftAttr, int rightAttr) {
insets.setTop(attrs.getDimension(topAttr, insets.getTop()));
insets.setBottom(attrs.getDimension(bottomAttr, insets.getBottom()));
insets.setLeft(attrs.getDimension(leftAttr, insets.getLeft()));
insets.setRight(attrs.getDimension(rightAttr, insets.getRight()));
}
/**
* Configure a {@link Paint} instance used for drawing text from xml attrs.
* @param attrs
* @param paint
* @param colorAttr
* @param textSizeAttr
*/
public static void configureTextPaint(TypedArray attrs, Paint paint,
int colorAttr, int textSizeAttr) {
configureTextPaint(attrs, paint, colorAttr, textSizeAttr, null);
}
/**
* Configure a {@link Paint} instance used for drawing text from xml attrs.
* @param attrs
* @param paint
* @param colorAttr
* @param textSizeAttr
* @param alignAttr
*/
public static void configureTextPaint(TypedArray attrs, Paint paint, int colorAttr,
int textSizeAttr, Integer alignAttr) {
if(attrs != null) {
setColor(attrs, paint, colorAttr);
setTextSize(attrs, paint, textSizeAttr);
if(alignAttr != null && attrs.hasValue(alignAttr)) {
configureTextAlign(attrs, paint, alignAttr);
}
}
}
/**
* Configure {@link Paint} text alignment from xml attrs.
* @param attrs
* @param paint
* @param alignAttr
*/
public static void configureTextAlign(TypedArray attrs, Paint paint, int alignAttr) {
if (attrs != null) {
//if(attrs.hasValue(alignAttr)) {
final Paint.Align alignment = Paint.Align.values()
[attrs.getInt(alignAttr, paint.getTextAlign().ordinal())];
paint.setTextAlign(alignment);
//}
}
}
/**
* Configure a {@link Paint} instance used for drawing lines from xml attrs.
* @param attrs
* @param paint
* @param colorAttr
* @param strokeWidthAttr
*/
public static void configureLinePaint(TypedArray attrs, Paint paint, int colorAttr, int strokeWidthAttr) {
if(attrs != null) {
setColor(attrs, paint, colorAttr);
paint.setStrokeWidth(attrs.getDimension(strokeWidthAttr, paint.getStrokeWidth()));
}
}
public static void setColor(TypedArray attrs, Paint paint, int attrId) {
if(paint == null) {
Log.w(TAG, "Attempt to configure null Paint property for attrId: " + attrId);
} else {
paint.setColor(attrs.getColor(attrId, paint.getColor()));
}
}
public static void setTextSize(TypedArray attrs, Paint paint, int attrId) {
paint.setTextSize(attrs.getDimension(attrId, paint.getTextSize()));
}
/**
* Configure a {@link BoxModelable} instance from xml attrs.
* @param attrs
* @param model
* @param marginTop
* @param marginBottom
* @param marginLeft
* @param marginRight
* @param paddingTop
* @param paddingBottom
* @param paddingLeft
* @param paddingRight
*/
public static void configureBoxModelable(TypedArray attrs, BoxModelable model, int marginTop, int marginBottom,
int marginLeft, int marginRight, int paddingTop, int paddingBottom,
int paddingLeft, int paddingRight) {
if(attrs != null) {
model.setMargins(attrs.getDimension(marginLeft, model.getMarginLeft()),
attrs.getDimension(marginTop, model.getMarginTop()),
attrs.getDimension(marginRight, model.getMarginRight()),
attrs.getDimension(marginBottom, model.getMarginBottom()));
model.setPadding(attrs.getDimension(paddingLeft, model.getPaddingLeft()),
attrs.getDimension(paddingTop, model.getPaddingTop()),
attrs.getDimension(paddingRight, model.getPaddingRight()),
attrs.getDimension(paddingBottom, model.getPaddingBottom()));
}
}
/**
* Configure a {@link Size} instance from xml attrs.
* @param attrs
* @param model
* @param heightSizeLayoutTypeAttr
* @param heightAttr
* @param widthSizeLayoutTypeAttr
* @param widthAttr
*/
public static void configureSize(TypedArray attrs, Size model, int heightSizeLayoutTypeAttr, int heightAttr,
int widthSizeLayoutTypeAttr, int widthAttr) {
if(attrs != null) {
configureSizeMetric(attrs, model.getHeight(), heightSizeLayoutTypeAttr, heightAttr);
configureSizeMetric(attrs, model.getWidth(), widthSizeLayoutTypeAttr, widthAttr);
}
}
private static void configureSizeMetric(TypedArray attrs, SizeMetric model, int typeAttr, int valueAttr) {
final float value = getIntFloatDimenValue(attrs, valueAttr, model.getValue()).floatValue();
final SizeMode sizeMode =
getSizeLayoutType(attrs, typeAttr, model.getLayoutType());
model.set(value, sizeMode);
}
private static SizeMode getSizeLayoutType(TypedArray attrs, int attr, SizeMode defaultValue) {
return SizeMode.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
public static void configureWidget(TypedArray attrs, Widget widget, int heightSizeLayoutTypeAttr, int heightAttr,
int widthSizeLayoutTypeAttr, int widthAttr, int xLayoutStyleAttr,
int xLayoutValueAttr, int yLayoutStyleAttr, int yLayoutValueAttr,
int anchorPositionAttr, int visibilityAttr) {
if(attrs != null) {
configureSize(attrs, widget.getSize(), heightSizeLayoutTypeAttr,
heightAttr, widthSizeLayoutTypeAttr, widthAttr);
configurePositionMetrics(attrs, widget.getPositionMetrics(), xLayoutStyleAttr, xLayoutValueAttr,
yLayoutStyleAttr, yLayoutValueAttr, anchorPositionAttr);
widget.setVisible(attrs.getBoolean(visibilityAttr, widget.isVisible()));
}
}
public static void configureWidgetRotation(TypedArray attrs, Widget widget, int rotationAttr) {
if(attrs != null) {
widget.setRotation(getWidgetRotation(attrs, rotationAttr, Widget.Rotation.NONE));
}
}
/**
* Configure a {@link Widget} from xml attrs.
* @param attrs
* @param metrics
* @param xLayoutStyleAttr
* @param xLayoutValueAttr
* @param yLayoutStyleAttr
* @param yLayoutValueAttr
* @param anchorPositionAttr
*/
public static void configurePositionMetrics(TypedArray attrs, PositionMetrics metrics, int xLayoutStyleAttr,
int xLayoutValueAttr, int yLayoutStyleAttr, int yLayoutValueAttr,
int anchorPositionAttr) {
if(attrs != null && metrics != null) {
metrics.getXPositionMetric().set(
getIntFloatDimenValue(attrs, xLayoutValueAttr, metrics.getXPositionMetric().getValue()).floatValue(),
getXLayoutStyle(attrs, xLayoutStyleAttr, metrics.getXPositionMetric().getLayoutType()));
metrics.getYPositionMetric().set(
getIntFloatDimenValue(attrs, yLayoutValueAttr, metrics.getYPositionMetric().getValue()).floatValue(),
getYLayoutStyle(attrs, yLayoutStyleAttr, metrics.getYPositionMetric().getLayoutType()));
metrics.setAnchor(getAnchorPosition(attrs, anchorPositionAttr, metrics.getAnchor()));
}
}
/**
* Convenience method to retrieve values from xml that can be entered as a int, float or dimen.
* @param attrs
* @param valueAttr
* @param defaultValue
* @return
*/
private static Number getIntFloatDimenValue(TypedArray attrs, int valueAttr, Number defaultValue) {
Number result = defaultValue;
if(attrs != null && attrs.hasValue(valueAttr)) {
final int valueType = attrs.peekValue(valueAttr).type;
if (valueType == TypedValue.TYPE_DIMENSION) {
result = attrs.getDimension(valueAttr, defaultValue.floatValue());
} else if(valueType == TypedValue.TYPE_INT_DEC) {
result = attrs.getInt(valueAttr, defaultValue.intValue());
} else if (valueType == TypedValue.TYPE_FLOAT) {
result = attrs.getFloat(valueAttr, defaultValue.floatValue());
} else {
throw new IllegalArgumentException("Invalid value type - must be int, float or dimension.");
}
}
return result;
}
private static HorizontalPositioning getXLayoutStyle(TypedArray attrs, int attr, HorizontalPositioning defaultValue) {
return HorizontalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
private static VerticalPositioning getYLayoutStyle(TypedArray attrs, int attr, VerticalPositioning defaultValue) {
return VerticalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
private static Widget.Rotation getWidgetRotation(TypedArray attrs, int attr, Widget.Rotation defaultValue) {
return Widget.Rotation.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
private static Anchor getAnchorPosition(TypedArray attrs, int attr, Anchor defaultValue) {
return Anchor.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
public static void configureStep(TypedArray attrs, StepModel model, int stepModeAttr, int stepValueAttr) {
if(attrs != null) {
model.setMode(StepMode.values()[attrs.getInt(stepModeAttr, model.getMode().ordinal())]);
model.setValue(getIntFloatDimenValue(attrs, stepValueAttr, model.getValue()).doubleValue());
}
}
}
@@ -0,0 +1,47 @@
/*
* 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.RectF;
/**
* Convenience class for managing {@link com.androidplot.ui.BoxModel} data
*/
public class DisplayDimensions {
private static final int ONE = 1;
public final RectF canvasRect;
public final RectF marginatedRect;
public final RectF paddedRect;
// init to 1 to avoid potential divide by zero errors (yet to be observed in practice)
private static final RectF initRect;
static {
initRect = new RectF(ONE, ONE, ONE, ONE);
}
public DisplayDimensions() {
this(initRect, initRect, initRect);
}
public DisplayDimensions(RectF canvasRect, RectF marginatedRect, RectF paddedRect) {
this.canvasRect = canvasRect;
this.marginatedRect = marginatedRect;
this.paddedRect = paddedRect;
}
}
@@ -0,0 +1,119 @@
package com.androidplot.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* An extension of {@link Number} optimized for speed at the cost of memory.
*/
public class FastNumber extends Number {
@NonNull private final Number number;
private boolean hasDoublePrimitive;
private boolean hasFloatPrimitive;
private boolean hasIntPrimitive;
private double doublePrimitive;
private float floatPrimitive;
private int intPrimitive;
/**
* Safe-instantiator of FastNumber; returns a null result if the input Number is also null.
* @param number
* @return
*/
public static FastNumber orNull(@NonNull Number number) {
if(number == null) {
return null;
} else {
return new FastNumber(number);
}
}
private FastNumber(@NonNull Number number) {
//noinspection ConstantConditions //in case someone ignores the @NonNull annotation
if (number == null) {
throw new IllegalArgumentException("number parameter cannot be null");
}
// avoid nested instances of FastNumber :
if(number instanceof FastNumber) {
FastNumber rhs = (FastNumber) number;
this.number = rhs.number;
this.hasDoublePrimitive = rhs.hasDoublePrimitive;
this.hasFloatPrimitive = rhs.hasFloatPrimitive;
this.hasIntPrimitive = rhs.hasIntPrimitive;
this.doublePrimitive = rhs.doublePrimitive;
this.floatPrimitive = rhs.floatPrimitive;
this.intPrimitive = rhs.intPrimitive;
} else {
this.number = number;
}
}
@Override
public int intValue() {
if(!hasIntPrimitive) {
intPrimitive = number.intValue();
hasIntPrimitive = true;
}
return intPrimitive;
}
@Override
public long longValue() {
// TODO: optimize me!
return number.longValue();
}
@Override
public float floatValue() {
if(!hasFloatPrimitive) {
floatPrimitive = number.floatValue();
hasFloatPrimitive = true;
}
return floatPrimitive;
}
@Override
public double doubleValue() {
if(!hasDoublePrimitive) {
doublePrimitive = number.doubleValue();
hasDoublePrimitive = true;
}
return doublePrimitive;
}
/**
* To be equal, two instances must both be instances of {@link FastNumber}. The inner {@link
* #number} field must also be a common type. Numbers which are mathematically equal are not
* necessarily equal. This keeps with the java implementation of common Number classes where for
* instance {@code new Integer(0).equals(new Double(0))} returns {@code false}
*/
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FastNumber that = (FastNumber) o;
return number.equals(that.number);
}
@Override
public int hashCode() {
return number.hashCode();
}
@NonNull
@Override
public String toString() {
return String.valueOf(doubleValue());
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.util;
import android.graphics.*;
public class FontUtils {
private static final int ZERO = 0;
/**
* Determines the height of the tallest character that can be drawn by paint.
* @param paint
* @return
*/
public static float getFontHeight(Paint paint) {
Paint.FontMetrics metrics = paint.getFontMetrics();
return (-metrics.ascent) + metrics.descent;
//return (-metrics.top) + metrics.bottom;
}
/**
* Get the smallest rect that ecompasses the text to be drawn using paint.
* @param text
* @param paint
* @return
*/
public static Rect getPackedStringDimensions(String text, Paint paint) {
Rect size = new Rect();
paint.getTextBounds(text, ZERO, text.length(), size);
return size;
}
/**
* Like getPackedStringDimensions except adds extra space to accommodate all
* characters that can be drawn regardless of whether or not they exist in text.
* This ensures a more uniform appearance for things that have dynamic text.
* @param text
* @param paint
* @return
*/
public static Rect getStringDimensions(String text, Paint paint) {
Rect size = new Rect();
if(text == null || text.length() == ZERO) {
return null;
}
paint.getTextBounds(text, ZERO, text.length(), size);
size.bottom = size.top + (int) getFontHeight(paint);
return size;
}
/**
* Draws text vertically centered on the specified coordinates
* @param canvas
* @param paint
* @param text
* @param cx
* @param cy
*/
public static void drawTextVerticallyCentered(Canvas canvas, String text, float cx, float cy, Paint paint) {
Rect textBounds = new Rect();
paint.getTextBounds(text, 0, text.length(), textBounds);
canvas.drawText(text, cx, cy - textBounds.exactCenterY(), paint);
}
}
@@ -0,0 +1,180 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.util;
import java.util.HashMap;
import java.util.List;
/**
* An implementation of {@link Layerable}. Provides fast element retrieval via hash key in addition to
* mutable ordering (z indexing) of elements.
*/
public class LayerHash<KeyType, ValueType> implements Layerable<KeyType> {
private HashMap<KeyType, ValueType> hash;
private LinkedLayerList<KeyType> zlist;
{
hash = new HashMap<>();
zlist = new LinkedLayerList<>();
}
public int size() {
return zlist.size();
}
public ValueType get(KeyType key) {
return hash.get(key);
}
public List<KeyType> getKeysAsList() {
return zlist;
}
/**
* If key already exists within the structure, it's value is replaced with the new value and
* it's existing order is maintained.
* @param key
* @param value
*/
public synchronized void addToTop(KeyType key, ValueType value) {
if(hash.containsKey(key)) {
hash.put(key, value);
} else {
hash.put(key, value);
zlist.addToTop(key);
}
}
/**
* If key already exists within the structure, it's value is replaced with the new value and
* it's existing order is maintained.
* @param key
* @param value
*/
public synchronized void addToBottom(KeyType key, ValueType value) {
if(hash.containsKey(key)) {
hash.put(key, value);
} else {
hash.put(key, value);
zlist.addToBottom(key);
}
}
public synchronized boolean moveToTop(KeyType element) {
if(!hash.containsKey(element)) {
return false;
} else {
return zlist.moveToTop(element);
}
}
public synchronized boolean moveAbove(KeyType objectToMove, KeyType reference) {
if(objectToMove == reference) {
throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B.");
}
if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) {
return false;
} else {
return zlist.moveAbove(objectToMove, reference);
}
}
public synchronized boolean moveBeneath(KeyType objectToMove, KeyType reference) {
if(objectToMove == reference) {
throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B.");
}
if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) {
return false;
} else {
return zlist.moveBeneath(objectToMove, reference);
}
}
public synchronized boolean moveToBottom(KeyType key) {
if(!hash.containsKey(key)) {
return false;
} else {
return zlist.moveToBottom(key);
}
}
public synchronized boolean moveUp(KeyType key) {
if (!hash.containsKey(key)) {
return false;
} else {
return zlist.moveUp(key);
}
}
public synchronized boolean moveDown(KeyType key) {
if (!hash.containsKey(key)) {
return false;
} else {
return zlist.moveDown(key);
}
}
@Override
public List<KeyType> elements() {
return zlist;
}
/**
*
* @return Ordered list of keys.
*/
public List<KeyType> keys() {
return elements();
}
public synchronized boolean remove(KeyType key) {
if(hash.containsKey(key)) {
hash.remove(key);
zlist.remove(key);
return true;
} else {
return false;
}
}
public ValueType getTop() {
return hash.get(zlist.getLast());
}
public ValueType getBottom() {
return hash.get(zlist.getFirst());
}
public ValueType getAbove(KeyType key) {
final int index = zlist.indexOf(key);
if(index >= 0 && index < size() - 1) {
return hash.get(zlist.get(index + 1));
}
return null;
}
public ValueType getBeneath(KeyType key) {
final int index = zlist.indexOf(key);
if(index > 0) {
return hash.get(zlist.get(index - 1));
}
return null;
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.util;
import java.util.List;
/**
* Utility class providing additional element organization operations.
* @param <ElementType>
*/
public class LayerListOrganizer<ElementType> implements Layerable<ElementType> {
private static final int ZERO = 0;
private static final int ONE = 1;
private List<ElementType> list;
public LayerListOrganizer(List<ElementType> list) {
this.list = list;
}
public boolean moveToTop(ElementType element) {
if(list.remove(element)) {
list.add(list.size(), element);
return true;
} else {
return false;
}
}
public boolean moveAbove(ElementType objectToMove, ElementType reference) {
if(objectToMove == reference) {
throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B.");
}
list.remove(objectToMove);
int refIndex = list.indexOf(reference);
list.add(refIndex + ONE, objectToMove);
return true;
}
public boolean moveBeneath(ElementType objectToMove, ElementType reference) {
if (objectToMove == reference) {
throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B.");
}
list.remove(objectToMove);
int refIndex = list.indexOf(reference);
list.add(refIndex, objectToMove);
return true;
}
public boolean moveToBottom(ElementType key) {
list.remove(key);
list.add(ZERO, key);
return true;
}
public boolean moveUp(ElementType key) {
int widgetIndex = list.indexOf(key);
if(widgetIndex == - ONE) {
// key not found:
return false;
}
if(widgetIndex >= list.size() - ONE) {
// already at the top:
return true;
}
ElementType widgetAbove = list.get(widgetIndex + ONE);
return moveAbove(key, widgetAbove);
}
public boolean moveDown(ElementType key) {
int widgetIndex = list.indexOf(key);
if(widgetIndex == - ONE) {
// key not found:
return false;
}
if(widgetIndex <= ZERO) {
// already at the bottom:
return true;
}
ElementType widgetBeneath = list.get(widgetIndex - ONE);
return moveBeneath(key, widgetBeneath);
}
@Override
public List<ElementType> elements() {
return list;
}
public void addToBottom(ElementType element) {
list.add(ZERO, element);
}
public void addToTop(ElementType element) {
list.add(list.size(), element);
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.util;
import java.util.List;
/**
* Encapsulates the concept of "layerable" objects; Each object is stored above or below each other object and may
* be moved up and down in the queue relative to other elements in the hash or absolutely to the front or back of the queue.
*
* Note that the method names correspond to the order of items drawn directly on top of one another using an iterator;
* the first element drawn (lowest layer) is effectively the "bottom" element.
* @param <ElementType>
*/
public interface Layerable<ElementType> {
/**
* Move above all other elements
* @param element
* @return
*/
boolean moveToTop(ElementType element);
/**
* Move above the specified element
* @param objectToMove
* @param reference
* @return
*/
boolean moveAbove(ElementType objectToMove, ElementType reference);
/**
* Move beneath the specified element
*
* @param objectToMove
* @param reference
* @return
*/
boolean moveBeneath(ElementType objectToMove, ElementType reference);
/**
* Move beneath all other elements
* @param key
* @return
*/
boolean moveToBottom(ElementType key);
/**
* Move up by one element
* @param key
* @return
*/
boolean moveUp(ElementType key);
/**
* Move down by one element
* @param key
* @return
*/
boolean moveDown(ElementType key);
List<ElementType> elements();
}
@@ -0,0 +1,75 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.util;
import java.util.LinkedList;
import java.util.List;
/**
* A implementation of {@link Layerable} backed by a {@link LinkedList}.
* @param <Type>
*/
public class LinkedLayerList<Type> extends LinkedList<Type> implements Layerable<Type> {
private LayerListOrganizer<Type> organizer = new LayerListOrganizer<>(this);
@Override
public boolean moveToTop(Type element) {
return organizer.moveToTop(element);
}
@Override
public boolean moveAbove(Type objectToMove, Type reference) {
return organizer.moveAbove(objectToMove, reference);
}
@Override
public boolean moveBeneath(Type objectToMove, Type reference) {
return organizer.moveBeneath(objectToMove, reference);
}
@Override
public boolean moveToBottom(Type key) {
return organizer.moveToBottom(key);
}
@Override
public boolean moveUp(Type key) {
return organizer.moveUp(key);
}
@Override
public boolean moveDown(Type key) {
return organizer.moveDown(key);
}
@Override
public List<Type> elements() {
return organizer.elements();
}
public void addToBottom(Type element) {
organizer.addToBottom(element);
}
public void addToTop(Type element) {
organizer.addToTop(element);
}
}
@@ -0,0 +1,31 @@
/*
* 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;
/**
* Essentially just a version of the Map interface used to associate
* a key of a given type with a value of a given type, does impose a 1:1
* relationship between keys and values and defines no method for insertion or deletion.
*/
public interface Mapping<Key, Value> {
/**
* @param value
* @return The Key associated with the specified value.
*/
Key get(Value value);
}
@@ -0,0 +1,147 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.util;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PixelUtils {
private static DisplayMetrics metrics;
/**
* Recalculates scale value etc. Should be called when an application starts or
* whenever the screen is rotated.
*/
public static void init(Context ctx) {
metrics = ctx.getResources().getDisplayMetrics();
}
public static PointF add(PointF lhs, PointF rhs) {
return new PointF(lhs.x + rhs.x, lhs.y + rhs.y);
}
public static PointF sub(PointF lhs, PointF rhs) {
return new PointF(lhs.x - rhs.x, lhs.y - rhs.y);
}
/**
* Converts a dp value to pixels.
* @param dp
* @return Pixel value of dp.
*/
public static float dpToPix(float dp) {
checkMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
}
/**
* Converts an sp value to pixels.
* @param sp
* @return Pixel value of sp.
*/
@SuppressWarnings("SameParameterValue")
public static float spToPix(float sp) {
checkMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
}
/**
*
* CODE BELOW IS ADAPTED IN PART FROM MINDRIOT'S SAMPLE CODE HERE:
* http://stackoverflow.com/questions/8343971/how-to-parse-a-dimension-string-and-convert-it-to-a-dimension-value
*/
// Initialize dimension string to constant lookup.
public static final Map<String, Integer> dimensionConstantLookup = initDimensionConstantLookup();
private static Map<String, Integer> initDimensionConstantLookup() {
Map<String, Integer> m = new HashMap<String, Integer>();
m.put("px", TypedValue.COMPLEX_UNIT_PX);
m.put("dip", TypedValue.COMPLEX_UNIT_DIP);
m.put("dp", TypedValue.COMPLEX_UNIT_DIP);
m.put("sp", TypedValue.COMPLEX_UNIT_SP);
m.put("pt", TypedValue.COMPLEX_UNIT_PT);
m.put("in", TypedValue.COMPLEX_UNIT_IN);
m.put("mm", TypedValue.COMPLEX_UNIT_MM);
return Collections.unmodifiableMap(m);
}
// Initialize pattern for dimension string.
protected static final String DIMENSION_REGEX = "^\\-?\\s*(\\d+(\\.\\d+)*)\\s*([a-zA-Z]+)\\s*$";
protected static final Pattern DIMENSION_PATTERN = Pattern.compile(DIMENSION_REGEX);
public static float stringToDimension(String dimension) {
// Mimics TypedValue.complexToDimension(int data, DisplayMetrics metrics).
InternalDimension internalDimension = stringToInternalDimension(dimension);
return TypedValue.applyDimension(internalDimension.unit, internalDimension.value, metrics);
}
private static InternalDimension stringToInternalDimension(String dimension) {
// Match target against pattern.
Matcher matcher = DIMENSION_PATTERN.matcher(dimension);
if (matcher.matches()) {
// Match found.
// Extract value.
float value = Float.valueOf(matcher.group(1));
// Extract dimension units.
String unit = matcher.group(3).toLowerCase();
// Get Android dimension constant.
Integer dimensionUnit = dimensionConstantLookup.get(unit);
if (dimensionUnit == null) {
// Invalid format.
throw new NumberFormatException();
} else {
// Return valid dimension.
return new InternalDimension(value, dimensionUnit);
}
} else {
// Invalid format.
throw new NumberFormatException();
}
}
private static class InternalDimension {
float value;
int unit;
public InternalDimension(float value, int unit) {
this.value = value;
this.unit = unit;
}
}
/**
* Safety run to hopefully help clarify what could otherwise be a confusing NPE.
*/
private static void checkMetrics() {
if(metrics == null) {
throw new RuntimeException("PixelUtils not initialized; call PixelUtils.init(Context) before using.");
}
}
}
@@ -0,0 +1,112 @@
/*
* 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.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import com.androidplot.Plot;
import com.androidplot.PlotListener;
/**
* !!! THIS CLASS IS STILL UNDER DEVELOPMENT AND MAY CONTAIN BUGS !!!
* Gathers performance statistics from a Plot. Instances of PlotStatistics
* should never be added to more than one Plot, otherwise the statiscs will
* be invalid.
*/
public class PlotStatistics implements PlotListener {
long updateDelayMs;
long longestRenderMs = 0;
long shortestRenderMs = 0;
long lastStart = 0;
long lastLatency = 0;
long lastAnnotation;
long latencySamples = 0;
long latencySum = 0;
String annotationString = "";
private boolean annotatePlotEnabled;
private Paint paint;
{
paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setColor(Color.WHITE);
paint.setTextSize(30);
resetCounters();
}
public PlotStatistics(long updateDelayMs, boolean annotatePlotEnabled) {
this.updateDelayMs = updateDelayMs;
this.annotatePlotEnabled = annotatePlotEnabled;
}
public void setAnnotatePlotEnabled(boolean enabled) {
this.annotatePlotEnabled = enabled;
}
private void resetCounters() {
longestRenderMs = 0;
shortestRenderMs = 999999999;
latencySamples = 0;
latencySum = 0;
}
private void annotatePlot(Plot source, Canvas canvas) {
long nowMs = System.currentTimeMillis();
// throttle the update frequency:
long msSinceUpdate = (nowMs - lastAnnotation);
if(msSinceUpdate >= updateDelayMs) {
float avgLatency = latencySamples > 0 ? latencySum/latencySamples : 0;
String overallFPS = String.format("%.2f", latencySamples > 0 ? (1000f/msSinceUpdate) * latencySamples : 0);
String potentialFPS = String.format("%.2f", latencySamples > 0 ? 1000f/avgLatency : 0);
annotationString = "FPS (potential): " + potentialFPS + " FPS (actual): " + overallFPS + " Latency (ms) Avg: " + lastLatency + " \nMin: " + shortestRenderMs +
" Max: " + longestRenderMs;
lastAnnotation = nowMs;
resetCounters();
}
RectF r = source.getDisplayDimensions().canvasRect;
if(annotatePlotEnabled) {
canvas.drawText(annotationString, r.centerX(), r.centerY(), paint);
}
}
@Override
public void onBeforeDraw(Plot source, Canvas canvas) {
lastStart = System.currentTimeMillis();
}
@Override
public void onAfterDraw(Plot source, Canvas canvas) {
lastLatency = System.currentTimeMillis() - lastStart;
if(lastLatency < shortestRenderMs) {
shortestRenderMs = lastLatency;
}
if(lastLatency > longestRenderMs) {
longestRenderMs = lastLatency;
}
latencySum += lastLatency;
latencySamples++;
annotatePlot(source, canvas);
}
public void setEnabled(boolean isEnabled) {
this.annotatePlotEnabled = isEnabled;
}
}
@@ -0,0 +1,82 @@
/*
* 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.RectF;
import com.androidplot.ui.*;
/**
* Convenience methods for dealing with {@link android.graphics.RectF}
*/
public abstract class RectFUtils {
/**
* Determine if two {@link RectF} instances are equal. Must be used in place
* of default equality operation due to a bug that exists in older versions of Android:
* http://stackoverflow.com/questions/13517852/rectf-equals-fails-on-android-versions-below-jelly-bean
* @param r1 May not be null
* @param r2 May not be null
* @return True if r1 and r2 are identical, false otherwise.
*/
public static boolean areIdentical(RectF r1, RectF r2) {
return r1.left == r2.left &&
r1.top == r2.top &&
r1.right == r2.right &&
r1.bottom == r2.bottom;
}
/**
* Calculates a new {@link RectF} by applying insets to rect.
* @param rect
* @param insets
* @return The {@link RectF} created as a result of applying insets, or the passed in
* instance, if the insets were null.
*/
public static RectF applyInsets(RectF rect, Insets insets) {
if (insets != null) {
return new RectF(
rect.left + insets.getLeft(),
rect.top + insets.getTop(),
rect.right - insets.getRight(),
rect.bottom - insets.getBottom());
} else {
return rect;
}
}
/**
* Generates a RectF from two height and two width values; the h and w values will
* be passed into the RectF constructor such that RectF.left <= RectF.right and
* RectF.top <= RectF.bottom.
* @param w1 width1
* @param h1 height1
* @param w2 width2
* @param h2 height2
* @return
*/
public static RectF createFromEdges(float w1, float h1, float w2, float h2) {
final boolean w1IsLeft = w1 <= w2;
final boolean h1IsTop = h1 <= h2;
return new RectF(
w1IsLeft ? w1 : w2,
h1IsTop ? h1 : h2,
w1IsLeft ? w2 : w1,
h1IsTop ? h2 : h1);
}
}
@@ -0,0 +1,142 @@
/*
* 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.util.Log;
import com.androidplot.Plot;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Utility class for invoking Plot.redraw() on a background thread
* at a set frequency.
*/
public class Redrawer implements Runnable {
private static final int ONE_SECOND_MS = 1000;
private static final String TAG = Redrawer.class.getName();
private List<WeakReference<Plot>> plots;
private long sleepTime;
// used to temporarily pause rendering without disposing of the run thread
private boolean keepRunning;
// when set to false, run thread will be allowed to exit the main run loop
private boolean keepAlive;
private Thread thread;
/**
*
* @param plots List of Plot instances to be redrawn
* @param maxRefreshRate Desired frequency at which to redraw plots.
* @param startImmediately If true, invokes run() immediately after construction.
*/
public Redrawer(List<Plot> plots, float maxRefreshRate, boolean startImmediately) {
this.plots = new ArrayList<>(plots.size());
for(Plot plot : plots) {
this.plots.add(new WeakReference<>(plot));
}
setMaxRefreshRate(maxRefreshRate);
thread = new Thread(this, "Androidplot Redrawer");
thread.start();
if(startImmediately) {
start();
}
}
public Redrawer(Plot plot, float maxRefreshRate, boolean startImmediately) {
this(Collections.singletonList(plot), maxRefreshRate, startImmediately);
}
/**
* Temporarily stop redrawing the plot.
*/
public synchronized void pause() {
keepRunning = false;
notify();
Log.d(TAG, "Redrawer paused.");
}
/**
* Start/resume redrawing the plot.
*/
public synchronized void start() {
keepRunning = true;
notify();
Log.d(TAG, "Redrawer started.");
}
/**
* Internally, this causes
* the refresh thread to exit. Should always be called
* before exiting the application.
*/
public synchronized void finish() {
keepRunning = false;
keepAlive = false;
notify();
}
@Override
public void run() {
keepAlive = true;
try {
while(keepAlive) {
if(keepRunning) {
// redraw plot(s) and sleep in an interruptible state for a
// max of sleepTime ms.
// TODO: record start and end timestamps and
// TODO: calculate sleepTime from that, in order to more accurately
// TODO: meet desired refresh rate.
for(WeakReference<Plot> plotRef : plots) {
plotRef.get().redraw();
}
synchronized (this) {
wait(sleepTime);
}
} else {
// sleep until notified
synchronized (this) {
wait();
}
}
}
} catch (InterruptedException ignored) {
} finally {
Log.d(TAG, "Redrawer thread exited.");
}
}
/**
* Set the maximum refresh rate that Redrawer should use. Actual
* refresh rate could be slower.
* @param refreshRate Refresh rate in Hz.
*/
public void setMaxRefreshRate(float refreshRate) {
sleepTime = (long)(ONE_SECOND_MS / refreshRate);
Log.d(TAG, "Set Redrawer refresh rate to " +
refreshRate + "( " + sleepTime + " ms)");
}
}
@@ -0,0 +1,271 @@
/*
* 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 com.androidplot.Region;
import com.androidplot.xy.FastXYSeries;
import com.androidplot.xy.OrderedXYSeries;
import com.androidplot.xy.RectRegion;
import com.androidplot.xy.XYConstraints;
import com.androidplot.xy.XYSeries;
import java.util.List;
/**
* Utilities for dealing with Series data.
*/
public class SeriesUtils {
public static RectRegion minMax(List<XYSeries> seriesList) {
return minMax(null, seriesList);
}
public static RectRegion minMax(XYSeries... seriesList) {
return minMax(null, seriesList);
}
public static Region minMaxX(XYSeries... seriesList) {
final Region bounds = new Region();
for (XYSeries series : seriesList) {
for (int i = 0; i < series.size(); i++) {
bounds.union(series.getX(i));
}
}
return bounds;
}
public static Region minMaxY(XYSeries... seriesList) {
final Region bounds = new Region();
for (XYSeries series : seriesList) {
for (int i = 0; i < series.size(); i++) {
bounds.union(series.getY(i));
}
}
return bounds;
}
/**
* @param constraints may be null.
* @param seriesList
* @return
* @since 0.9.7
*/
public static RectRegion minMax(XYConstraints constraints, List<XYSeries> seriesList) {
return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()]));
}
/**
* @param constraints May be null.
* @param seriesArray
* @return
* @since 0.9.7
*/
public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArray) {
final RectRegion bounds = new RectRegion();
// make sure there is series data to iterate over:
if (seriesArray != null && seriesArray.length > 0) {
// iterate over each series
for (XYSeries series : seriesArray) {
// if this is an advanced xy series then minMax have already been calculated for us:
if (series instanceof FastXYSeries) {
final RectRegion b = ((FastXYSeries) series).minMax();
if(b == null) {
//this series doesn't currently have min or max region (might be empty)
continue;
}
if(constraints == null || constraints.contains(b)) {
bounds.union(b);
continue;
}
}
for (int i = 0; i < series.size(); i++) {
final Number xi = series.getX(i);
final Number yi = series.getY(i);
// if constraints have been set, make sure this xy coordinate exists within them:
if (constraints == null || constraints.contains(xi, yi)) {
bounds.union(xi, yi);
}
}
}
}
return bounds;
}
/**
*
* @param bounds Starting minMax values to work from; only lists values that are greater than or less
* than those in bounds will be be used.
* @param lists lists to be evaluated for min/max values.
* @return the original bounds instance passed in
*/
public static Region minMax(Region bounds, List<Number>... lists) {
for (final List<Number> list : lists) {
for (final Number i : list) {
bounds.union(i);
}
}
return bounds;
}
/**
* Compute the range of visible i-vals in the specified series. Assumes that x-vals are
* in strict ascending order; behavior is undefined otherwise.
* @param series
* @param visibleBounds The visible constraints of the plot
* @return
*/
public static Region iBounds(XYSeries series, RectRegion visibleBounds) {
final float step = series.size() >= 200 ? 50 : 1;
final int iBoundsMin = iBoundsMin(series, visibleBounds.getMinX().doubleValue(), step);
final int iBoundsMax = iBoundsMax(series, visibleBounds.getMaxX().doubleValue(), step);
return new Region(iBoundsMin, iBoundsMax);
}
/**
* TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing
* TODO a binary search algorithm that also supports nulls is not trivial and would not likely
* TODO result in any noticeable performance increase here. It's a task for another day!
* @param series
* @param visibleMax
* @param step
* @return The index of the smallest non-null value that is greater than visibleMax, or the index
* of the last element if no such value exists.
*/
protected static int iBoundsMax(XYSeries series, double visibleMax, float step) {
int max = series.size() - 1;
final int seriesSize = series.size();
final int steps = (int) Math.ceil(seriesSize / step);
for (int stepIndex = steps; stepIndex >= 0; stepIndex--) {
final int i = stepIndex * (int) step;
for (int ii = 0; ii < step; ii++) {
final int iii = i + ii;
if(iii < seriesSize) {
final Number thisX = series.getX(iii);
if (thisX != null) {
final double thisDouble = thisX.doubleValue();
if (thisDouble > visibleMax) {
// this is the smallest non-null value in this block, so skip
// to the next block:
max = iii;
break;
} else if (thisDouble == visibleMax) {
return iii;
} else {
return max;
}
}
}
}
}
return max;
}
/**
* TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing
* TODO a binary search algorithm that also supports nulls is not trivial and would not likely
* TODO result in any noticeable performance increase here. It's a task for another day!
* @param series
* @param visibleMin
* @param step
* @return The index of the largest non-null value that is less than visible, or 0
* (the first element index) if no such value exists.
*/
protected static int iBoundsMin(XYSeries series, double visibleMin, float step) {
int min = 0;
final int steps = (int) Math.ceil(series.size() / step);
for (int stepIndex = 1; stepIndex <= steps; stepIndex++) {
final int i = stepIndex * (int) step;
for (int ii = 1; ii <= step; ii++) {
final int iii = i - ii;
if(iii < 0) {
break;
}
if(iii < series.size()) {
final Number thisX = series.getX(iii);
if (thisX != null) {
if (thisX.doubleValue() < visibleMin) {
// this is the largest non-null value in this block, so skip
// to the next block:
min = iii;
break;
} else if (thisX.doubleValue() == visibleMin) {
return iii;
} else {
return min;
}
}
}
}
}
return min;
}
/**
* Determine the minMax iVals of the xVals surrounding a range of one or more null values.
* @param series
* @param index index of the null value in question
* @return The iVals of the non-null values surrounding the null range. If the null range is unbounded on
* either side then either or both min and max values will also be null.
*/
protected static Region getNullRegion(XYSeries series, int index) {
Region region = new Region();
if(series.getX(index) != null) {
throw new IllegalArgumentException("Attempt to find null region for non null index: " + index);
}
for(int i = index - 1; i >= 0; i--) {
Number val = series.getX(i);
if(val != null) {
region.setMin(i);
break;
}
}
for(int i = index + 1; i < series.size(); i++) {
Number val = series.getX(i);
if(val != null) {
region.setMax(i);
break;
}
}
return region;
}
/**
* @param lists
* @return
* @since 0.9.7
*/
public static Region minMax(List<Number>... lists) {
return minMax(new Region(), lists);
}
/**
* Determine the XVal order of an XYSeries. If series does not implement {@link OrderedXYSeries}
* then {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE} is assumed.
* @param series
* @return The {@link com.androidplot.xy.OrderedXYSeries.XOrder} of the series.
*/
public static OrderedXYSeries.XOrder getXYOrder(XYSeries series) {
return series instanceof OrderedXYSeries ?
((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE;
}
}
@@ -0,0 +1,130 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.content.*;
import android.graphics.*;
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesRenderer;
/**
* This is an experimental (but stable) implementation of an {@link XYSeriesRenderer} that provides instrumentation
* allowing advanced behaviors like dynamically coloring / styling individual segments of a series, etc. This class
* may be removed or renamed in future releases.
* Currently has the following constraints:
* - Interpolation is not supported
* - Only draws lines; no points or fill
* - Draws series lines using simple Canvas.drawLine(...) invocations.
* @since 0.9.9
*/
public class AdvancedLineAndPointRenderer extends XYSeriesRenderer<XYSeries, AdvancedLineAndPointRenderer.Formatter> {
private int latestIndex;
public AdvancedLineAndPointRenderer(XYPlot plot) {
super(plot);
}
@Override
protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) {
PointF thisPoint;
PointF lastPoint = null;
for (int i = 0; i < series.size(); i++) {
Number y = series.getY(i);
Number x = series.getX(i);
if (y != null && x != null) {
thisPoint = getPlot().getBounds()
.transformScreen(x, y, plotArea);
} else {
thisPoint = null;
}
// don't need to do any of this if the line isnt going to be drawn:
if(formatter.getLinePaint() != null) {
if (thisPoint != null && lastPoint != null) {
canvas.drawLine(lastPoint.x, lastPoint.y, thisPoint.x, thisPoint.y, formatter.getLinePaint(i, latestIndex, series.size()));
}
}
lastPoint = thisPoint;
}
}
@Override
protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
if(formatter.getLinePaint() != null) {
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
}
}
public void setLatestIndex(int latestIndex) {
this.latestIndex = latestIndex;
}
/**
* Formatter designed to work in tandem with {@link AdvancedLineAndPointRenderer}.
* @since 0.9.9
*/
public static class Formatter extends XYSeriesFormatter<XYRegionFormatter> {
private static final int DEFAULT_STROKE_WIDTH = 3;
private Paint linePaint;
public Formatter() {
linePaint = new Paint();
linePaint.setStrokeWidth(DEFAULT_STROKE_WIDTH);
linePaint.setColor(Color.RED);
}
public Formatter(Context context, int xmlConfigId) {
this();
configure(context, xmlConfigId);
}
@Override
public Class<? extends SeriesRenderer> getRendererClass() {
return AdvancedLineAndPointRenderer.class;
}
@Override
public AdvancedLineAndPointRenderer doGetRendererInstance(XYPlot plot) {
return new AdvancedLineAndPointRenderer(plot);
}
public Paint getLinePaint() {
return linePaint;
}
/**
* By default, simply returns the line paint as-is. May be overridden to provide custom behavior based
* on input params.
* @param thisIndex
* @param latestIndex
* @param seriesSize
* @return
*/
public Paint getLinePaint(int thisIndex, int latestIndex, int seriesSize) {
return getLinePaint();
}
public void setLinePaint(Paint linePaint) {
this.linePaint = linePaint;
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
public enum Axis {
DOMAIN,
RANGE
}
@@ -0,0 +1,112 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.content.*;
import android.graphics.Paint;
import com.androidplot.ui.SeriesRenderer;
public class BarFormatter extends LineAndPointFormatter {
public Paint getFillPaint() {
return fillPaint;
}
public void setFillPaint(Paint fillPaint) {
this.fillPaint = fillPaint;
}
public Paint getBorderPaint() {
return borderPaint;
}
public void setBorderPaint(Paint borderPaint) {
this.borderPaint = borderPaint;
}
private Paint fillPaint;
private Paint borderPaint;
private float marginTop;
private float marginBottom;
private float marginLeft;
private float marginRight;
/**
* Should only be used in conjunction with calls to configure()...
*/
public BarFormatter() {
fillPaint = new Paint();
fillPaint.setStyle(Paint.Style.FILL);
fillPaint.setAlpha(100);
borderPaint = new Paint();
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setAlpha(100);
}
public BarFormatter(int fillColor, int borderColor) {
this();
fillPaint.setColor(fillColor);
borderPaint.setColor(borderColor);
}
public BarFormatter(Context context, int xmlCfgId) {
this();
configure(context, xmlCfgId);
}
@Override
public Class<? extends SeriesRenderer> getRendererClass() {
return BarRenderer.class;
}
@Override
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
return new BarRenderer(plot);
}
public float getMarginTop() {
return marginTop;
}
public void setMarginTop(float marginTop) {
this.marginTop = marginTop;
}
public float getMarginBottom() {
return marginBottom;
}
public void setMarginBottom(float marginBottom) {
this.marginBottom = marginBottom;
}
public float getMarginLeft() {
return marginLeft;
}
public void setMarginLeft(float marginLeft) {
this.marginLeft = marginLeft;
}
public float getMarginRight() {
return marginRight;
}
public void setMarginRight(float marginRight) {
this.marginRight = marginRight;
}
}
@@ -0,0 +1,385 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.graphics.Canvas;
import android.graphics.RectF;
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesBundle;
import com.androidplot.util.PixelUtils;
import com.androidplot.util.RectFUtils;
/**
* Renders the points in an XYSeries as bars. See {@link BarOrientation} javadoc for details on supported
* presentation styles.
*
*/
public class BarRenderer<FormatterType extends BarFormatter> extends GroupRenderer<FormatterType> {
private BarOrientation barOrientation = BarOrientation.OVERLAID; // default Render Style
private BarGroupWidthMode barGroupWidthMode = BarGroupWidthMode.FIXED_WIDTH; // default Width Style
/**
* Represents the size in pixels of either bar width or bar gap width, depending on the current
* value of barWidthMode.
*/
private float width = PixelUtils.dpToPix(3);
/**
* How bars should be laid out when in a group of 2 or more series.
*/
public enum BarOrientation {
/**
* Bars are drawn overlapping one another, in the order their respective series
* was added to the plot.
*/
IN_ORDER,
/**
* Bars are drawn overlapping one another, with taller bars being drawn behind
* the shorter ones.
*/
OVERLAID, // bars are overlaid in descending y-val order (largest val in back)
/**
* Bars are stacked on top of one another so that the sum of their yVals produces the final
* height of that bar.
*/
STACKED, // bars are drawn stacked vertically on top of each other
/**
* Bars are drawn next to one another, grouped by iVal
*/
SIDE_BY_SIDE // bars are drawn horizontally next to each-other
}
/**
* Mode with which to calculate the width of each bar.
*/
public enum BarGroupWidthMode {
FIXED_WIDTH, // bar width is always barWidth
FIXED_GAP // bar width is calculated relative to a fixed gap width between each bar
}
public BarRenderer(XYPlot plot) {
super(plot);
}
public void setBarOrientation(BarOrientation renderBarOrientation) {
this.barOrientation = renderBarOrientation;
}
public BarOrientation getBarOrientation() {
return this.barOrientation;
}
public BarGroupWidthMode getBarGroupWidthMode() {
return this.barGroupWidthMode;
}
public float getBarGroupWidth() {
return this.width;
}
public void setBarGroupWidth(BarGroupWidthMode mode, float width) {
this.barGroupWidthMode = mode;
this.width = width;
}
protected BarComparator getBarComparator(float rangeOriginPx) {
return new BarComparator(getBarOrientation(), rangeOriginPx);
}
@Override
public void doDrawLegendIcon(Canvas canvas, RectF rect, BarFormatter formatter) {
if (formatter.hasFillPaint()) {
canvas.drawRect(rect, formatter.getFillPaint());
}
canvas.drawRect(rect, formatter.getBorderPaint());
}
/**
* Retrieves the BarFormatter instance that corresponds with the series passed in.
* Can be overridden to return other BarFormatters as a result of touch events etc.
* @param index index of the point being rendered.
* @param series XYSeries to which the point being rendered belongs.
* @return The desired getFormatter or null to use the default.
*/
@SuppressWarnings("UnusedParameters")
public FormatterType getFormatter(int index, XYSeries series) {
return null;
}
@Override
public void onRender(Canvas canvas, RectF plotArea, List<SeriesBundle<XYSeries,
? extends FormatterType>> sfList, int seriesSize, RenderStack stack) {
List<BarGroup> barGroups = new ArrayList<>();
/*
* Build the axisMap (yVal,BarGroup)... a TreeMap of BarGroups
* BarGroups represent a point on the X axis where a single or group of bars need to be drawn.
*/
for(int i = 0; i < seriesSize; i++) {
final BarGroup group = new BarGroup(i, 0, plotArea);
int seriesOrder = 0;
for(SeriesBundle<XYSeries, ? extends FormatterType> bundle : sfList) {
// TODO: is this null check really necessary?
if(bundle.getSeries().getX(i) != null) {
Bar bar = new Bar(getPlot(), bundle.getSeries(),
bundle.getFormatter(), seriesOrder, i, plotArea);
group.addBar(bar);
group.centerPix = bar.xPix;
}
seriesOrder++;
}
barGroups.add(group);
}
/*
* Calculate the dimensions of each barGroup and then draw each bar within it according to
* the Render Style and Width Style.
*/
final int groupCount = barGroups.size();
for(BarGroup barGroup : barGroups) {
// Determine the exact left and right X for the Bar Group
switch (barGroupWidthMode) {
case FIXED_WIDTH:
barGroup.leftPix = barGroup.centerPix - (width / 2);
barGroup.rightPix = barGroup.leftPix + width;
break;
case FIXED_GAP:
float barWidth = plotArea.width();
if(groupCount > 1) {
barWidth = (barGroups.get(1).centerPix - barGroups.get(0).centerPix) - width;
}
final float halfWidth = barWidth / 2;
barGroup.leftPix = barGroup.centerPix - halfWidth;
barGroup.rightPix = barGroup.centerPix + halfWidth;
break;
default:
break;
}
/*
* Draw the bars within the barGroup area.
*/
double rangeOrigin = getPlot().getRangeOrigin().doubleValue();
float rangeOriginPx = (float) getPlot().getBounds().yRegion
.transform(rangeOrigin, plotArea.top, plotArea.bottom, true);
final BarComparator comparator = getBarComparator(rangeOriginPx);
switch (barOrientation) {
case IN_ORDER:
case OVERLAID:
Collections.sort(barGroup.bars, comparator);
for (Bar bar : barGroup.bars) {
drawBar(canvas, bar, createBarRect(
bar.barGroup.leftPix,
bar.yPix,
bar.barGroup.rightPix,
rangeOriginPx, bar.formatter));
}
break;
case SIDE_BY_SIDE:
final float width = barGroup.getWidth() / barGroup.bars.size();
float leftX = barGroup.leftPix;
Collections.sort(barGroup.bars, comparator);
for (Bar bar : barGroup.bars) {
drawBar(canvas, bar, createBarRect(
leftX, bar.yPix,
leftX + width, rangeOriginPx,
bar.formatter));
leftX = leftX + width;
}
break;
case STACKED:
float bottom = (int) barGroup.plotArea.bottom;
Collections.sort(barGroup.bars, comparator);
for (Bar bar : barGroup.bars) {
// TODO: handling sub range-origin values for the purpose of labeling
final float height = (int) bar.barGroup.plotArea.bottom - bar.yPix;
final float top = bottom - height;
drawBar(canvas, bar, createBarRect(
bar.barGroup.leftPix, top,
bar.barGroup.rightPix, bottom,
bar.formatter));
bottom = top;
}
break;
default:
throw new UnsupportedOperationException("Unexpected BarOrientation: " + barOrientation);
}
}
}
protected RectF createBarRect(float w1, float h1, float w2, float h2, BarFormatter formatter) {
final RectF result = RectFUtils.createFromEdges(w1, h1,w2, h2);
result.left += formatter.getMarginLeft();
result.right -= formatter.getMarginRight();
result.top += formatter.getMarginTop();
result.bottom -= formatter.getMarginBottom();
return result;
}
protected void drawBar(Canvas canvas, Bar<FormatterType> bar, RectF rect) {
// null yVals are skipped:
if(bar.getY() == null) {
return;
}
BarFormatter formatter = getFormatter(bar.i, bar.series);
if(formatter == null) {
formatter = bar.formatter;
}
if(rect.height() > 0 && rect.width() > 0) {
if (formatter.hasFillPaint()) {
canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom,
formatter.getFillPaint());
}
if (formatter.hasLinePaint()) {
canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom,
formatter.getBorderPaint());
}
}
PointLabelFormatter plf =
formatter.hasPointLabelFormatter() ? formatter.getPointLabelFormatter() : null;
PointLabeler pointLabeler =
formatter != null ? formatter.getPointLabeler() : null;
if (plf != null && plf.hasTextPaint() && pointLabeler != null) {
canvas.drawText(pointLabeler.getLabel(bar.series, bar.i),
rect.centerX() + plf.hOffset, bar.yPix + plf.vOffset,
plf.getTextPaint());
}
}
/**
*
* @param <FormatterType>
*/
public static class Bar<FormatterType extends BarFormatter> {
public final XYSeries series;
public final FormatterType formatter;
public final int i;
public final int seriesOrder;
public final float xPix;
public final float yPix;
protected BarGroup barGroup;
// TODO: factor out plot param
public Bar(XYPlot plot, XYSeries series, FormatterType formatter, int seriesOrder, int i, RectF plotArea) {
this.series = series;
this.formatter = formatter;
this.i = i;
this.seriesOrder = seriesOrder;
final double xVal = series.getX(i).doubleValue();
xPix = (float) plot.getBounds().getxRegion()
.transform(xVal, plotArea.left, plotArea.right, false);
if (series.getY(i) != null) {
final double yVal = series.getY(i).doubleValue();
this.yPix = (float) plot.getBounds().yRegion
.transform(yVal, plotArea.top, plotArea.bottom, true);
} else {
this.yPix = 0;
}
}
public Number getY() {
return series.getY(i);
}
}
/**
* A collection of one or more {@Bar} instances sharing a common iVal.
*/
private static class BarGroup {
public ArrayList<Bar> bars;
public int i;
public float centerPix;
public float leftPix;
public float rightPix;
public RectF plotArea;
public BarGroup(int i, float centerPix, RectF plotArea) {
// Setup the TreeMap with the required comparator
this.bars = new ArrayList<>(); // create a comparator that compares series title given the index.
this.centerPix = centerPix;
this.plotArea = plotArea;
this.i = i;
}
public void addBar(Bar bar) {
bar.barGroup = this;
this.bars.add(bar);
}
protected float getWidth() {
return rightPix - leftPix;
}
}
/**
* Used to determine the order in which bars of the same group will be drawn.
*/
@SuppressWarnings("WeakerAccess")
public static class BarComparator implements Comparator<Bar> {
private final BarOrientation barOrientation;
private final float rangeOriginPx;
public BarComparator(BarOrientation barOrientation, float rangeOriginPx) {
this.rangeOriginPx = rangeOriginPx;
this.barOrientation = barOrientation;
}
@Override
public int compare(Bar bar1, Bar bar2) {
switch (barOrientation) {
case OVERLAID:
if(bar1.yPix > rangeOriginPx && bar2.yPix > rangeOriginPx) {
return Float.valueOf(bar2.yPix).compareTo(bar1.yPix);
} else {
return Float.valueOf(bar1.yPix).compareTo(bar2.yPix);
}
case IN_ORDER:
case SIDE_BY_SIDE:
case STACKED:
default:
return Integer.valueOf(bar1.seriesOrder).compareTo(bar2.seriesOrder);
}
}
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
public enum BoundaryMode {
FIXED,
AUTO,
GROW,
SHRINK
}
@@ -0,0 +1,97 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Paint;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.util.PixelUtils;
/**
* Format for drawing a value using {@link BubbleRenderer}.
* @since 1.2.2
*/
public class BubbleFormatter extends XYSeriesFormatter<XYRegionFormatter> {
private static final float DEFAULT_STROKE_PIX = 1;
private static final int DEFAULT_STROKE_COLOR = Color.BLACK;
private static final int DEFAULT_FILL_COLOR = Color.YELLOW;
private Paint strokePaint;
private Paint fillPaint;
{
strokePaint = new Paint();
strokePaint.setAntiAlias(true);
strokePaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_STROKE_PIX));
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setColor(DEFAULT_STROKE_COLOR);
fillPaint = new Paint();
fillPaint.setAntiAlias(true);
fillPaint.setColor(DEFAULT_FILL_COLOR);
// default point labeler should draw z for bubbles:
setPointLabeler(new PointLabeler<BubbleSeries>() {
@Override
public String getLabel(BubbleSeries series, int index) {
return String.valueOf(series.getZ(index));
}
});
}
public BubbleFormatter() {}
public BubbleFormatter(Context context, int xmlCfgId) {
this();
configure(context, xmlCfgId);
}
public BubbleFormatter(int fillColor, int strokeColor) {
fillPaint.setColor(fillColor);
strokePaint.setColor(strokeColor);
}
@Override
public Class<? extends SeriesRenderer> getRendererClass() {
return BubbleRenderer.class;
}
@Override
public BubbleRenderer doGetRendererInstance(XYPlot plot) {
return new BubbleRenderer(plot);
}
public Paint getStrokePaint() {
return strokePaint;
}
public void setStrokePaint(Paint strokePaint) {
this.strokePaint = strokePaint;
}
public Paint getFillPaint() {
return fillPaint;
}
public void setFillPaint(Paint fillPaint) {
this.fillPaint = fillPaint;
}
}
@@ -0,0 +1,151 @@
package com.androidplot.xy;
import android.graphics.*;
import com.androidplot.Region;
import com.androidplot.ui.*;
import com.androidplot.util.*;
/**
* Renders three dimensional data onto an {@link XYPlot} as bubbles; the x/y values define the position
* of the bubble and z is uses as a scaling value for the bubble's radius.
* @since 1.2.2
*/
public class BubbleRenderer<FormatterType extends BubbleFormatter> extends XYSeriesRenderer<BubbleSeries, FormatterType> {
protected static final float MIN_BUBBLE_RADIUS_DEFAULT_DP = 9;
protected static final float MAX_BUBBLE_RADIUS_DEFAULT_DP = 25;
private Region bubbleBounds;
private BubbleScaleMode bubbleScaleMode = BubbleScaleMode.SQUARE_ROOT;
public enum BubbleScaleMode {
/**
* Bubble radius is scaled directly by {@link BubbleSeries} z-vals
*/
LINEAR,
/**
* Bubble radius is scaled by the square root of {@link BubbleSeries} z-vals.
* This is the default scaling used.
*/
SQUARE_ROOT
}
public BubbleRenderer(XYPlot plot) {
super(plot);
bubbleBounds = new Region(
PixelUtils.dpToPix(MIN_BUBBLE_RADIUS_DEFAULT_DP),
PixelUtils.dpToPix(MAX_BUBBLE_RADIUS_DEFAULT_DP));
}
@Override
protected void onRender(Canvas canvas, RectF plotArea, BubbleSeries series,
FormatterType formatter, RenderStack stack) {
Region magnitudeBounds = calculateBounds();
for(int i = 0; i < series.size(); i++) {
// only render non-null values greater than zero:
if(series.getY(i) != null && series.getZ(i).doubleValue() > 0) {
final PointF centerPoint = getPlot().getBounds().
transform(series.getX(i), series.getY(i), plotArea, false, true);
// calculate bubble radius:
float bubbleRadius = magnitudeBounds.
transform(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT ?
Math.sqrt(series.getZ(i).doubleValue()) :
series.getZ(i).doubleValue(), bubbleBounds).floatValue();
drawBubble(canvas, formatter, series, i, centerPoint, bubbleRadius);
}
}
}
/**
* Render a bubble onto the canvas
* @param canvas
* @param formatter
* @param series
* @param index
* @param centerPoint the x/y coords of the center of the bubble
* @param radius size of the bubble
*/
protected void drawBubble(Canvas canvas, FormatterType formatter, BubbleSeries series,
int index, PointF centerPoint, float radius) {
canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getFillPaint());
canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getStrokePaint());
if(series != null && formatter.hasPointLabelFormatter() && formatter.getPointLabeler() != null) {
FontUtils.drawTextVerticallyCentered(
canvas,
formatter.getPointLabeler().getLabel(series, index),
centerPoint.x,
centerPoint.y,
formatter.getPointLabelFormatter().getTextPaint());
}
}
@Override
protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) {
drawBubble(canvas, formatter, null, 0,
new PointF(rect.centerX(), rect.centerY()), (rect.width()/2.5f));
}
public float getMinBubbleRadius() {
return bubbleBounds.getMin().floatValue();
}
public void setMinBubbleRadius(float minBubbleRadius) {
bubbleBounds.setMin(minBubbleRadius);
}
public float getMaxBubbleRadius() {
return bubbleBounds.getMax().floatValue();
}
public void setMaxBubbleRadius(float maxBubbleRadius) {
bubbleBounds.setMax(maxBubbleRadius);
}
public BubbleScaleMode getBubbleScaleMode() {
return bubbleScaleMode;
}
public void setBubbleScaleMode(BubbleScaleMode bubbleScaleMode) {
this.bubbleScaleMode = bubbleScaleMode;
}
protected Region calculateBounds() {
Region bounds = new Region();
for(SeriesBundle<BubbleSeries, ? extends FormatterType> f : getSeriesAndFormatterList()) {
SeriesUtils.minMax(bounds, f.getSeries().getZVals());
}
if(bounds.getMax() != null && bounds.getMax().doubleValue() > 0) {
if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) {
// scale for easier visual interpretation. see:
// https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly
bounds.setMax(Math.sqrt(bounds.getMax().doubleValue()));
}
} else {
// no non-null, greater than zero vals so bounds are undefined
return null;
}
if(bounds.getMin().doubleValue() > 0) {
if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) {
// scale for easier visual interpretation. see:
// https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly
bounds.setMin(Math.sqrt(bounds.getMin().doubleValue()));
}
} else {
// if the smallest value is negative, use zero instead since those vals arent visible:
bounds.setMax(0);
}
return bounds;
}
}
@@ -0,0 +1,81 @@
package com.androidplot.xy;
import java.util.*;
/**
* Created by halfhp on 9/17/16.
*/
public class BubbleSeries implements XYSeries {
private List<Number> xVals;
private List<Number> yVals;
private List<Number> zVals;
private String title;
/**
*
* @param interleavedValues Interleaved values ordered as x, y, z; total size must be a multiple of 3.
*/
public BubbleSeries(Number... interleavedValues) {
if(interleavedValues == null || interleavedValues.length % 3 > 0) {
throw new RuntimeException("BubbleSeries interleave array length must be a non-zero multiple of 3.");
}
xVals = new ArrayList<>();
yVals = new ArrayList<>();
zVals = new ArrayList<>();
for(int i = 0; i < interleavedValues.length; i+=3) {
xVals.add(interleavedValues[i]);
yVals.add(interleavedValues[i+1]);
zVals.add(interleavedValues[i+2]);
}
}
public BubbleSeries(List<Number> yVals, List<Number> zVals, String title) {
this.yVals = yVals;
this.zVals = zVals;
this.title = title;
// populate x with iVals:
this.xVals = new ArrayList<>(zVals.size());
for(int i = 0; i < zVals.size(); i++) {
this.xVals.add(i);
}
}
public BubbleSeries(List<Number> xVals, List<Number> yVals, List<Number> zVals, String title) {
this.xVals = xVals;
this.yVals = yVals;
this.zVals = zVals;
this.title = title;
}
@Override
public String getTitle() {
return title;
}
@Override
public int size() {
return xVals.size();
}
@Override
public Number getX(int index) {
return xVals.get(index);
}
@Override
public Number getY(int index) {
return yVals.get(index);
}
public Number getZ(int index) {
return zVals.get(index);
}
public List<Number> getZVals() {
return zVals;
}
}
@@ -0,0 +1,205 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.content.*;
import android.graphics.Color;
import android.graphics.Paint;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.util.PixelUtils;
/**
* Format for drawing a value using {@link CandlestickRenderer}.
* @since 0.9.7
*/
public class CandlestickFormatter extends XYSeriesFormatter<XYRegionFormatter> {
private static final float DEFAULT_WIDTH_PIX = PixelUtils.dpToPix(10);
private static final float DEFAULT_STROKE_PIX = PixelUtils.dpToPix(4);
private Paint wickPaint;
private Paint risingBodyFillPaint;
private Paint fallingBodyFillPaint;
private Paint risingBodyStrokePaint;
private Paint fallingBodyStrokePaint;
private Paint upperCapPaint;
private Paint lowerCapPaint;
private float bodyWidth = DEFAULT_WIDTH_PIX;
private float upperCapWidth = DEFAULT_WIDTH_PIX;
private float lowerCapWidth = DEFAULT_WIDTH_PIX;
private BodyStyle bodyStyle;
public enum BodyStyle {
SQUARE,
TRIANGULAR
}
protected static Paint getDefaultFillPaint(int color) {
Paint p = new Paint();
p.setStyle(Paint.Style.FILL);
p.setColor(color);
return p;
}
protected static Paint getDefaultStrokePaint(int color) {
Paint p = new Paint();
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(DEFAULT_STROKE_PIX);
p.setColor(color);
p.setAntiAlias(true);
return p;
}
public CandlestickFormatter(Context context, int xmlCfgId) {
this();
configure(context, xmlCfgId);
}
public CandlestickFormatter() {
this(getDefaultStrokePaint(Color.YELLOW),
getDefaultFillPaint(Color.GREEN),
getDefaultFillPaint(Color.RED),
getDefaultStrokePaint(Color.GREEN),
getDefaultStrokePaint(Color.RED),
getDefaultStrokePaint(Color.YELLOW),
getDefaultStrokePaint(Color.YELLOW),
BodyStyle.SQUARE);
}
public CandlestickFormatter(Paint wickPaint, Paint risingBodyFillPaint, Paint fallingBodyFillPaint,
Paint risingBodyStrokePaint, Paint fallingBodyStrokePaint,
Paint upperCapPaint, Paint lowerCapPaint, BodyStyle bodyStyle) {
setWickPaint(wickPaint);
setRisingBodyFillPaint(risingBodyFillPaint);
setFallingBodyFillPaint(fallingBodyFillPaint);
setRisingBodyStrokePaint(risingBodyStrokePaint);
setFallingBodyStrokePaint(fallingBodyStrokePaint);
setUpperCapPaint(upperCapPaint);
setLowerCapPaint(lowerCapPaint);
setBodyStyle(bodyStyle);
}
@Override
public Class<? extends SeriesRenderer> getRendererClass() {
return CandlestickRenderer.class;
}
@Override
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
return new CandlestickRenderer(plot);
}
public Paint getWickPaint() {
return wickPaint;
}
public void setWickPaint(Paint wickPaint) {
this.wickPaint = wickPaint;
}
public Paint getRisingBodyFillPaint() {
return risingBodyFillPaint;
}
public void setRisingBodyFillPaint(Paint risingBodyFillPaint) {
this.risingBodyFillPaint = risingBodyFillPaint;
}
public Paint getRisingBodyStrokePaint() {
return risingBodyStrokePaint;
}
public void setRisingBodyStrokePaint(Paint risingBodyStrokePaint) {
this.risingBodyStrokePaint = risingBodyStrokePaint;
}
public Paint getUpperCapPaint() {
return upperCapPaint;
}
public void setUpperCapPaint(Paint upperCapPaint) {
this.upperCapPaint = upperCapPaint;
}
public Paint getLowerCapPaint() {
return lowerCapPaint;
}
public void setLowerCapPaint(Paint lowerCapPaint) {
this.lowerCapPaint = lowerCapPaint;
}
public float getBodyWidth() {
return bodyWidth;
}
public void setBodyWidth(float bodyWidth) {
this.bodyWidth = bodyWidth;
}
public float getLowerCapWidth() {
return lowerCapWidth;
}
public void setLowerCapWidth(float lowerCapWidth) {
this.lowerCapWidth = lowerCapWidth;
}
public float getUpperCapWidth() {
return upperCapWidth;
}
public void setUpperCapWidth(float upperCapWidth) {
this.upperCapWidth = upperCapWidth;
}
public Paint getFallingBodyFillPaint() {
return fallingBodyFillPaint;
}
public void setFallingBodyFillPaint(Paint fallingBodyFillPaint) {
this.fallingBodyFillPaint = fallingBodyFillPaint;
}
public Paint getFallingBodyStrokePaint() {
return fallingBodyStrokePaint;
}
public void setFallingBodyStrokePaint(Paint fallingBodyStrokePaint) {
this.fallingBodyStrokePaint = fallingBodyStrokePaint;
}
public BodyStyle getBodyStyle() {
return bodyStyle;
}
public void setBodyStyle(BodyStyle bodyStyle) {
this.bodyStyle = bodyStyle;
}
/**
* Convenience method to set caps and wick to a single color in one call.
* @param paint
*/
public void setCapAndWickPaint(Paint paint) {
setUpperCapPaint(paint);
setLowerCapPaint(paint);
setWickPaint(paint);
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
/**
* Helper utility to simplify the creation of of candlestick charts
* @since 0.9.7
*/
public abstract class CandlestickMaker {
/**
* Adds a candlestick chart to the specified plot using the specified
* high, low, open and close values.
* @param plot
* @param formatter
* @param openVals
* @param closeVals
* @param highVals
* @param lowVals
*/
public static void make(XYPlot plot, CandlestickFormatter formatter,
XYSeries openVals, XYSeries closeVals, XYSeries highVals, XYSeries lowVals) {
plot.addSeries(formatter, highVals, lowVals, openVals, closeVals);
}
/**
* Add a candlestick chart to the specified plot using the specified {@link CandlestickSeries}.
* @param plot
* @param formatter
* @param series
* @since 0.9.8
*/
public static void make(XYPlot plot, CandlestickFormatter formatter, CandlestickSeries series) {
make(plot, formatter, series.getOpenSeries(), series.getCloseSeries(),
series.getHighSeries(), series.getLowSeries());
}
/**
* Check the validity of series data comprising a {@link CandlestickSeries}.
* This is a development aid; be sure to remove any usage of this method in production code.
* @param series
* @since 0.9.8
*/
public static void check(CandlestickSeries series) {
check(series.getOpenSeries(), series.getCloseSeries(), series.getHighSeries(), series.getLowSeries());
}
/**
* Check the validity of series data comprising a candlestick chart.
* This is a development aid; be sure to remove any usage of this method in production code.
* @param openVals
* @param closeVals
* @param highVals
* @param lowVals
* @since 0.9.8
*/
public static void check(XYSeries openVals, XYSeries closeVals, XYSeries highVals, XYSeries lowVals) {
final int size = openVals.size();
assert closeVals.size() == size : "closeVals has irregular size.";
assert highVals.size() == size : "highVals has irregular size.";
assert lowVals.size() == size : "lowVals has irregular size.";
for(int i = 0; i < size; i++) {
final double highVal = highVals.getY(i).doubleValue();
final double lowVal = lowVals.getY(i).doubleValue();
final double openVal = openVals.getY(i).doubleValue();
final double closeVal = closeVals.getY(i).doubleValue();
assert openVal <= highVal : "Detected openVal > highVal at index " + i;
assert openVal >= lowVal : "Detected openVal < lowVal at index " + i;
assert closeVal <= highVal : "Detected closeVal > highVal at index " + i;
assert closeVal >= lowVal : "Detected closeVal < lowVal at index " + i;
assert lowVal <= highVal : "Detected lowVal > highVal at index " + i;
}
}
}
@@ -0,0 +1,150 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.graphics.*;
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesBundle;
import java.util.List;
/**
* Renders a group of {@link com.androidplot.xy.XYSeries} as a candlestick chart
* into an {@link com.androidplot.xy.XYPlot}.
*
* Constraints:
* - Exactly four series must be added using the same {@link CandlestickFormatter}.
* - Each of the four series has the same x(i) value.
* - Expects that series are added in the order of:
* high, low, open, close
*
* {@link CandlestickSeries} and {@link CandlestickMaker} provide simplified classes and methods
* for setting up a candlestick chart.
* @since 0.9.7
*/
public class CandlestickRenderer<FormatterType extends CandlestickFormatter> extends GroupRenderer<FormatterType> {
protected static final int HIGH_INDEX = 0;
protected static final int LOW_INDEX = 1;
protected static final int OPEN_INDEX = 2;
protected static final int CLOSE_INDEX = 3;
public CandlestickRenderer(XYPlot plot) {
super(plot);
}
@Override
public void onRender(Canvas canvas, RectF plotArea, List<SeriesBundle<XYSeries,
? extends FormatterType>> sfList, int seriesSize, RenderStack stack) {
final FormatterType formatter = sfList.get(0).getFormatter();
for(int i = 0; i < seriesSize; i++) {
final XYSeries highSeries = sfList.get(HIGH_INDEX).getSeries();
final XYSeries lowSeries = sfList.get(LOW_INDEX).getSeries();
final XYSeries openSeries = sfList.get(OPEN_INDEX).getSeries();
final XYSeries closeSeries = sfList.get(CLOSE_INDEX).getSeries();
// x-val for all series should be identical so just grab x from the first series:
Number x = highSeries.getX(i);
Number high = highSeries.getY(i);
Number low = lowSeries.getY(i);
Number open = openSeries.getY(i);
Number close = closeSeries.getY(i);
// draw the candlestick:
final PointF highPix = getPlot().getBounds().transformScreen(x, high, plotArea);
final PointF lowPix = getPlot().getBounds().transformScreen(x, low, plotArea);
final PointF openPix = getPlot().getBounds().transformScreen(x, open, plotArea);
final PointF closePix = getPlot().getBounds().transformScreen(x, close, plotArea);
drawWick(canvas, highPix, lowPix, formatter);
drawBody(canvas, openPix, closePix, formatter);
drawUpperCap(canvas, highPix, formatter);
drawLowerCap(canvas, lowPix, formatter);
// draw labels, if any:
final PointLabelFormatter plf = formatter.hasPointLabelFormatter()
? formatter.getPointLabelFormatter() : null;
final PointLabeler pointLabeler = formatter.getPointLabeler();
if(plf != null && pointLabeler != null) {
drawTextLabel(canvas, highPix, pointLabeler.getLabel(highSeries, i), plf);
drawTextLabel(canvas, lowPix, pointLabeler.getLabel(lowSeries, i), plf);
drawTextLabel(canvas, openPix, pointLabeler.getLabel(openSeries, i), plf);
drawTextLabel(canvas, closePix, pointLabeler.getLabel(closeSeries, i), plf);
}
}
}
protected void drawTextLabel(Canvas canvas, PointF coords, String text, PointLabelFormatter plf) {
if(text != null) {
canvas.drawText(text, coords.x + plf.hOffset, coords.y + plf.vOffset, plf.getTextPaint());
}
}
protected void drawWick(Canvas canvas, PointF min, PointF max, FormatterType formatter) {
canvas.drawLine(min.x, min.y, max.x, max.y, formatter.getWickPaint());
}
protected void drawBody(Canvas canvas, PointF open, PointF close, FormatterType formatter) {
final float halfWidth = formatter.getBodyWidth() / 2;
final RectF rect = new RectF(open.x - halfWidth, open.y, close.x + halfWidth, close.y);
Paint bodyFillPaint = open.y >= close.y ?
formatter.getRisingBodyFillPaint() : formatter.getFallingBodyFillPaint();
Paint bodyStrokePaint = open.y >= close.y ?
formatter.getRisingBodyStrokePaint() : formatter.getFallingBodyStrokePaint();
switch(formatter.getBodyStyle()) {
case SQUARE:
canvas.drawRect(rect, bodyFillPaint);
canvas.drawRect(rect, bodyStrokePaint);
break;
case TRIANGULAR:
drawTriangle(canvas, rect, bodyFillPaint, bodyStrokePaint);
}
}
protected void drawUpperCap(Canvas canvas, PointF val, FormatterType formatter) {
final float halfWidth = formatter.getUpperCapWidth();
canvas.drawLine(val.x - halfWidth, val.y, val.x + halfWidth, val.y, formatter.getUpperCapPaint());
}
protected void drawLowerCap(Canvas canvas, PointF val, FormatterType formatter) {
final float halfWidth = formatter.getLowerCapWidth();
canvas.drawLine(val.x - halfWidth, val.y, val.x + halfWidth, val.y, formatter.getLowerCapPaint());
}
@Override
protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) {
// TODO
}
protected void drawTriangle(Canvas canvas, RectF rect,
Paint fillPaint, Paint strokePaint) {
Path path = new Path();
path.moveTo(rect.centerX(), rect.bottom);
path.lineTo(rect.left,rect.top);
path.lineTo(rect.right, rect.top);
path.close();
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
}
}
@@ -0,0 +1,160 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import com.androidplot.xy.SimpleXYSeries;
import java.util.*;
/**
* Convenience class for representing a series of candlestick values;
* is NOT a descendant of {@link com.androidplot.xy.XYSeries} and therefore
* cannot be directly added to an {@link com.androidplot.xy.XYPlot}.
*
* This class is NOT threadsafe.
*
* @since 0.9.8
*/
public class CandlestickSeries {
private SimpleXYSeries highSeries = new SimpleXYSeries(null);
private SimpleXYSeries lowSeries = new SimpleXYSeries(null);
private SimpleXYSeries openSeries = new SimpleXYSeries(null);
private SimpleXYSeries closeSeries = new SimpleXYSeries(null);
protected static List<Number> generateRange(int start, int end) {
List<Number> range = new ArrayList<>(end - start);
for(int i = start; i < end; i++) {
range.add(i);
}
return range;
}
public CandlestickSeries(Item... items) {
this(Arrays.asList(items));
}
/**
* Creates a new CandlestickSeries.
* Calls {@link #CandlestickSeries(List, List)} with a list of xVals
* generated using the formula x=i.
* @param items
*/
public CandlestickSeries(List<Item> items) {
this(generateRange(0, items.size()), items);
}
public CandlestickSeries(List<Number> xVals, List<Item> items) {
if(xVals.size() != items.size()) {
throw new IllegalArgumentException("xVals and yVals length must be identical.");
}
for(int i = 0; i < xVals.size(); i++) {
Number x = xVals.get(i);
highSeries.addLast(x, items.get(i).getHigh());
lowSeries.addLast(x, items.get(i).getLow());
openSeries.addLast(x, items.get(i).getOpen());
closeSeries.addLast(x, items.get(i).getClose());
}
}
public SimpleXYSeries getHighSeries() {
return highSeries;
}
public void setHighSeries(SimpleXYSeries highSeries) {
this.highSeries = highSeries;
}
public SimpleXYSeries getLowSeries() {
return lowSeries;
}
public void setLowSeries(SimpleXYSeries lowSeries) {
this.lowSeries = lowSeries;
}
public SimpleXYSeries getOpenSeries() {
return openSeries;
}
public void setOpenSeries(SimpleXYSeries openSeries) {
this.openSeries = openSeries;
}
public SimpleXYSeries getCloseSeries() {
return closeSeries;
}
public void setCloseSeries(SimpleXYSeries closeSeries) {
this.closeSeries = closeSeries;
}
public static class Item {
private double low;
private double high;
private double open;
private double close;
/**
* An individual candlestick value. Since it is illegal to include
* null values for any member of a candlestick, this class is modeled with
* double values instead of {@link Number} instances.
* @param low
* @param high
* @param open
* @param close
*/
public Item(double low, double high, double open, double close) {
this.low = low;
this.high = high;
this.open = open;
this.close = close;
}
public double getLow() {
return low;
}
public void setLow(double low) {
this.low = low;
}
public double getHigh() {
return high;
}
public void setHigh(double high) {
this.high = high;
}
public double getOpen() {
return open;
}
public void setOpen(double open) {
this.open = open;
}
public double getClose() {
return close;
}
public void setClose(double close) {
this.close = close;
}
}
}
@@ -0,0 +1,263 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import java.util.ArrayList;
import java.util.List;
/**
* 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<CatmullRomInterpolator.Params> {
public enum Type {
Uniform,
Centripetal
}
public static class Params implements InterpolationParams {
private int pointPerSegment;
private Type type;
public Params(int pointPerSegment, Type type) {
this.pointPerSegment = pointPerSegment;
this.type = type;
}
@Override
public Class<CatmullRomInterpolator> getInterpolatorClass() {
return CatmullRomInterpolator.class;
}
public int getPointPerSegment() {
return pointPerSegment;
}
public void setPointPerSegment(int pointPerSegment) {
this.pointPerSegment = pointPerSegment;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
}
/**
* Wraps a normal XYSeries, inserting a new point at the beginning and end of the series.
*/
static class ExtrapolatedXYSeries implements XYSeries {
private final XYCoords first;
private final XYCoords last;
private final XYSeries series;
public ExtrapolatedXYSeries(XYSeries series, XYCoords first, XYCoords last) {
this.series = series;
this.first = first;
this.last = last;
}
@Override
public Number getX(int i) {
if(i == 0) {
return first.x;
} else if(i == series.size() + 1) {
return last.x;
} else {
return series.getX(i-1);
}
}
@Override
public Number getY(int i) {
if(i == 0) {
return first.y;
} else if(i == series.size() + 1) {
return last.y;
} else {
return series.getY(i-1);
}
}
@Override
public int size() {
return series.size() + 2;
}
@Override
public String getTitle() {
return series.getTitle();
}
}
/**
* This method will calculate the Catmull-Rom interpolation curve, returning it as a list of Coord coordinate
* objects. This method in particular adds the first and last control points which are not visible, but required
* for calculating the spline.
*
* @param series The list of original straight line points to calculate an interpolation from.
* @return The list of interpolated coordinates.
* @throws java.lang.IllegalArgumentException if pointsPerSegment is less than 2.
*/
@Override
public List<XYCoords> interpolate(XYSeries series, Params params) {
if (params.getPointPerSegment() < 2) {
throw new IllegalArgumentException(
"pointsPerSegment must be greater than 2, since 2 points is just the linear segment.");
}
// Cannot interpolate curves given only two points. Two points
// is best represented as a simple line segment.
if (series.size() < 3) {
throw new IllegalArgumentException("Cannot interpolate a series with fewer than 3 vertices.");
}
// Get the change in x and y between the first and second coordinates.
double dx = series.getX(1).doubleValue() - series.getX(0).doubleValue();
double dy = series.getY(1).doubleValue() - series.getY(0).doubleValue();
// Then using the change, extrapolate backwards to find a control point.
double x1 = series.getX(0).doubleValue() - dx;
double y1 = series.getY(0).doubleValue() - dy;
// Actually create the start point from the extrapolated values.
XYCoords start = new XYCoords(x1, y1);
// Repeat for the end control point.
int n = series.size() -1;
dx = series.getX(n).doubleValue() - series.getX(n-1).doubleValue();
dy = series.getY(n).doubleValue() - series.getY(n - 1).doubleValue();
double xn = series.getX(n).doubleValue() + dx;
double yn = series.getY(n).doubleValue() + dy;
XYCoords end = new XYCoords(xn, yn);
// TODO: figure out whether this extra control-point synthesis is
// TODO: really necessary and either remove the above or fix the below.
// insert the start control point at the start of the vertices list.
// TODO vertices.add(0, start);
// append the end control ponit to the end of the vertices list.
// TODO vertices.add(end);
//}
ExtrapolatedXYSeries extrapolatedXYSeries = new ExtrapolatedXYSeries(series, start, end);
// Dimension a result list of coordinates.
List<XYCoords> result = new ArrayList<>();
// When looping, remember that each cycle requires 4 points, starting
// with i and ending with i+3. So we don't loop through all the points.
for (int i = 0; i < extrapolatedXYSeries.size() - 3; i++) {
// Actually calculate the Catmull-Rom curve for one segment.
List<XYCoords> points = interpolate(extrapolatedXYSeries, i, params);
// Since the middle points are added twice, once for each bordering
// segment, we only add the 0 index result point for the first
// segment. Otherwise we will have duplicate points.
if (result.size() > 0) {
points.remove(0);
}
// Add the coordinates for the segment to the result list.
result.addAll(points);
}
return result;
}
/**
* Given a list of control points, this will create a list of pointsPerSegment
* points spaced uniformly along the resulting Catmull-Rom curve.
*
* @param series The list of control points, leading and ending with a
* coordinate that is only used for controling the spline and is not visualized.
* @param index The index of control point p0, where p0, p1, p2, and p3 are
* used in order to create a curve between p1 and p2.
* @return the list of coordinates that define the CatmullRom curve
* between the points defined by index+1 and index+2.
*/
protected List<XYCoords> interpolate(XYSeries series, int index, Params params) {
List<XYCoords> result = new ArrayList<>();
double[] x = new double[4];
double[] y = new double[4];
double[] time = new double[4];
for (int i = 0; i < 4; i++) {
x[i] = series.getX(index + i).doubleValue();
y[i] = series.getY(index + i).doubleValue();
time[i] = i;
}
double tstart = 1;
double tend = 2;
if (params.getType() != Type.Uniform) {
double total = 0;
for (int i = 1; i < 4; i++) {
double dx = x[i] - x[i - 1];
double dy = y[i] - y[i - 1];
if (params.getType() == Type.Centripetal) {
total += Math.pow(dx * dx + dy * dy, .25);
} else {
total += Math.pow(dx * dx + dy * dy, .5);
}
time[i] = total;
}
tstart = time[1];
tend = time[2];
}
int segments = params.getPointPerSegment() - 1;
result.add(new XYCoords(series.getX(index + 1), series.getY(index + 1)));
for (int i = 1; i < segments; i++) {
double xi = interpolate(x, time, tstart + (i * (tend - tstart)) / segments);
double yi = interpolate(y, time, tstart + (i * (tend - tstart)) / segments);
result.add(new XYCoords(xi, yi));
}
result.add(new XYCoords(series.getX(index + 2), series.getY(index + 2)));
return result;
}
/**
* Unlike the other implementation here, which uses the default "uniform"
* treatment of t, this computation is used to calculate the same values but
* introduces the ability to "parameterize" the t values used in the
* calculation. This is based on Figure 3 from
* http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
*
* @param p An array of double values of length 4, where interpolation
* occurs from p1 to p2.
* @param time An array of time measures of length 4, corresponding to each
* p value.
* @param t the actual interpolation ratio from 0 to 1 representing the
* position between p1 and p2 to interpolate the value.
* @return
*/
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]);
double L012 = L01 * (time[2] - t) / (time[2] - time[0]) + L12 * (t - time[0]) / (time[2] - time[0]);
double L123 = L12 * (time[3] - t) / (time[3] - time[1]) + L23 * (t - time[1]) / (time[3] - time[1]);
double C12 = L012 * (time[2] - t) / (time[2] - time[1]) + L123 * (t - time[1]) / (time[2] - time[1]);
return C12;
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
/**
* An {@link XYSeries} that exposes methods to set values and resize
*/
public interface EditableXYSeries extends XYSeries {
void setX(Number x, int index);
void setY(Number y, int index);
/**
* Resize to accommodate the specified number of x/y pairs. If elements must be droped, those
* at the highest iVal should be removed first.
* @param size
*/
void resize(int size);
}
@@ -0,0 +1,10 @@
package com.androidplot.xy;
/**
* Base for all estimation management schemes.
*/
public abstract class Estimator {
public abstract void run(XYPlot plot, XYSeriesBundle sf);
}
@@ -0,0 +1,169 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesRenderer;
import java.util.ArrayList;
import java.util.List;
/**
* A faster implementation of of {@link LineAndPointRenderer}. For performance reasons, has these constraints:
* - Interpolation is not supported
* - Does not draw fill
* - Does not support null values
* @since 1.2.0
*/
public class FastLineAndPointRenderer extends XYSeriesRenderer<XYSeries, FastLineAndPointRenderer.Formatter> {
/**
* 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<Integer> segmentOffsets = new ArrayList<>();
List<Integer> segmentLengths = new ArrayList<>();
public FastLineAndPointRenderer(XYPlot plot) {
super(plot);
}
@Override
protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) {
segmentOffsets.clear();
segmentLengths.clear();
final int numPoints = series.size() * 2;
if(points == null || points.length != numPoints) {
// only allocate when necessary:
points = new float[series.size()*2];
}
int segmentLen = 0;
boolean isLastPointNull = true;
PointF resultPoint = new PointF();
for (int i = 0, j = 0; i < series.size(); i++, j+=2) {
Number y = series.getY(i);
Number x = series.getX(i);
if (y != null && x != null) {
if(isLastPointNull) {
segmentOffsets.add(j);
segmentLen = 0;
isLastPointNull = false;
}
getPlot().getBounds().transformScreen(resultPoint, x, y, plotArea);
points[j] = resultPoint.x;
points[j + 1] = resultPoint.y;
segmentLen+=2;
// if this is the last point, account for it in segment lengths:
if(i == series.size()-1) {
segmentLengths.add(segmentLen);
}
} else if(!isLastPointNull) {
segmentLengths.add(segmentLen);
isLastPointNull = true;
}
}
// draw segments
if(formatter.linePaint != null || formatter.vertexPaint != null) {
for (int i = 0; i < segmentOffsets.size(); i++) {
final int len = segmentLengths.get(i);
final int offset = segmentOffsets.get(i);
drawSegment(canvas, points, offset, len, formatter);
}
}
}
protected void drawSegment(@NonNull Canvas canvas,
@NonNull float[] points,
int offset,
int len,
Formatter formatter) {
if(formatter.linePaint != null) {
// draw lines:
if (len >= MINIMUM_NUMBER_OF_POINTS_TO_DEFINE_A_LINE) {
// optimization to avoid using 2x storage space to represent the full path:
if ((len & 2) != 0) {
canvas.drawLines(points, offset, len - 2, formatter.linePaint);
canvas.drawLines(points, offset + 2, len - 2, formatter.linePaint);
} else {
canvas.drawLines(points, offset, len, formatter.linePaint);
canvas.drawLines(points, offset + 2, len - 4, formatter.linePaint);
}
}
}
if(formatter.vertexPaint != null) {
// draw vertices:
canvas.drawPoints(points, offset, len, formatter.vertexPaint);
}
}
@Override
protected void doDrawLegendIcon(@NonNull Canvas canvas,
@NonNull RectF rect,
@NonNull Formatter formatter) {
if(formatter.hasLinePaint()) {
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
}
if(formatter.hasVertexPaint()) {
canvas.drawPoint(rect.centerX(), rect.centerY(), formatter.getVertexPaint());
}
}
/**
* Formatter designed to work in tandem with {@link AdvancedLineAndPointRenderer}.
* @since 0.9.9
*/
public static class Formatter extends LineAndPointFormatter {
public Formatter(Integer lineColor, Integer vertexColor, PointLabelFormatter plf) {
super(lineColor, vertexColor, null, plf);
}
@Override
protected void initLinePaint(Integer lineColor) {
super.initLinePaint(lineColor);
// disable anti-aliasing by default:
getLinePaint().setAntiAlias(false);
}
@Override
public Class<? extends SeriesRenderer> getRendererClass() {
return FastLineAndPointRenderer.class;
}
@Override
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
return new FastLineAndPointRenderer(plot);
}
}
}
@@ -0,0 +1,19 @@
package com.androidplot.xy;
/**
* An implementation of {@link XYSeries} that defines additional methods to speed up rendering by
* giving a hint to the renderer about the min/max values contained in the series.
*
* Note that these hints can only be leveraged if the containing XYPlot's constraints completely
* contain the FastXYSeries min/max values. If this condition is not met then XYPlot falls back
* to manually determining the min/max values of the series that exist within the defined constraints.
*/
public interface FastXYSeries extends XYSeries {
/**
* TIP: You can use {@link RectRegion#union(Number, Number)} during
* to keep a running tally of min/max values when iterating.
* @return A {@link RectRegion} representing the min/max values that currently exist this series.
*/
RectRegion minMax();
}
@@ -0,0 +1,36 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
/**
* Defines which edge is used to close a fill path for drawing lines.
*
* TOP - Use the top edge of the plot.
* BOTTOM - Use the bottom edge of the plot.
* LEFT - (Not implemented) Use the left edge of the plot.
* RIGHT - (Not implemented) Use the right edge of the plot.
* DOMAIN_ORIGIN - (Not implemented) Use the domain origin line.
* RANGE_ORIGIN - Use the range origin line.
*/
public enum FillDirection {
TOP,
BOTTOM,
LEFT,
RIGHT,
DOMAIN_ORIGIN,
RANGE_ORIGIN
}
@@ -0,0 +1,89 @@
package com.androidplot.xy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.androidplot.util.FastNumber;
import java.util.ArrayList;
import java.util.List;
/**
* An efficient implementation of {@link EditableXYSeries} intended for use cases where
* the total number of points visible is known ahead of time and is fairly static.
*
* {@link #resize(int)} may be used to resize the series when necessary, however it is a slow
* operation and should be avoided as much as possible.
*
*/
public class FixedSizeEditableXYSeries implements EditableXYSeries {
@NonNull
private List<FastNumber> xVals = new ArrayList<>();
@NonNull
private List<FastNumber> yVals = new ArrayList<>();
private String title;
public FixedSizeEditableXYSeries(String title, int size) {
setTitle(title);
resize(size);
}
@Override
public void setX(@Nullable Number x, int index) {
xVals.set(index, FastNumber.orNull(x));
}
@Override
public void setY(@Nullable Number y, int index) {
yVals.set(index, FastNumber.orNull(y));
}
/**
* May be used to dynamically resize the series. This is a relatively slow operation, especially
* as size increases so care should be taken to avoid unnecessary usage.
* @param size
*/
@Override
public void resize(int size) {
resize(xVals, size);
resize(yVals, size);
}
protected void resize(@NonNull List list, int size) {
if (size > list.size()) {
while (list.size() < size) {
list.add(null);
}
} else if (size < list.size()) {
while (list.size() > size) {
list.remove(list.size() - 1);
}
}
}
@Override
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public int size() {
return xVals.size();
}
@Override
public Number getX(int index) {
return xVals.get(index);
}
@Override
public Number getY(int index) {
return yVals.get(index);
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2016 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.graphics.Canvas;
import android.graphics.RectF;
import android.util.Log;
import com.androidplot.ui.RenderStack;
import com.androidplot.ui.SeriesBundle;
import java.util.List;
/**
* Renders data to an XYPlot that potentially contains more than a single yVal per index, or in other
* words data with potentially more than two dimensions.
* Examples of such data are bar plot groups and candlestick charts.
* @since 0.9.7
*/
public abstract class GroupRenderer<FormatterType extends XYSeriesFormatter<XYRegionFormatter>>
extends XYSeriesRenderer<XYSeries, FormatterType> {
private static final String TAG = GroupRenderer.class.getName();
public GroupRenderer(XYPlot plot) {
super(plot);
}
@Override
protected void onRender(Canvas canvas, RectF plotArea, XYSeries series,
FormatterType formatter, RenderStack stack) {
// get all the data associated with this renderer:
List<SeriesBundle<XYSeries, ? extends FormatterType>> sfList = getSeriesAndFormatterList();
// no data to render so exit:
if(sfList == null) {
return;
}
int seriesLength = sfList.get(0).getSeries().size();
// make sure all associated series are the same length:
for(int i = 1; i < sfList.size(); i++) {
if(sfList.get(i).getSeries().size() != seriesLength) {
// series sizes are irregular so don't try to render:
Log.w(TAG, getClass() + ": " + "not all associated series are of same size.");
return;
}
}
// this renderer uses special element-by-element z-indexing that results in
// all series associated with the renderer being rendered in a single pass, so
// we need to exclude the rest of the series on the render stack from being redrawn later:
stack.disable(getClass());
onRender(canvas, plotArea, sfList, seriesLength, stack);
}
/**
*
* @param canvas
* @param plotArea
* @param sfList
* @param stack
*/
public abstract void onRender(Canvas canvas, RectF plotArea, List<SeriesBundle<XYSeries,
? extends FormatterType>> sfList, int size, RenderStack stack);
}
@@ -0,0 +1,25 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
/**
* Created by nick_f on 9/25/14.
*/
public interface InterpolationParams<InterpolatorType extends Interpolator> {
Class<InterpolatorType> getInterpolatorClass();
}
@@ -0,0 +1,28 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import java.util.List;
/**
* Created by nick_f on 9/25/14.
*/
public interface Interpolator<ParamsType extends InterpolationParams> {
List<XYCoords> interpolate(XYSeries series, ParamsType params);
}
@@ -0,0 +1,106 @@
package com.androidplot.xy;
import android.util.*;
/**
* Adapted from:
* https://github.com/drcrane/downsample
*
* Note that this implementation does not yet support null values.
*
* Basic usage example:
* <pre>
* {@code
* // An instance of any implementation of XYSeries; SimpleXYSeries, etc:
* XYSeries origalSeries = ...;
*
* // Sampled series with half the resolution of the
* EditableXYSeries sampledSeries = new FixedSizeEditableXYSeries(
* origalSeries.getTitle(), origalSeries.size() / 2);
*
* // does the actual sampling:
* new LTTBSampler().run(origalSeries, sampledSeries);
* }
* </pre>
*/
public class LTTBSampler implements Sampler {
public RectRegion run(XYSeries rawData, EditableXYSeries sampled) {
RectRegion bounds = new RectRegion();
final int threshold = sampled.size();
final int dataLength = rawData.size();
final int startIndex = 0;
if (threshold >= dataLength || threshold == 0) {
//return data; // Nothing to do
// TODO: set flag to return raw data
throw new RuntimeException("Shouldnt be here!");
}
int sampledIndex = 0;
// Bucket size. Leave room for start and end data points
final double bucketSize = (double) (dataLength - 2) / (threshold - 2);
int a = 0; // Initially a is the first point in the triangle
int nextA = 0;
setSample(rawData, sampled, a + startIndex, sampledIndex, bounds);
sampledIndex++;
for (int i = 0; i < threshold - 2; i++) {
// Calculate point average for next bucket (containing c)
double pointCX = 0;
double pointCY = 0;
int pointCStart = (int) Math.floor((i + 1) * bucketSize) + 1;
int pointCEnd = (int) Math.floor((i + 2) * bucketSize) + 1;
pointCEnd = pointCEnd < dataLength ? pointCEnd : dataLength;
final int pointCSize = pointCEnd - pointCStart;
for (; pointCStart < pointCEnd; pointCStart++) {
if(rawData.getX(pointCStart + startIndex) != null) {
pointCX += rawData.getX(pointCStart + startIndex).doubleValue();
}
if(rawData.getY(pointCStart + startIndex) != null) {
pointCY += rawData.getY(pointCStart + startIndex).doubleValue();
}
}
pointCX /= pointCSize;
pointCY /= pointCSize;
double pointAX = rawData.getX(a + startIndex).doubleValue();
double pointAY = rawData.getY(a + startIndex).doubleValue();
// Get the range for bucket b
int pointBStart = (int) Math.floor((i + 0) * bucketSize) + 1;
final int pointBEnd = (int) Math.floor((i + 1) * bucketSize) + 1;
double maxArea = -1;
XYCoords maxAreaPoint = null;
for (; pointBStart < pointBEnd; pointBStart++) {
final double area = Math.abs((pointAX - pointCX) * (rawData.getY(pointBStart + startIndex)
.doubleValue() - pointAY) - (pointAX - rawData.getX(pointBStart + startIndex)
.doubleValue())
* (pointCY - pointAY)) * 0.5;
if (area > maxArea) {
if(rawData.getY(pointBStart + startIndex) == null) {
Log.i("LTTB", "Null value encountered in raw data at index: " + pointBStart);
}
maxArea = area;
maxAreaPoint = new XYCoords(rawData.getX(pointBStart + startIndex),
rawData.getY(pointBStart + startIndex));
nextA = pointBStart; // Next a is this b
}
}
setSample(sampled, maxAreaPoint.x, maxAreaPoint.y, sampledIndex, bounds);
sampledIndex++;
a = nextA; // This a is the next a (chosen b)
}
setSample(rawData, sampled, (dataLength + startIndex) - 1, sampledIndex, bounds);
sampledIndex++;
return bounds;
}
protected void setSample(XYSeries raw, EditableXYSeries sampled, int rawIndex, int sampleIndex, RectRegion bounds) {
setSample(sampled, raw.getX(rawIndex), raw.getY(rawIndex), sampleIndex, bounds);
}
protected void setSample(EditableXYSeries sampled, Number x, Number y, int sampleIndex, RectRegion bounds) {
bounds.union(x, y);
sampled.setX(x, sampleIndex);
sampled.setY(y, sampleIndex);
}
}
@@ -0,0 +1,203 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.content.*;
import android.graphics.Color;
import android.graphics.Paint;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.util.PixelUtils;
/**
* Defines the visual aesthetics of an XYSeries; outline color and width, fill style,
* vertex size and color, shadowing, etc.
*/
public class LineAndPointFormatter extends XYSeriesFormatter<XYRegionFormatter> {
private static final float DEFAULT_LINE_STROKE_WIDTH_DP = 1.5f;
private static final float DEFAULT_VERTEX_STROKE_WIDTH_DP = 4.5f;
public FillDirection getFillDirection() {
return fillDirection;
}
/**
* Sets which edge to use to close the line's path for filling purposes.
* See {@link FillDirection}.
* @param fillDirection
*/
public void setFillDirection(FillDirection fillDirection) {
this.fillDirection = fillDirection;
}
protected FillDirection fillDirection = FillDirection.BOTTOM;
protected Paint linePaint;
protected Paint vertexPaint;
protected Paint fillPaint;
protected InterpolationParams interpolationParams;
public LineAndPointFormatter(Context context, int xmlCfgId) {
super(context, xmlCfgId);
}
/**
* Should only be used in conjunction with calls to configure()...
*/
public LineAndPointFormatter() {
this(Color.RED, Color.GREEN, Color.BLUE, null);
}
public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor,
PointLabelFormatter plf) {
this(lineColor, vertexColor, fillColor, plf, FillDirection.BOTTOM);
}
public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor,
PointLabelFormatter plf, FillDirection fillDir) {
initLinePaint(lineColor);
initVertexPaint(vertexColor);
initFillPaint(fillColor);
setFillDirection(fillDir);
this.setPointLabelFormatter(plf);
}
@Override
public Class<? extends SeriesRenderer> getRendererClass() {
return LineAndPointRenderer.class;
}
@Override
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
return new LineAndPointRenderer(plot);
}
protected void initLinePaint(Integer lineColor) {
if (lineColor == null) {
linePaint = null;
} else {
linePaint = new Paint();
linePaint.setAntiAlias(true);
linePaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_LINE_STROKE_WIDTH_DP));
linePaint.setColor(lineColor);
linePaint.setStyle(Paint.Style.STROKE);
}
}
protected void initVertexPaint(Integer vertexColor) {
if (vertexColor == null) {
vertexPaint = null;
} else {
vertexPaint = new Paint();
vertexPaint.setAntiAlias(true);
vertexPaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_VERTEX_STROKE_WIDTH_DP));
vertexPaint.setColor(vertexColor);
vertexPaint.setStrokeCap(Paint.Cap.ROUND);
}
}
protected void initFillPaint(Integer fillColor) {
if (fillColor == null) {
fillPaint = null;
} else {
fillPaint = new Paint();
fillPaint.setAntiAlias(true);
fillPaint.setColor(fillColor);
}
}
/**
*
* @return True if linePaint has been set, false otherwise.
*/
public boolean hasLinePaint() {
return linePaint != null;
}
/**
* Get the {@link Paint} used to draw lines. Will instantiate and a new default instance
* if it is currently null. To run whether or not line paint has been set, use
* {@link #hasLinePaint()}.
* @return
*/
public Paint getLinePaint() {
if(linePaint == null) {
initLinePaint(Color.TRANSPARENT);
}
return linePaint;
}
public void setLinePaint(Paint linePaint) {
this.linePaint = linePaint;
}
/**
*
* @return True if vertexPaint has been set, false otherwise.
*/
public boolean hasVertexPaint() {
return vertexPaint != null;
}
/**
* Get the {@link Paint} used to draw vertices (points). Will instantiate and a new default instance
* if it is currently null. To run whether or not vertex paint has been set, use
* {@link #hasVertexPaint()}.
* @return
*/
public Paint getVertexPaint() {
if(vertexPaint == null) {
initVertexPaint(Color.TRANSPARENT);
}
return vertexPaint;
}
public void setVertexPaint(Paint vertexPaint) {
this.vertexPaint = vertexPaint;
}
/**
*
* @return True if fillPaint has been set, false otherwise.
*/
public boolean hasFillPaint() {
return fillPaint != null;
}
/**
* Get the {@link Paint} used to fill series areas. Will instantiate and a new default instance
* if it is currently null. To run whether or not fill paint has been set, use
* {@link #hasFillPaint()}.
* @return
*/
public Paint getFillPaint() {
if(fillPaint == null) {
initFillPaint(Color.TRANSPARENT);
}
return fillPaint;
}
public void setFillPaint(Paint fillPaint) {
this.fillPaint = fillPaint;
}
public InterpolationParams getInterpolationParams() {
return interpolationParams;
}
public void setInterpolationParams(InterpolationParams params) {
this.interpolationParams = params;
}
}
@@ -0,0 +1,328 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import com.androidplot.Plot;
import com.androidplot.PlotListener;
import com.androidplot.Region;
import com.androidplot.ui.RenderStack;
import com.androidplot.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* Renders a point as a line with the vertices marked. Requires 2 or more points to
* be rendered.
*/
public class LineAndPointRenderer<FormatterType extends LineAndPointFormatter> extends XYSeriesRenderer<XYSeries, FormatterType> {
protected static final int ZERO = 0;
protected static final int ONE = 1;
private final Path path = new Path();
protected final ConcurrentHashMap<XYSeries, ArrayList<PointF>> pointsCaches
= new ConcurrentHashMap<>(2, 0.75f, 2);
public LineAndPointRenderer(XYPlot plot) {
super(plot);
plot.addListener(new PlotListener() {
@Override
public void onBeforeDraw(Plot source, Canvas canvas) {
cullPointsCache();
}
@Override
public void onAfterDraw(Plot source, Canvas canvas) {
}
});
}
@Override
public void onRender(Canvas canvas, RectF plotArea, XYSeries series, FormatterType formatter, RenderStack stack) {
drawSeries(canvas, plotArea, series, formatter);
}
@Override
public void doDrawLegendIcon(Canvas canvas, RectF rect, LineAndPointFormatter formatter) {
// horizontal icon:
float centerY = rect.centerY();
float centerX = rect.centerX();
if(formatter.getFillPaint() != null) {
canvas.drawRect(rect, formatter.getFillPaint());
}
if(formatter.hasLinePaint()) {
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
}
if(formatter.hasVertexPaint()) {
canvas.drawPoint(centerX, centerY, formatter.getVertexPaint());
}
}
/**
* This method exists for StepRenderer to override without having to duplicate any
* additional code.
*/
protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) {
path.lineTo(thisPoint.x, thisPoint.y);
}
/**
* Retrieves or initializes a list for storing calculated screen-coords to render as points.
* Also handles automatic resizing and culling of unused caches.
* Should only be called once per render cycle.
* @param series
* @return
*/
protected ArrayList<PointF> getPointsCache(XYSeries series) {
ArrayList<PointF> pointsCache = pointsCaches.get(series);
final int seriesSize = series.size();
if(pointsCache == null) {
pointsCache = new ArrayList<>(seriesSize);
pointsCaches.put(series, pointsCache);
}
if(pointsCache.size() < seriesSize) {
while(pointsCache.size() < seriesSize) {
pointsCache.add(null);
}
} else if(pointsCache.size() > seriesSize) {
while(pointsCache.size() > seriesSize) {
pointsCache.remove(0);
}
}
return pointsCache;
}
protected void cullPointsCache() {
for(XYSeries series : pointsCaches.keySet()) {
if(!getPlot().getRegistry().contains(series, LineAndPointFormatter.class)) {
pointsCaches.remove(series);
}
}
}
protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) {
PointF thisPoint;
PointF lastPoint = null;
PointF firstPoint = null;
path.reset();
final List<PointF> points = getPointsCache(series);
int iStart = 0;
int iEnd = series.size();
if(SeriesUtils.getXYOrder(series) == OrderedXYSeries.XOrder.ASCENDING) {
final Region iBounds = SeriesUtils.iBounds(series, getPlot().getBounds());
iStart = iBounds.getMin().intValue();
if(iStart > 0) {
iStart--;
}
iEnd = iBounds.getMax().intValue() + 1;
if(iEnd < series.size() - 1) {
iEnd++;
}
}
for (int i = iStart; i < iEnd; i++) {
final Number y = series.getY(i);
final Number x = series.getX(i);
PointF iPoint = points.get(i);
if (y != null && x != null) {
if(iPoint == null) {
iPoint = new PointF();
points.set(i, iPoint);
}
thisPoint = iPoint;
getPlot().getBounds().transformScreen(thisPoint, x, y, plotArea);
} else {
thisPoint = null;
iPoint = null;
points.set(i, iPoint);
}
// don't need to do any of this if the line isnt going to be drawn:
if(formatter.hasLinePaint() && formatter.getInterpolationParams() == null) {
if (thisPoint != null) {
// record the first point of the new Path
if (firstPoint == null) {
path.reset();
firstPoint = thisPoint;
// create our first point at the bottom/x position so filling will look good:
path.moveTo(firstPoint.x, firstPoint.y);
}
if (lastPoint != null) {
appendToPath(path, thisPoint, lastPoint);
}
lastPoint = thisPoint;
} else {
if (lastPoint != null) {
renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter);
}
firstPoint = null;
lastPoint = null;
}
}
}
if(formatter.hasLinePaint()) {
if(formatter.getInterpolationParams() != null) {
List<XYCoords> interpolatedPoints = getInterpolator(
formatter.getInterpolationParams()).interpolate(series,
formatter.getInterpolationParams());
firstPoint = convertPoint(interpolatedPoints.get(ZERO), plotArea);
lastPoint = convertPoint(interpolatedPoints.get(interpolatedPoints.size()-ONE), plotArea);
path.reset();
path.moveTo(firstPoint.x, firstPoint.y);
for(int i = 1; i < interpolatedPoints.size(); i++) {
thisPoint = convertPoint(interpolatedPoints.get(i), plotArea);
path.lineTo(thisPoint.x, thisPoint.y);
}
}
if(firstPoint != null) {
renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter);
}
}
renderPoints(canvas, plotArea, series, iStart, iEnd, points, formatter);
}
/**
* TODO: retrieve from a persistent registry
* @param params
* @return An interpol
*/
protected Interpolator getInterpolator(InterpolationParams params) {
try {
return (Interpolator) params.getInterpolatorClass().newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
protected PointF convertPoint(XYCoords coord, RectF plotArea) {
return getPlot().getBounds().transformScreen(coord, plotArea);
}
protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, int iStart, int iEnd, List<PointF> points,
LineAndPointFormatter formatter) {
if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) {
final Paint vertexPaint = formatter.hasVertexPaint() ? formatter.getVertexPaint() : null;
final boolean hasPointLabelFormatter = formatter.hasPointLabelFormatter();
final PointLabelFormatter plf = hasPointLabelFormatter ? formatter.getPointLabelFormatter() : null;
final PointLabeler pointLabeler = hasPointLabelFormatter ? formatter.getPointLabeler() : null;
for(int i = iStart; i < iEnd; i++) {
PointF p = points.get(i);
if(p != null) {
// if vertexPaint is available, draw vertex:
if (vertexPaint != null) {
canvas.drawPoint(p.x, p.y, vertexPaint);
}
// if textPaint and pointLabeler are available, draw point's text label:
if (pointLabeler != null) {
canvas.drawText(pointLabeler.getLabel(series, i),
p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint());
}
}
}
}
}
protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF firstPoint, PointF lastPoint, LineAndPointFormatter formatter) {
Path outlinePath = new Path(path);
// determine how to close the path for filling purposes:
// We always need to calculate this path because it is also used for
// masking off for region highlighting.
switch (formatter.getFillDirection()) {
case BOTTOM:
path.lineTo(lastPoint.x, plotArea.bottom);
path.lineTo(firstPoint.x, plotArea.bottom);
path.close();
break;
case TOP:
path.lineTo(lastPoint.x, plotArea.top);
path.lineTo(firstPoint.x, plotArea.top);
path.close();
break;
case RANGE_ORIGIN:
float originPix = (float) getPlot().getBounds().getxRegion()
.transform(getPlot().getRangeOrigin()
.doubleValue(), plotArea.top, plotArea.bottom, true);
path.lineTo(lastPoint.x, originPix);
path.lineTo(firstPoint.x, originPix);
path.close();
break;
default:
throw new UnsupportedOperationException(
"Fill direction not yet implemented: " + formatter.getFillDirection());
}
if (formatter.getFillPaint() != null) {
canvas.drawPath(path, formatter.getFillPaint());
}
final RectRegion bounds = getPlot().getBounds();
final RectRegion plotRegion = new RectRegion(plotArea);
// draw each region:
for (RectRegion thisRegion : bounds.intersects(formatter.getRegions().elements())) {
XYRegionFormatter regionFormatter = formatter.getRegionFormatter(thisRegion);
RectRegion thisRegionTransformed = bounds
.transform(thisRegion, plotRegion, false, true);
thisRegionTransformed.intersect(plotRegion);
if(thisRegion.isFullyDefined()) {
RectF thisRegionRectF = thisRegionTransformed.asRectF();
if (thisRegionRectF != null) {
try {
canvas.save();
canvas.clipPath(path);
canvas.drawRect(thisRegionRectF, regionFormatter.getPaint());
} finally {
canvas.restore();
}
}
}
}
// finally we draw the outline path on top of everything else:
if(formatter.hasLinePaint()) {
canvas.drawPath(outlinePath, formatter.getLinePaint());
}
path.rewind();
}
}
@@ -0,0 +1,136 @@
package com.androidplot.xy;
import com.androidplot.Region;
import com.androidplot.util.SeriesUtils;
/**
* Wrapper implementation of {@link XYSeries} that wraps another XYSeries, normalizing values in the range of 0 to 1.
* Note that it's possible to push normed values outside of the standard 0, 1 range by applying
* a sufficiently large offset.
*/
public class NormedXYSeries implements XYSeries {
private XYSeries rawData;
private Region minMaxX;
private Region minMaxY;
private Region transformX;
private Region transformY;
public static class Norm {
final Region minMax;
final double offset;
final boolean useOffsetCompression;
public Norm(Region minMax) {
this(minMax, 0, false);
}
/**
*
* @param minMax Boundary to use when calculating the norm coefficient. Set to null to let
* Androidplot auto calculate the bounds. (Very inefficient)
* @param offset An extra offset to apply, generally within the range of -1 and 1.
* This value is useful for adjusting the positioning of a series relative to another normalized series.
* @param useOffsetCompression If true, the offset value will result in further scaling down
* of the series data in order to ensure that all points within the specified bounds remain
* visible on the screen. If set to true, the specified offset MUST be > -1 and < 1. Will be
* ignored if bounds != null.
*/
public Norm(Region minMax, double offset, boolean useOffsetCompression) {
this.minMax = minMax;
this.offset = offset;
this.useOffsetCompression = useOffsetCompression;
if (useOffsetCompression && (offset <= -1 || offset >= 1)) {
throw new IllegalArgumentException(
"When useOffsetCompression is true, offset must be > -1 and < 1.");
}
}
}
/**
* Normalizes yVals only, auto calculating min/max.
* @param rawData
*/
public NormedXYSeries(XYSeries rawData) {
this(rawData, null, new Norm(null, 0, false));
}
/**
*
* @param rawData The XYSeries to be normalized.
* @param x Normalization to apply to xVals. Set to null to disable normalization on the x axis.
* @param y Normalization to apply to yVals. Set to null to disable normalization on the y axis.
*/
public NormedXYSeries(XYSeries rawData, Norm x, Norm y) {
this.rawData = rawData;
normalize(x, y);
}
protected void normalize(Norm x, Norm y) {
if( x != null) {
this.minMaxX = x.minMax != null ? x.minMax : SeriesUtils.minMaxX(rawData);
this.transformX = calculateTransform(x);
}
if( y != null) {
this.minMaxY = y.minMax != null ? y.minMax : SeriesUtils.minMaxY(rawData);
this.transformY = calculateTransform(y);
}
}
protected Region calculateTransform(Norm norm) {
if(norm.useOffsetCompression) {
return new Region(
norm.offset > 0 ? norm.offset : 0,
norm.offset < 0 ? 1 + norm.offset : 1);
} else {
return new Region(0 + norm.offset, 1 + norm.offset);
}
}
@Override
public String getTitle() {
return rawData.getTitle();
}
@Override
public int size() {
return rawData.size();
}
public Number denormalizeXVal(Number xVal) {
if(xVal != null) {
return transformX.transform(xVal.doubleValue(), minMaxX);
}
return null;
}
public Number denormalizeYVal(Number yVal) {
if(yVal != null) {
return transformY.transform(yVal.doubleValue(), minMaxY);
}
return null;
}
@Override
public Number getX(int index) {
final Number xVal = rawData.getX(index);
if(xVal != null && transformX != null) {
return minMaxX.transform(xVal.doubleValue(), transformX);
}
return xVal;
}
@Override
public Number getY(int index) {
final Number yVal = rawData.getY(index);
if(yVal != null && transformY != null) {
return minMaxY.transform(yVal.doubleValue(), transformY);
}
return yVal;
}
}
@@ -0,0 +1,33 @@
package com.androidplot.xy;
/**
* An implementation of {@link XYSeries} that gives hints to it's renderer about the order
* of the data being rendered.
*/
public interface OrderedXYSeries extends XYSeries {
enum XOrder {
/**
* XVals are in strict ascending order such that:
* x(i) < x(i+1) == true
*/
ASCENDING,
/**
* XVals are in strict descending order such that:
* x(i) > x(i+1) == true
*/
DESCENDING,
/**
* XVals appear in no particular order.
*/
NONE
}
/**
* The order of XVals as they appear in this series.
* @return
*/
XOrder getXOrder();
}
@@ -0,0 +1,535 @@
package com.androidplot.xy;
import android.graphics.RectF;
import android.graphics.PointF;
import androidx.annotation.NonNull;
import android.view.*;
import com.androidplot.*;
import com.androidplot.util.*;
import java.io.Serializable;
import java.util.*;
/**
* Enables basic pan/zoom touch behavior for an {@link XYPlot}.
* By default boundaries there are no boundaries imposed on scrolling and zooming. You can provide these boundaries
* on your {@link XYPlot} using {@link XYPlot#getOuterLimits()}.
* TODO: zoom using dynamic center point
* TODO: stretch both mode
*/
public class PanZoom implements View.OnTouchListener {
protected static final float MIN_DIST_2_FING = 5f;
protected static final int FIRST_FINGER = 0;
protected static final int SECOND_FINGER = 1;
private XYPlot plot;
private Pan pan;
private Zoom zoom;
private ZoomLimit zoomLimit;
private boolean isEnabled = true;
private DragState dragState = DragState.NONE;
private PointF firstFingerPos;
// rectangle created by the space between two fingers
protected RectF fingersRect;
private View.OnTouchListener delegate;
private State state = new State();
// Definition of the touch states
protected enum DragState {
NONE,
ONE_FINGER,
TWO_FINGERS
}
public enum Pan {
NONE,
HORIZONTAL,
VERTICAL,
BOTH
}
public enum Zoom {
/**
* Comletely disable panning
*/
NONE,
/**
* Zoom on the horizontal axis only
*/
STRETCH_HORIZONTAL,
/**
* Zoom on the vertical axis only
*/
STRETCH_VERTICAL,
/**
* Zoom on the vertical axis by the vertical distance between each finger, while zooming
* on the horizontal axis by the horizantal distance between each finger.
*/
STRETCH_BOTH,
/**
* Zoom each axis by the same amount, specifically the total distance between each finger.
*/
SCALE
}
/**
* Limits imposed on the zoom.
*/
public enum ZoomLimit {
/**
* Do not zoom outside the plots outer bounds, if they are defined.
*/
OUTER,
/**
* Additionally to the outer bounds if plot.StepModel defines a value based increment
* make sure at least one tick is visible by not zooming in further.
*/
MIN_TICKS
}
// TODO: consider making this immutable / threadsafe
public static class State implements Serializable {
private Number domainLowerBoundary;
private Number domainUpperBoundary;
private Number rangeLowerBoundary;
private Number rangeUpperBoundary;
private BoundaryMode domainBoundaryMode;
private BoundaryMode rangeBoundaryMode;
public void setDomainBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
this.domainLowerBoundary = lowerBoundary;
this.domainUpperBoundary = upperBoundary;
this.domainBoundaryMode = mode;
}
public void setRangeBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
this.rangeLowerBoundary = lowerBoundary;
this.rangeUpperBoundary = upperBoundary;
this.rangeBoundaryMode = mode;
}
public void applyDomainBoundaries(@NonNull XYPlot plot) {
plot.setDomainBoundaries(domainLowerBoundary, domainUpperBoundary, domainBoundaryMode);
}
public void applyRangeBoundaries(@NonNull XYPlot plot) {
plot.setRangeBoundaries(rangeLowerBoundary, rangeUpperBoundary, rangeBoundaryMode);
}
public void apply(@NonNull XYPlot plot) {
applyDomainBoundaries(plot);
applyRangeBoundaries(plot);
}
}
protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom) {
this.plot = plot;
this.pan = pan;
this.zoom = zoom;
this.zoomLimit = ZoomLimit.OUTER;
}
// additional constructor not to break api
protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom, ZoomLimit limit) {
this.plot = plot;
this.pan = pan;
this.zoom = zoom;
this.zoomLimit = limit;
}
public State getState() {
return this.state;
}
public void setState(@NonNull State state) {
this.state = state;
state.apply(plot);
}
protected void adjustRangeBoundary(Number lower, Number upper, BoundaryMode mode) {
state.setRangeBoundaries(lower, upper, mode);
state.applyRangeBoundaries(plot);
}
protected void adjustDomainBoundary(Number lower, Number upper, BoundaryMode mode) {
state.setDomainBoundaries(lower, upper, mode);
state.applyDomainBoundaries(plot);
}
/**
* Convenience method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using
* a default behavior of {@link Pan#BOTH} and {@link Zoom#SCALE}.
* Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior.
* @param plot
* @return
*/
public static PanZoom attach(@NonNull XYPlot plot) {
return attach(plot, Pan.BOTH, Zoom.SCALE);
}
/**
* Old method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using
* the default behavior of {@link ZoomLimit#OUTER}.
* Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior.
* @param plot
* @param pan
* @param zoom
* @return
*/
public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom) {
return attach(plot,pan,zoom, ZoomLimit.OUTER);
}
/**
* New method for enabling pan/zoom behavior on an instance of {@link XYPlot}.
* @param plot
* @param pan
* @param zoom
* @param limit
* @return
*/
public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom, @NonNull ZoomLimit limit) {
PanZoom pz = new PanZoom(plot, pan, zoom, limit);
plot.setOnTouchListener(pz);
return pz;
}
public boolean isEnabled() {
return isEnabled;
}
public void setEnabled(boolean enabled) {
isEnabled = enabled;
}
@Override
public boolean onTouch(final View view, final MotionEvent event) {
boolean isConsumed = false;
if (delegate != null) {
isConsumed = delegate.onTouch(view, event);
}
if (isEnabled() && !isConsumed) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: // start gesture
firstFingerPos = new PointF(event.getX(), event.getY());
dragState = DragState.ONE_FINGER;
break;
case MotionEvent.ACTION_POINTER_DOWN: // second finger
{
setFingersRect(fingerDistance(event));
// the distance run is done to avoid false alarms
if (getFingersRect().width() > MIN_DIST_2_FING || getFingersRect().width() < -MIN_DIST_2_FING) {
dragState = DragState.TWO_FINGERS;
}
break;
}
case MotionEvent.ACTION_POINTER_UP: // end zoom
dragState = DragState.NONE;
break;
case MotionEvent.ACTION_MOVE:
if (dragState == DragState.ONE_FINGER) {
pan(event);
} else if (dragState == DragState.TWO_FINGERS) {
zoom(event);
}
break;
case MotionEvent.ACTION_UP:
reset();
break;
}
}
// we're forced to consume the event here as not consuming it will prevent future calls:
return true;
}
/**
* Calculates the distance between two finger motion events.
* @param firstFingerX
* @param firstFingerY
* @param secondFingerX
* @param secondFingerY
* @return
*/
protected RectF fingerDistance(float firstFingerX, float firstFingerY, float secondFingerX, float secondFingerY) {
final float left = firstFingerX > secondFingerX ? secondFingerX : firstFingerX;
final float right = firstFingerX > secondFingerX ? firstFingerX : secondFingerX;
final float top = firstFingerY > secondFingerY ? secondFingerY : firstFingerY;
final float bottom = firstFingerY > secondFingerY ? firstFingerY : secondFingerY;
return new RectF(left, top, right, bottom);
}
/**
* Calculates the distance between two finger motion events.
* @param evt
* @return
*/
protected RectF fingerDistance(final MotionEvent evt) {
return fingerDistance(
evt.getX(FIRST_FINGER),
evt.getY(FIRST_FINGER),
evt.getX(SECOND_FINGER),
evt.getY(SECOND_FINGER));
}
protected void pan(final MotionEvent motionEvent) {
if (pan == Pan.NONE) {
return;
}
final PointF oldFirstFinger = firstFingerPos; //save old position of finger
firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position
if (EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) {
Region newBounds = new Region();
calculatePan(oldFirstFinger, newBounds, true);
adjustDomainBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
}
if (EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) {
Region newBounds = new Region();
calculatePan(oldFirstFinger, newBounds, false);
adjustRangeBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
}
plot.redraw();
}
protected void calculatePan(final PointF oldFirstFinger, Region bounds, final boolean horizontal) {
final float offset;
// multiply the absolute finger movement for a factor.
// the factor is dependent on the calculated min and max
if (horizontal) {
bounds.setMinMax(plot.getBounds().getxRegion());
offset = (oldFirstFinger.x - firstFingerPos.x) *
((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getWidth());
} else {
bounds.setMinMax(plot.getBounds().getyRegion());
offset = -(oldFirstFinger.y - firstFingerPos.y) *
((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getHeight());
}
// move the calculated offset
bounds.setMin(bounds.getMin().floatValue() + offset);
bounds.setMax(bounds.getMax().floatValue() + offset);
//get the distance between max and min
final float diff = bounds.length().floatValue();
//run if we reached the limit of panning
if (horizontal && plot.getOuterLimits().getxRegion().isDefined()) {
if (bounds.getMin().floatValue() < plot.getOuterLimits().getMinX().floatValue()) {
bounds.setMin(plot.getOuterLimits().getMinX());
bounds.setMax(bounds.getMin().floatValue() + diff);
}
if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxX().floatValue()) {
bounds.setMax(plot.getOuterLimits().getMaxX());
bounds.setMin(bounds.getMax().floatValue() - diff);
}
} else if(plot.getOuterLimits().getyRegion().isDefined()) {
if (bounds.getMin().floatValue() < plot.getOuterLimits().getMinY().floatValue()) {
bounds.setMin(plot.getOuterLimits().getMinY());
bounds.setMax(bounds.getMin().floatValue() + diff);
}
if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxY().floatValue()) {
bounds.setMax(plot.getOuterLimits().getMaxY());
bounds.setMin(bounds.getMax().floatValue() - diff);
}
}
}
protected boolean isValidScale(float scale) {
return !Float.isInfinite(scale)
&& !Float.isNaN(scale)
&& (!(scale > -0.001) || !(scale < 0.001));
}
protected void zoom(final MotionEvent motionEvent) {
if (zoom == Zoom.NONE) {
return;
}
final RectF oldFingersRect = getFingersRect();
final RectF newFingersRect = fingerDistance(motionEvent);
setFingersRect(newFingersRect);
if(oldFingersRect == null || RectFUtils.areIdentical(oldFingersRect, newFingersRect)) {
// zooming gesture has not happened yet so skip:
return;
}
RectF newRect = new RectF();
float scaleX = 1;
float scaleY = 1;
switch (zoom) {
case STRETCH_HORIZONTAL:
scaleX = oldFingersRect.width() / getFingersRect().width();
if (!isValidScale(scaleX)) {
return;
}
break;
case STRETCH_VERTICAL:
scaleY = oldFingersRect.height() / getFingersRect().height();
if (!isValidScale(scaleY)) {
return;
}
break;
case STRETCH_BOTH:
scaleX = oldFingersRect.width() / getFingersRect().width();
scaleY = oldFingersRect.height() / getFingersRect().height();
if (!isValidScale(scaleX) || !isValidScale(scaleY)) {
return;
}
break;
case SCALE:
float sc1 = (float) Math.hypot(oldFingersRect.height(), oldFingersRect.width());
float sc2 = (float) Math.hypot(getFingersRect().height(), getFingersRect().width());
float sc = sc1 / sc2;
scaleX = sc;
scaleY = sc;
if (!isValidScale(scaleX) || !isValidScale(scaleY)) {
return;
}
break;
}
if (EnumSet.of(
Zoom.STRETCH_HORIZONTAL,
Zoom.STRETCH_BOTH,
Zoom.SCALE).contains(zoom)) {
calculateZoom(newRect, scaleX, true);
adjustDomainBoundary(newRect.left, newRect.right, BoundaryMode.FIXED);
}
if (EnumSet.of(
Zoom.STRETCH_VERTICAL,
Zoom.STRETCH_BOTH,
Zoom.SCALE).contains(zoom)) {
calculateZoom(newRect, scaleY, false);
adjustRangeBoundary(newRect.top, newRect.bottom, BoundaryMode.FIXED);
}
plot.redraw();
}
/**
*
* @param newRect RectF into which zoom calculation results should be placed.
* @param scale
* @param isHorizontal
*/
protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) {
final float calcMax;
final float span;
final RectRegion bounds = plot.getBounds();
if (isHorizontal) {
calcMax = bounds.getMaxX().floatValue();
span = calcMax - bounds.getMinX().floatValue();
} else {
calcMax = bounds.getMaxY().floatValue();
span = calcMax - bounds.getMinY().floatValue();
}
final float midPoint = calcMax - (span / 2.0f);
float offset = span * scale / 2.0f;
final RectRegion limits = plot.getOuterLimits();
if (isHorizontal ) {
// zoom limited and increment by value StepMode?
if (zoomLimit == ZoomLimit.MIN_TICKS) {
// make sure we do not zoom in too far (there should be at least one grid line visible)
if (plot.getDomainStepValue() > (scale*span)) {
offset = (float)(plot.getDomainStepValue() / 2.0f);
}
}
newRect.left = midPoint - offset;
newRect.right = midPoint + offset;
if(limits.isFullyDefined()) {
if (newRect.left < limits.getMinX().floatValue()) {
newRect.left = limits.getMinX().floatValue();
}
if (newRect.right > limits.getMaxX().floatValue()) {
newRect.right = limits.getMaxX().floatValue();
}
}
} else {
// zoom limited and increment by value StepMode?
if (zoomLimit == ZoomLimit.MIN_TICKS) {
// make sure we do not zoom in too far (there should be at least one grid line visible)
if (plot.getRangeStepValue() > (scale*span)) {
offset = (float)(plot.getRangeStepValue() / 2.0f);
}
}
newRect.top = midPoint - offset;
newRect.bottom = midPoint + offset;
if(limits.isFullyDefined()) {
if (newRect.top < limits.getMinY().floatValue()) {
newRect.top = limits.getMinY().floatValue();
}
if (newRect.bottom > limits.getMaxY().floatValue()) {
newRect.bottom = limits.getMaxY().floatValue();
}
}
}
}
public Pan getPan() {
return pan;
}
public void setPan(Pan pan) {
this.pan = pan;
}
public Zoom getZoom() {
return zoom;
}
public void setZoom(Zoom zoom) {
this.zoom = zoom;
}
public ZoomLimit getZoomLimit() {
return zoomLimit;
}
public void setZoomLimit(ZoomLimit zoomLimit) {
this.zoomLimit = zoomLimit;
}
public View.OnTouchListener getDelegate() {
return delegate;
}
/**
* Set a delegate to receive onTouch calls before this class does. If the delegate wishes
* to consume the event, it should return true, otherwise it should return false. Returning
* false will not prevent future onTouch events from filtering through the delegate as it normally
* would when attaching directly to an instance of {@link View}.
* @param delegate
*/
public void setDelegate(View.OnTouchListener delegate) {
this.delegate = delegate;
}
public void reset() {
this.firstFingerPos = null;
setFingersRect(null);
this.setFingersRect(null);
}
protected RectF getFingersRect() {
return fingersRect;
}
protected void setFingersRect(RectF fingersRect) {
this.fingersRect = fingersRect;
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.graphics.Color;
import android.graphics.Paint;
import com.androidplot.util.PixelUtils;
public class PointLabelFormatter {
private static final float DEFAULT_H_OFFSET_DP = 0;
private static final float DEFAULT_V_OFFSET_DP = -4;
private static final float DEFAULT_TEXT_SIZE_SP = 12;
private Paint textPaint;
public float hOffset;
public float vOffset;
public PointLabelFormatter() {
this(Color.WHITE);
}
public PointLabelFormatter(int textColor) {
this(textColor, PixelUtils.dpToPix(DEFAULT_H_OFFSET_DP),
PixelUtils.dpToPix(DEFAULT_V_OFFSET_DP));
}
/**
*
* @param textColor
* @param hOffset Horizontal offset of text in pixels.
* @param vOffset Vertical offset of text in pixels. Offset is in screen coordinates;
* positive values shift the text further down the screen.
*/
public PointLabelFormatter(int textColor, float hOffset, float vOffset) {
initTextPaint(textColor);
this.hOffset = hOffset;
this.vOffset = vOffset;
}
public boolean hasTextPaint() {
return textPaint != null;
}
public Paint getTextPaint() {
if(textPaint == null) {
initTextPaint(Color.TRANSPARENT);
}
return textPaint;
}
public void setTextPaint(Paint textPaint) {
this.textPaint = textPaint;
}
protected void initTextPaint(Integer textColor) {
if (textColor == null) {
setTextPaint(null);
} else {
setTextPaint(new Paint());
getTextPaint().setAntiAlias(true);
getTextPaint().setColor(textColor);
getTextPaint().setTextAlign(Paint.Align.CENTER);
getTextPaint().setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_SP));
//textPaint.setStyle(Paint.Style.STROKE);
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
public interface PointLabeler<SeriesType extends XYSeries> {
String getLabel(SeriesType series, int index);
}
@@ -0,0 +1,357 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
import android.graphics.PointF;
import android.graphics.RectF;
import com.androidplot.Region;
import java.util.ArrayList;
import java.util.List;
/**
* RectRegion is just a rectangle with additional methods for determining
* intersections with other RectRegion instances. RectRegion operates on
* "native units" which are simply whatever unit type is passed into it by the user.
*/
public class RectRegion {
Region xRegion;
Region yRegion;
private String label;
public RectRegion() {
xRegion = new Region();
yRegion = new Region();
}
public static RectRegion withDefaults(RectRegion defaults) {
if(defaults == null || !defaults.isFullyDefined()) {
throw new IllegalArgumentException("When specifying defaults, RectRegion param must contain no null values.");
}
RectRegion r = new RectRegion();
r.xRegion = Region.withDefaults(defaults.getxRegion());
r.yRegion = Region.withDefaults(defaults.getyRegion());
return r;
}
public RectRegion(XYCoords min, XYCoords max) {
this(min.x, max.x, min.y, max.y);
}
/**
* @param minX
* @param maxX
* @param minY
* @param maxY
*/
public RectRegion(Number minX, Number maxX, Number minY, Number maxY, String label) {
xRegion = new Region(minX, maxX);
yRegion = new Region(minY, maxY);
this.setLabel(label);
}
public RectRegion(RectF rect) {
this(rect.left < rect.right ? rect.left : rect.right,
rect.right > rect.left ? rect.right : rect.left,
rect.bottom < rect.top ? rect.bottom : rect.top,
rect.top > rect.bottom ? rect.top : rect.bottom);
}
@SuppressWarnings("SameParameterValue")
public RectRegion(Number minX, Number maxX, Number minY, Number maxY) {
this(minX, maxX, minY, maxY, null);
}
public XYCoords transform(Number x, Number y, RectRegion region2, boolean flipX, boolean flipY) {
Number xx = xRegion.transform(x.doubleValue(), region2.xRegion, flipX);
Number yy = yRegion.transform(y.doubleValue(), region2.yRegion, flipY);
return new XYCoords(xx, yy);
}
public XYCoords transform(Number x, Number y, RectRegion region2) {
return transform(x, y, region2, false, false);
}
public XYCoords transform(XYCoords value, RectRegion region2) {
return transform(value.x, value.y, region2);
}
/**
* Transform a region (r) from the current region space (this) into the specified one (r2)
* @param r The region to which the transformation applies
* @param r2 The region into which r is being transformed
* @return
*/
public RectRegion transform(RectRegion r, RectRegion r2, boolean flipX, boolean flipY) {
return new RectRegion(
transform(r.getMinX(), r.getMinY(), r2, flipX, flipY),
transform(r.getMaxX(), r.getMaxY(), r2, flipX, flipY)
);
}
/**
* Convenience method to transform into screen coordinate space. Equivalent to invoking
* {@link #transform(Number, Number, RectF, boolean, boolean)} with
* flipX = false and flipY = true.
* @param x
* @param y
* @param region2
* @return
*/
public PointF transformScreen(Number x, Number y, RectF region2) {
return transform(x, y, region2, false, true);
}
public void transformScreen(PointF result, Number x, Number y, RectF region2) {
transform(result, x, y, region2, false, true);
}
public void transform(PointF result, Number x, Number y, RectF region2, boolean flipX, boolean flipY) {
result.x = (float) xRegion.transform(x.doubleValue(), region2.left, region2.right, flipX);
result.y = (float) yRegion.transform(y.doubleValue(), region2.top, region2.bottom, flipY);
}
public PointF transform(Number x, Number y, RectF region2, boolean flipX, boolean flipY) {
PointF result = new PointF();
transform(result, x, y, region2, flipX, flipY);
return result;
}
public PointF transformScreen(XYCoords value, RectF region2) {
return transform(value, region2, false, true);
}
/**
* Convenience method to transform into screen coordinate space. Equivalent to invoking
* {@link #transform(XYCoords, RectF, boolean, boolean)} with flipX = false and flipY = true.
* @param value
* @param region2
* @param flipX
* @param flipY
* @return
*/
public PointF transform(XYCoords value, RectF region2, boolean flipX, boolean flipY) {
return transform(value.x, value.y, region2, flipX, flipY);
}
public void union(Number x, Number y) {
xRegion.union(x);
yRegion.union(y);
}
/**
* Compares the input bounds xy min/max vals against this instance's current xy min/max vals.
* If the input.min is less than this.min then this.min will be set to input.min.
* If the input.max is greater than this.max then this.max will be set to input.max
*
* The result will always have equal or greater area than the inputs.
* @param input
*/
public void union(RectRegion input) {
xRegion.union(input.xRegion);
yRegion.union(input.yRegion);
}
public boolean intersects(RectRegion region) {
return intersects(region.getMinX(), region.getMaxX(), region.getMinY(), region.getMaxY());
}
/**
* Tests whether this region intersects the region defined by params. Use
* null to represent infinity. Negative and positive infinity is implied by
* the boundary edge, ie. a maxX of null equals positive infinity while a
* minX of null equals negative infinity.
* @param minX
* @param maxX
* @param minY
* @param maxY
* @return
*/
public boolean intersects(Number minX, Number maxX, Number minY, Number maxY) {
return xRegion.intersects(minX, maxX) && yRegion.intersects(minY, maxY);
}
public RectF asRectF() {
return new RectF(getMinX().floatValue(), getMinY().floatValue(),
getMaxX().floatValue(), getMaxY().floatValue());
}
/**
* The result of an intersect is always a RectRegion with an equal or smaller area.
* @param clippingBounds
*/
public void intersect(RectRegion clippingBounds) {
if(intersects(clippingBounds)) {
xRegion.intersect(clippingBounds.xRegion);
yRegion.intersect(clippingBounds.yRegion);
} else {
setMinY(null);
setMaxY(null);
setMinX(null);
setMaxX(null);
}
}
/**
* Returns a list of XYRegions that either completely or partially intersect the area
* defined by params. A null value for any parameter represents infinity / no boundary.
* @param regions The list of regions to search through
* @return
*/
public List<RectRegion> intersects(List<RectRegion> regions) {
ArrayList<RectRegion> intersectingRegions = new ArrayList<>();
for (RectRegion r : regions) {
if (r.intersects(getMinX(), getMaxX(), getMinY(), getMaxY())) {
intersectingRegions.add(r);
}
}
return intersectingRegions;
}
/**
* @return Width of this region, in native units
*/
public Number getWidth() {
return distanceBetween(getMinX(), getMaxX());
}
/**
* @return Height of this region, in native units
*/
public Number getHeight() {
return distanceBetween(getMinY(), getMaxY());
}
/**
* Calculate the distance between two points in a single dimension.
* @param x
* @param y
* @return
*/
private static Number distanceBetween(Number x, Number y) {
return Math.abs(x.doubleValue() - y.doubleValue());
}
public void set(Number minX, Number maxX, Number minY, Number maxY) {
setMinX(minX);
setMaxX(maxX);
setMinY(minY);
setMaxY(maxY);
}
public boolean isMinXSet() {
return xRegion.isMinSet();
}
public Number getMinX() {
return xRegion.getMin();
}
public void setMinX(Number minX) {
xRegion.setMin(minX);
}
public boolean isMaxXSet() {
return xRegion.isMaxSet();
}
public Number getMaxX() {
return xRegion.getMax();
}
public void setMaxX(Number maxX) {
xRegion.setMax(maxX);
}
public boolean isMinYSet() {
return yRegion.isMinSet();
}
public Number getMinY() {
return yRegion.getMin();
}
public void setMinY(Number minY) {
yRegion.setMin(minY);
}
public boolean isMaxYSet() {
return yRegion.isMaxSet();
}
public Number getMaxY() {
return yRegion.getMax();
}
public void setMaxY(Number maxY) {
yRegion.setMax(maxY);
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public Region getxRegion() {
return xRegion;
}
public void setxRegion(Region xRegion) {
this.xRegion = xRegion;
}
public Region getyRegion() {
return yRegion;
}
public void setyRegion(Region yRegion) {
this.yRegion = yRegion;
}
/**
*
* @return True if both xRegion and yRegion are defined, false otherwise
*/
public boolean isFullyDefined() {
return xRegion.isDefined() && yRegion.isDefined();
}
/**
* True if this region contains the specified coordinates.
* @param x
* @param y
* @return
*/
public boolean contains(Number x, Number y) {
return getxRegion().contains(x) && getyRegion().contains(y);
}
@Override
public String toString() {
return "RectRegion{" +
"xRegion=" + xRegion +
", yRegion=" + yRegion +
", label='" + label + '\'' +
'}';
}
}
@@ -0,0 +1,216 @@
package com.androidplot.xy;
import com.androidplot.*;
import com.androidplot.util.SeriesUtils;
import java.util.*;
/**
* An implementation of {@link FastXYSeries} that samples its self into multiple levels to
* achieve faster rendering / zoom behavior. By default, uses {@link LTTBSampler} as
* it's sampling algorithm. Note that this algorithm does not yet support null values.
*
* Sampling behavior is controlled by two values:
* Ratio: A value greater than 1; controls the sampling ratio of each successive series. For example,
* a step of 2 would mean that each successive series contains 2x fewer points than the previous.
*
* Threshold: A value < the original series size; controls the lower limit at which point sampling should stop.
* For example, sampling a series with size 1000 given a ratio of 2 and a threshold of 100,
* three sampled resolutions will be generated:
*
* 500 - 2x sampling
* 250 - 4x sampling
* 125 - 8x sampling
*
*/
public class SampledXYSeries implements FastXYSeries, OrderedXYSeries {
private int threshold;
private Sampler algorithm = new LTTBSampler();
private XYSeries rawData;
private List<EditableXYSeries> zoomLevels;
private XYSeries activeSeries;
private RectRegion bounds;
private Exception lastResamplingException;
private final XOrder xOrder;
private float ratio;
/**
*
* @param rawData
* @param xOrder If your data is in ascending or descending order, specifying it here speed up
* optimize render times.
* @param ratio The ratio used to determine the size of each new sampled series. Must be > 1.
* downsampled series until threshold is reached.
* @param threshold The desired size of the smallest sample series. Must be < rawData.size.
*/
public SampledXYSeries(XYSeries rawData, XOrder xOrder, float ratio, int threshold) {
this.rawData = rawData;
this.xOrder = xOrder;
this.setRatio(ratio);
this.setThreshold(threshold);
resample();
}
/**
* Generate a SampledXYSeries from the input series.
* @param rawData The original series to be downsampled
* @param ratio The ratio used to determine the size of each new sampled series. Must be > 1.
* downsampled series until threshold is reached.
* @param threshold The desired size of the smallest sample series. Must be < rawData.size.
*/
public SampledXYSeries(XYSeries rawData, float ratio, int threshold) {
this(rawData, SeriesUtils.getXYOrder(rawData), ratio, threshold);
}
public void resample() {
bounds = null;
zoomLevels = new ArrayList<>();
int t = (int) Math.ceil(rawData.size() / getRatio());
List<Thread> threads = new ArrayList<>(zoomLevels.size());
while (t > threshold) {
final int thisThreshold = t;
final EditableXYSeries thisSeries = new FixedSizeEditableXYSeries(getTitle(), thisThreshold);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
// TODO: make bounds a param to prevent calculating on each setZoomFactor level:
RectRegion b = getAlgorithm()
.run(rawData, thisSeries);
if (bounds == null) {
bounds = b;
}
} catch(Exception ex) {
lastResamplingException = ex;
}
}
}, "Androidplot XY Series Sampler");
getZoomLevels().add(thisSeries);
threads.add(thread);
thread.start();
t = (int) Math.ceil(t / getRatio());
}
for(Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if(lastResamplingException != null) {
throw new RuntimeException("Exception encountered during resampling", lastResamplingException);
}
}
protected List<EditableXYSeries> getZoomLevels() {
return this.zoomLevels;
}
/**
* Set zoom factor; 2.5 = 2.5x zoom, 10.0 = 10x zoom etc. This method will set the zoom level
* to the closest available factor to the specified factor; a specified factor of 4.5x may result
* in an actual factor of 4x.
* @param factor
*/
public void setZoomFactor(double factor) {
if(factor <= 1) {
activeSeries = rawData;
} else {
//int i = (int) Math.round(Math.sqrt(factor) - 1);
int i = getZoomIndex(factor, getRatio());
if (i < zoomLevels.size()) {
activeSeries = zoomLevels.get(i);
} else {
activeSeries = zoomLevels.get(zoomLevels.size() - 1);
}
}
}
protected static int getZoomIndex(double zoomFactor, double ratio) {
final double lhs = Math.log(zoomFactor);
final double rhs = Math.log(ratio);
final double log = lhs / rhs;
final int index = (int) Math.round(log);
return index > 0 ? index - 1 : 0;
}
public double getMaxZoomFactor() {
return Math.pow(getRatio(), zoomLevels.size());
}
public Sampler getAlgorithm() {
return algorithm;
}
public void setAlgorithm(Sampler algorithm) {
this.algorithm = algorithm;
resample();
}
@Override
public String getTitle() {
return rawData.getTitle();
}
@Override
public int size() {
return activeSeries.size();
}
@Override
public Number getX(int index) {
return activeSeries.getX(index);
}
@Override
public Number getY(int index) {
return activeSeries.getY(index);
}
public int getThreshold() {
return threshold;
}
public void setThreshold(int threshold) {
if(threshold >= rawData.size()) {
throw new IllegalArgumentException("Threshold must be < original series size.");
}
this.threshold = threshold;
}
public RectRegion getBounds() {
return bounds;
}
public void setBounds(RectRegion bounds) {
this.bounds = bounds;
}
@Override
public RectRegion minMax() {
return bounds;
}
@Override
public XOrder getXOrder() {
return xOrder;
}
public float getRatio() {
return ratio;
}
public void setRatio(float ratio) {
if(ratio <= 1) {
throw new IllegalArgumentException("Ratio must be greater than 1");
}
this.ratio = ratio;
}
}

Some files were not shown because too many files have changed in this diff Show More