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
+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;
}
}
@@ -0,0 +1,16 @@
package com.androidplot.xy;
/**
* An algorithm used to to resample a larger set of data into a smaller set.
*/
public interface Sampler {
/**
*
* @param input The original unsampled series
* @param output The destination series to contain sampled result.
* This series size should be set to the desired sampled size.
* @return min/max values encountered while processing input.
*/
RectRegion run(XYSeries input, EditableXYSeries output);
}
@@ -0,0 +1,67 @@
package com.androidplot.xy;
/**
* Wraps an existing {@link XYSeries} allowing easy scaling of that series' xy values.
*/
public class ScalingXYSeries implements XYSeries {
private double scale;
private XYSeries series;
private Mode mode;
public enum Mode {
X_ONLY,
Y_ONLY,
X_AND_Y
}
/**
*
* @param series The {@link XYSeries} to be scaled
* @param scale The initial scale to be applied
* @param mode Determines which axis (or both) to which scaling will be applied.
*/
public ScalingXYSeries(XYSeries series, double scale, Mode mode) {
this.series = series;
this.scale = scale;
this.mode = mode;
}
@Override
public String getTitle() {
return series.getTitle();
}
@Override
public int size() {
return series.size();
}
@Override
public Number getX(int index) {
Number x = series.getX(index);
if(mode == Mode.X_ONLY || mode == Mode.X_AND_Y) {
return x == null ? null : x.doubleValue() * scale;
} else {
return x;
}
}
@Override
public Number getY(int index) {
Number y = series.getY(index);
if(mode == Mode.Y_ONLY || mode == Mode.X_AND_Y) {
return y == null ? null : y.doubleValue() * scale;
} else {
return y;
}
}
public double getScale() {
return scale;
}
public void setScale(double scale) {
this.scale = scale;
}
}
@@ -0,0 +1,352 @@
/*
* 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 com.androidplot.Plot;
import com.androidplot.PlotListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* A convenience class used to create instances of XYPlot generated from Lists of Numbers.
*/
public class SimpleXYSeries implements EditableXYSeries, OrderedXYSeries, PlotListener {
private volatile LinkedList<Number> xVals = new LinkedList<>();
private volatile LinkedList<Number> yVals = new LinkedList<>();
private volatile String title = null;
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
private XOrder xOrder = XOrder.NONE;
public enum ArrayFormat {
Y_VALS_ONLY,
XY_VALS_INTERLEAVED
}
public SimpleXYSeries(String title) {
this.title = title;
}
public SimpleXYSeries(ArrayFormat format, String title, Number... model) {
this(asNumberList(model), format, title);
}
/**
* Retrieve the current x-ordering specified for this series. Default is
* {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE}.
* @return
*/
@Override
public XOrder getXOrder() {
return xOrder;
}
/**
* If XVals are in strict ascending order, use this method to set
* {@link com.androidplot.xy.OrderedXYSeries.XOrder#ASCENDING} to provide an optimization
* hint to the renderer.
* @param xOrder
*/
public void setXOrder(XOrder xOrder) {
this.xOrder = xOrder;
}
@Override
public void onBeforeDraw(Plot source, Canvas canvas) {
lock.readLock().lock();
}
@Override
public void onAfterDraw(Plot source, Canvas canvas) {
lock.readLock().unlock();
}
protected static List<Number> asNumberList(Number... model) {
List<Number> numbers = new ArrayList<>(model.length);
Collections.addAll(numbers, model);
return numbers;
}
/**
* Generates an XYSeries instance from the List of numbers passed in. This is a convenience class
* and should only be used for static data models; it is not suitable for representing dynamically
* changing data.
*
* @param model A List of Number elements comprising the data model.
* @param format Format of the model. A format of Y_VALS_ONLY means that the array only contains y-values.
* For this format x values are autogenerated using values of 0 through n-1 where n is the size of the model.
* @param title Title of the series
*/
public SimpleXYSeries(List<? extends Number> model, ArrayFormat format, String title) {
this(title);
setModel(model, format);
}
public SimpleXYSeries(List<? extends Number> xVals, List<? extends Number> yVals, String title) {
this(title);
if(xVals == null || yVals == null) {
throw new IllegalArgumentException("Neither the xVals nor the yVals parameters may be null.");
}
if(xVals.size() != yVals.size()) {
throw new IllegalArgumentException("xVals and yVals List parameters must be of the same size.");
}
this.xVals.addAll(xVals);
this.yVals.addAll(yVals);
}
/**
* Use index value as xVal, instead of explicit, user provided xVals.
*/
public void useImplicitXVals() {
lock.writeLock().lock();
try {
xVals = null;
} finally {
lock.writeLock().unlock();
}
}
/**
* Use the provided list of Numbers as yVals and their corresponding indexes as xVals.
* @param model A List of Number elements comprising the data model.
* @param format Format of the model. A format of Y_VALS_ONLY means that the array only contains y-values.
* For this format x values are autogenerated using values of 0 through n-1 where n is the size of the model.
*/
public void setModel(List<? extends Number> model, ArrayFormat format) {
lock.writeLock().lock();
try {
// empty the current values:
xVals.clear();
yVals.clear();
// make sure the new model has data:
if (model == null || model.size() == 0) {
return;
}
switch (format) {
// array containing only y-vals. assume x = index:
case Y_VALS_ONLY:
yVals.addAll(model);
for(int i = 0; i < yVals.size(); i++) {
xVals.add(i);
}
break;
// xy interleaved array:
case XY_VALS_INTERLEAVED:
if (xVals == null) {
xVals = new LinkedList<>();
}
if (model.size() % 2 != 0) {
throw new IndexOutOfBoundsException("Cannot auto-generate series from odd-sized xy List.");
}
// always need an x and y array so init them now:
int sz = model.size() / 2;
for (int i = 0, j = 0; i < sz; i++, j += 2) {
xVals.add(model.get(j));
yVals.add(model.get(j + 1));
}
break;
default:
throw new IllegalArgumentException("Unexpected enum value: " + format);
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Sets individual x value based on index
* @param value
* @param index
*/
public void setX(Number value, int index) {
lock.writeLock().lock();
try {
xVals.set(index, value);
} finally {
lock.writeLock().unlock();
}
}
/**
* Sets individual y value based on index
* @param value
* @param index
*/
public void setY(Number value, int index) {
lock.writeLock().lock();
try {
yVals.set(index, value);
} finally {
lock.writeLock().unlock();
}
}
@Override
public void resize(int size) {
try {
lock.writeLock().lock();
if (xVals.size() < size) {
for (int i = xVals.size(); i < size; i++) {
xVals.add(null);
yVals.add(null);
}
} else if(xVals.size() > size) {
for(int i = xVals.size(); i > size; i--) {
xVals.removeLast();
yVals.removeLast();
}
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Sets xy values based on index
* @param xVal
* @param yVal
* @param index
*/
public void setXY(Number xVal, Number yVal, int index) {
lock.writeLock().lock();
try {
yVals.set(index, yVal);
xVals.set(index, xVal);
} finally {lock.writeLock().unlock();}
}
public void addFirst(Number x, Number y) {
lock.writeLock().lock();
try {
if (xVals != null) {
xVals.addFirst(x);
}
yVals.addFirst(y);
} finally {
lock.writeLock().unlock();
}
}
/**
*
* @return {@link XYCoords} with first equal to x-val and second equal to y-val.
*/
public XYCoords removeFirst() {
lock.writeLock().lock();
try {
if (size() <= 0) {
throw new NoSuchElementException();
}
return new XYCoords(xVals != null ? xVals.removeFirst() : 0, yVals.removeFirst());
} finally {
lock.writeLock().unlock();
}
}
public void addLast(Number x, Number y) {
lock.writeLock().lock();
try {
if (xVals != null) {
xVals.addLast(x);
}
yVals.addLast(y);
} finally {
lock.writeLock().unlock();
}
}
/**
*
* @return {@link XYCoords} with first equal to x-val and second equal to y-val.
*/
public XYCoords removeLast() {
lock.writeLock().lock();
try {
if (size() <= 0) {
throw new NoSuchElementException();
}
return new XYCoords(xVals != null ? xVals.removeLast() : yVals.size() - 1, yVals.removeLast());
} finally {
lock.writeLock().unlock();
}
}
@Override
public String getTitle() {
return title;
}
public void setTitle(String title) {
lock.writeLock().lock();
try {
this.title = title;
} finally {lock.writeLock().unlock();}
}
@Override
public int size() {
return yVals != null ? yVals.size() : 0;
}
@Override
public Number getX(int index) {
return xVals != null ? xVals.get(index) : index;
}
@Override
public Number getY(int index) {
return yVals.get(index);
}
public LinkedList<Number> getxVals() {
return xVals;
}
public LinkedList<Number> getyVals() {
return yVals;
}
/**
* Remove all values from the series
*/
public void clear() {
lock.writeLock().lock();
try {
if (xVals != null) {
xVals.clear();
}
yVals.clear();
} finally {
lock.writeLock().unlock();
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2015 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.xy;
/**
* An immutable object generated by XYStepCalculator representing
* a stepping model to be used by an XYPlot.
*/
public class Step {
private final double stepCount;
private final double stepPix;
private final double stepVal;
//public XYStep() {}
public Step(double stepCount, double stepPix, double stepVal) {
this.stepCount = stepCount;
this.stepPix = stepPix;
this.stepVal = stepVal;
}
public double getStepCount() {
return stepCount;
}
/*public void setStepCount(double stepCount) {
this.stepCount = stepCount;
}*/
public double getStepPix() {
return stepPix;
}
/*public void setStepPix(float stepPix) {
this.stepPix = stepPix;
}*/
public double getStepVal() {
return stepVal;
}
/*public void setStepVal(double stepVal) {
this.stepVal = stepVal;
}*/
}

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