> sfList, int size, RenderStack stack);
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java b/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java
new file mode 100644
index 00000000..820f2250
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java
@@ -0,0 +1,106 @@
+package com.androidplot.xy;
+
+import android.util.*;
+
+/**
+ * Adapted from:
+ * https://github.com/drcrane/downsample
+ *
+ * Note that this implementation does not yet support null values.
+ *
+ * Basic usage example:
+ *
+ * {@code
+ * // An instance of any implementation of XYSeries; SimpleXYSeries, etc:
+ * XYSeries origalSeries = ...;
+ *
+ * // Sampled series with half the resolution of the
+ * EditableXYSeries sampledSeries = new FixedSizeEditableXYSeries(
+ * origalSeries.getTitle(), origalSeries.size() / 2);
+ *
+ * // does the actual sampling:
+ * new LTTBSampler().run(origalSeries, sampledSeries);
+ * }
+ *
+ */
+public class LTTBSampler implements Sampler {
+
+ public RectRegion run(XYSeries rawData, EditableXYSeries sampled) {
+ RectRegion bounds = new RectRegion();
+ final int threshold = sampled.size();
+ final int dataLength = rawData.size();
+ final int startIndex = 0;
+
+ if (threshold >= dataLength || threshold == 0) {
+ //return data; // Nothing to do
+ // TODO: set flag to return raw data
+ throw new RuntimeException("Shouldnt be here!");
+ }
+
+ int sampledIndex = 0;
+ // Bucket size. Leave room for start and end data points
+ final double bucketSize = (double) (dataLength - 2) / (threshold - 2);
+ int a = 0; // Initially a is the first point in the triangle
+ int nextA = 0;
+ setSample(rawData, sampled, a + startIndex, sampledIndex, bounds);
+ sampledIndex++;
+ for (int i = 0; i < threshold - 2; i++) {
+ // Calculate point average for next bucket (containing c)
+ double pointCX = 0;
+ double pointCY = 0;
+ int pointCStart = (int) Math.floor((i + 1) * bucketSize) + 1;
+ int pointCEnd = (int) Math.floor((i + 2) * bucketSize) + 1;
+ pointCEnd = pointCEnd < dataLength ? pointCEnd : dataLength;
+ final int pointCSize = pointCEnd - pointCStart;
+ for (; pointCStart < pointCEnd; pointCStart++) {
+ if(rawData.getX(pointCStart + startIndex) != null) {
+ pointCX += rawData.getX(pointCStart + startIndex).doubleValue();
+ }
+
+ if(rawData.getY(pointCStart + startIndex) != null) {
+ pointCY += rawData.getY(pointCStart + startIndex).doubleValue();
+ }
+ }
+ pointCX /= pointCSize;
+ pointCY /= pointCSize;
+ double pointAX = rawData.getX(a + startIndex).doubleValue();
+ double pointAY = rawData.getY(a + startIndex).doubleValue();
+ // Get the range for bucket b
+ int pointBStart = (int) Math.floor((i + 0) * bucketSize) + 1;
+ final int pointBEnd = (int) Math.floor((i + 1) * bucketSize) + 1;
+ double maxArea = -1;
+ XYCoords maxAreaPoint = null;
+ for (; pointBStart < pointBEnd; pointBStart++) {
+ final double area = Math.abs((pointAX - pointCX) * (rawData.getY(pointBStart + startIndex)
+ .doubleValue() - pointAY) - (pointAX - rawData.getX(pointBStart + startIndex)
+ .doubleValue())
+ * (pointCY - pointAY)) * 0.5;
+ if (area > maxArea) {
+ if(rawData.getY(pointBStart + startIndex) == null) {
+ Log.i("LTTB", "Null value encountered in raw data at index: " + pointBStart);
+ }
+ maxArea = area;
+ maxAreaPoint = new XYCoords(rawData.getX(pointBStart + startIndex),
+ rawData.getY(pointBStart + startIndex));
+ nextA = pointBStart; // Next a is this b
+ }
+ }
+ setSample(sampled, maxAreaPoint.x, maxAreaPoint.y, sampledIndex, bounds);
+ sampledIndex++;
+ a = nextA; // This a is the next a (chosen b)
+ }
+ setSample(rawData, sampled, (dataLength + startIndex) - 1, sampledIndex, bounds);
+ sampledIndex++;
+ return bounds;
+ }
+
+ protected void setSample(XYSeries raw, EditableXYSeries sampled, int rawIndex, int sampleIndex, RectRegion bounds) {
+ setSample(sampled, raw.getX(rawIndex), raw.getY(rawIndex), sampleIndex, bounds);
+ }
+
+ protected void setSample(EditableXYSeries sampled, Number x, Number y, int sampleIndex, RectRegion bounds) {
+ bounds.union(x, y);
+ sampled.setX(x, sampleIndex);
+ sampled.setY(y, sampleIndex);
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java
index 75d22119..f33501d2 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java
@@ -129,7 +129,7 @@ public boolean hasLinePaint() {
/**
* Get the {@link Paint} used to draw lines. Will instantiate and a new default instance
- * if it is currently null. To check whether or not line paint has been set, use
+ * if it is currently null. To run whether or not line paint has been set, use
* {@link #hasLinePaint()}.
* @return
*/
@@ -154,7 +154,7 @@ public boolean hasVertexPaint() {
/**
* Get the {@link Paint} used to draw vertices (points). Will instantiate and a new default instance
- * if it is currently null. To check whether or not vertex paint has been set, use
+ * if it is currently null. To run whether or not vertex paint has been set, use
* {@link #hasVertexPaint()}.
* @return
*/
@@ -178,7 +178,7 @@ public boolean hasFillPaint() {
}
/**
* Get the {@link Paint} used to fill series areas. Will instantiate and a new default instance
- * if it is currently null. To check whether or not fill paint has been set, use
+ * if it is currently null. To run whether or not fill paint has been set, use
* {@link #hasFillPaint()}.
* @return
*/
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
index 5f77e0ac..f50d6186 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
@@ -16,12 +16,21 @@
package com.androidplot.xy;
-import android.graphics.*;
-import com.androidplot.exception.PlotRenderException;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import com.androidplot.Plot;
+import com.androidplot.PlotListener;
+import com.androidplot.Region;
import com.androidplot.ui.RenderStack;
+import com.androidplot.util.*;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Renders a point as a line with the vertices marked. Requires 2 or more points to
@@ -32,12 +41,28 @@ public class LineAndPointRenderer e
protected static final int ZERO = 0;
protected static final int ONE = 1;
+ private final Path path = new Path();
+
+ protected final ConcurrentHashMap> pointsCaches
+ = new ConcurrentHashMap<>(2, 0.75f, 2);
+
public LineAndPointRenderer(XYPlot plot) {
super(plot);
+ plot.addListener(new PlotListener() {
+ @Override
+ public void onBeforeDraw(Plot source, Canvas canvas) {
+ cullPointsCache();
+ }
+
+ @Override
+ public void onAfterDraw(Plot source, Canvas canvas) {
+
+ }
+ });
}
@Override
- public void onRender(Canvas canvas, RectF plotArea, XYSeries series, FormatterType formatter, RenderStack stack) throws PlotRenderException {
+ public void onRender(Canvas canvas, RectF plotArea, XYSeries series, FormatterType formatter, RenderStack stack) {
drawSeries(canvas, plotArea, series, formatter);
}
@@ -68,23 +93,77 @@ protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) {
path.lineTo(thisPoint.x, thisPoint.y);
}
+ /**
+ * Retrieves or initializes a list for storing calculated screen-coords to render as points.
+ * Also handles automatic resizing and culling of unused caches.
+ * Should only be called once per render cycle.
+ * @param series
+ * @return
+ */
+ protected ArrayList getPointsCache(XYSeries series) {
+ ArrayList pointsCache = pointsCaches.get(series);
+ final int seriesSize = series.size();
+ if(pointsCache == null) {
+ pointsCache = new ArrayList<>(seriesSize);
+ pointsCaches.put(series, pointsCache);
+ }
+
+ if(pointsCache.size() < seriesSize) {
+ while(pointsCache.size() < seriesSize) {
+ pointsCache.add(null);
+ }
+ } else if(pointsCache.size() > seriesSize) {
+ while(pointsCache.size() > seriesSize) {
+ pointsCache.remove(0);
+ }
+ }
+ return pointsCache;
+ }
+
+ protected void cullPointsCache() {
+ for(XYSeries series : pointsCaches.keySet()) {
+ if(!getPlot().getRegistry().contains(series, LineAndPointFormatter.class)) {
+ pointsCaches.remove(series);
+ }
+ }
+ }
protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) {
PointF thisPoint;
PointF lastPoint = null;
PointF firstPoint = null;
- //Paint linePaint = formatter.getLinePaint();
- Path path = null;
- ArrayList points = new ArrayList<>(series.size());
- for (int i = 0; i < series.size(); i++) {
- Number y = series.getY(i);
- Number x = series.getX(i);
+ path.reset();
+ final List points = getPointsCache(series);
+
+ int iStart = 0;
+ int iEnd = series.size();
+ if(SeriesUtils.getXYOrder(series) == OrderedXYSeries.XOrder.ASCENDING) {
+ final Region iBounds = SeriesUtils.iBounds(series, getPlot().getBounds());
+ iStart = iBounds.getMin().intValue();
+ if(iStart > 0) {
+ iStart--;
+ }
+ iEnd = iBounds.getMax().intValue() + 1;
+ if(iEnd < series.size() - 1) {
+ iEnd++;
+ }
+ }
+ for (int i = iStart; i < iEnd; i++) {
+ final Number y = series.getY(i);
+ final Number x = series.getX(i);
+ PointF iPoint = points.get(i);
if (y != null && x != null) {
- thisPoint = getPlot().getBounds().transformScreen(x, y, plotArea);
- points.add(thisPoint);
+ if(iPoint == null) {
+ iPoint = new PointF();
+ points.set(i, iPoint);
+ }
+ thisPoint = iPoint;
+ getPlot().getBounds().transformScreen(thisPoint, x, y, plotArea);
} else {
thisPoint = null;
+ iPoint = null;
+ points.set(i, iPoint);
}
// don't need to do any of this if the line isnt going to be drawn:
@@ -93,7 +172,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn
// record the first point of the new Path
if (firstPoint == null) {
- path = new Path();
+ path.reset();
firstPoint = thisPoint;
// create our first point at the bottom/x position so filling will look good:
@@ -114,6 +193,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn
}
}
}
+
if(formatter.hasLinePaint()) {
if(formatter.getInterpolationParams() != null) {
List interpolatedPoints = getInterpolator(
@@ -121,7 +201,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn
formatter.getInterpolationParams());
firstPoint = convertPoint(interpolatedPoints.get(ZERO), plotArea);
lastPoint = convertPoint(interpolatedPoints.get(interpolatedPoints.size()-ONE), plotArea);
- path = new Path();
+ path.reset();
path.moveTo(firstPoint.x, firstPoint.y);
for(int i = 1; i < interpolatedPoints.size(); i++) {
thisPoint = convertPoint(interpolatedPoints.get(i), plotArea);
@@ -133,7 +213,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn
renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter);
}
}
- renderPoints(canvas, plotArea, series, points, formatter);
+ renderPoints(canvas, plotArea, series, iStart, iEnd, points, formatter);
}
/**
@@ -155,26 +235,28 @@ protected PointF convertPoint(XYCoords coord, RectF plotArea) {
return getPlot().getBounds().transformScreen(coord, plotArea);
}
- protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, List points,
+ protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, int iStart, int iEnd, List points,
LineAndPointFormatter formatter) {
- //PointLabelFormatter plf = formatter.getPointLabelFormatter();
if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) {
- int i = 0;
- for (PointF p : points) {
- PointLabeler pointLabeler = formatter.getPointLabeler();
-
- // if vertexPaint is available, draw vertex:
- if (formatter.hasVertexPaint()) {
- canvas.drawPoint(p.x, p.y, formatter.getVertexPaint());
- }
+ 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 (formatter.hasPointLabelFormatter() && pointLabeler != null) {
- final PointLabelFormatter plf = formatter.getPointLabelFormatter();
- canvas.drawText(pointLabeler.getLabel(series, i),
- p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint());
+ // if textPaint and pointLabeler are available, draw point's text label:
+ if (pointLabeler != null) {
+ canvas.drawText(pointLabeler.getLabel(series, i),
+ p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint());
+ }
}
- i++;
}
}
}
@@ -226,7 +308,7 @@ protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF first
RectF thisRegionRectF = thisRegionTransformed.asRectF();
if (thisRegionRectF != null) {
try {
- canvas.save(Canvas.ALL_SAVE_FLAG);
+ canvas.save();
canvas.clipPath(path);
canvas.drawRect(thisRegionRectF, regionFormatter.getPaint());
} finally {
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java
new file mode 100644
index 00000000..03760b39
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java
@@ -0,0 +1,136 @@
+package com.androidplot.xy;
+
+import com.androidplot.Region;
+import com.androidplot.util.SeriesUtils;
+
+/**
+ * Wrapper implementation of {@link XYSeries} that wraps another XYSeries, normalizing values in the range of 0 to 1.
+ * Note that it's possible to push normed values outside of the standard 0, 1 range by applying
+ * a sufficiently large offset.
+ */
+public class NormedXYSeries implements XYSeries {
+
+ private XYSeries rawData;
+
+ private Region minMaxX;
+ private Region minMaxY;
+
+ private Region transformX;
+ private Region transformY;
+
+ public static class Norm {
+
+ final Region minMax;
+ final double offset;
+ final boolean useOffsetCompression;
+
+ public Norm(Region minMax) {
+ this(minMax, 0, false);
+ }
+
+ /**
+ *
+ * @param minMax Boundary to use when calculating the norm coefficient. Set to null to let
+ * Androidplot auto calculate the bounds. (Very inefficient)
+ * @param offset An extra offset to apply, generally within the range of -1 and 1.
+ * This value is useful for adjusting the positioning of a series relative to another normalized series.
+ * @param useOffsetCompression If true, the offset value will result in further scaling down
+ * of the series data in order to ensure that all points within the specified bounds remain
+ * visible on the screen. If set to true, the specified offset MUST be > -1 and < 1. Will be
+ * ignored if bounds != null.
+ */
+ public Norm(Region minMax, double offset, boolean useOffsetCompression) {
+ this.minMax = minMax;
+ this.offset = offset;
+ this.useOffsetCompression = useOffsetCompression;
+
+ if (useOffsetCompression && (offset <= -1 || offset >= 1)) {
+ throw new IllegalArgumentException(
+ "When useOffsetCompression is true, offset must be > -1 and < 1.");
+ }
+ }
+ }
+
+ /**
+ * Normalizes yVals only, auto calculating min/max.
+ * @param rawData
+ */
+ public NormedXYSeries(XYSeries rawData) {
+ this(rawData, null, new Norm(null, 0, false));
+ }
+
+ /**
+ *
+ * @param rawData The XYSeries to be normalized.
+ * @param x Normalization to apply to xVals. Set to null to disable normalization on the x axis.
+ * @param y Normalization to apply to yVals. Set to null to disable normalization on the y axis.
+ */
+ public NormedXYSeries(XYSeries rawData, Norm x, Norm y) {
+ this.rawData = rawData;
+ normalize(x, y);
+ }
+
+ protected void normalize(Norm x, Norm y) {
+ if( x != null) {
+ this.minMaxX = x.minMax != null ? x.minMax : SeriesUtils.minMaxX(rawData);
+ this.transformX = calculateTransform(x);
+ }
+
+ if( y != null) {
+ this.minMaxY = y.minMax != null ? y.minMax : SeriesUtils.minMaxY(rawData);
+ this.transformY = calculateTransform(y);
+ }
+ }
+
+ protected Region calculateTransform(Norm norm) {
+ if(norm.useOffsetCompression) {
+ return new Region(
+ norm.offset > 0 ? norm.offset : 0,
+ norm.offset < 0 ? 1 + norm.offset : 1);
+ } else {
+ return new Region(0 + norm.offset, 1 + norm.offset);
+ }
+ }
+
+ @Override
+ public String getTitle() {
+ return rawData.getTitle();
+ }
+
+ @Override
+ public int size() {
+ return rawData.size();
+ }
+
+ public Number denormalizeXVal(Number xVal) {
+ if(xVal != null) {
+ return transformX.transform(xVal.doubleValue(), minMaxX);
+ }
+ return null;
+ }
+
+ public Number denormalizeYVal(Number yVal) {
+ if(yVal != null) {
+ return transformY.transform(yVal.doubleValue(), minMaxY);
+ }
+ return null;
+ }
+
+ @Override
+ public Number getX(int index) {
+ final Number xVal = rawData.getX(index);
+ if(xVal != null && transformX != null) {
+ return minMaxX.transform(xVal.doubleValue(), transformX);
+ }
+ return xVal;
+ }
+
+ @Override
+ public Number getY(int index) {
+ final Number yVal = rawData.getY(index);
+ if(yVal != null && transformY != null) {
+ return minMaxY.transform(yVal.doubleValue(), transformY);
+ }
+ return yVal;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java
new file mode 100644
index 00000000..820e4330
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java
@@ -0,0 +1,33 @@
+package com.androidplot.xy;
+
+/**
+ * An implementation of {@link XYSeries} that gives hints to it's renderer about the order
+ * of the data being rendered.
+ */
+public interface OrderedXYSeries extends XYSeries {
+
+ enum XOrder {
+ /**
+ * XVals are in strict ascending order such that:
+ * x(i) < x(i+1) == true
+ */
+ ASCENDING,
+
+ /**
+ * XVals are in strict descending order such that:
+ * x(i) > x(i+1) == true
+ */
+ DESCENDING,
+
+ /**
+ * XVals appear in no particular order.
+ */
+ NONE
+ }
+
+ /**
+ * The order of XVals as they appear in this series.
+ * @return
+ */
+ XOrder getXOrder();
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java
index ff9435f2..9cd80847 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java
@@ -1,55 +1,65 @@
package com.androidplot.xy;
-import android.graphics.*;
+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 float minXLimit = Float.MAX_VALUE;
- private float maxXLimit = Float.MAX_VALUE;
- private float minYLimit = Float.MAX_VALUE;
- private float maxYLimit = Float.MAX_VALUE;
- private float lastMinX = Float.MAX_VALUE;
- private float lastMaxX = Float.MAX_VALUE;
- private float lastMinY = Float.MAX_VALUE;
- private float lastMaxY = Float.MAX_VALUE;
private PointF firstFingerPos;
// rectangle created by the space between two fingers
- private RectF dist;
- private boolean mCalledBySelf;
+ protected RectF fingersRect;
private View.OnTouchListener delegate;
+ private State state = new State();
// Definition of the touch states
- protected enum DragState
- {
+ 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
*/
@@ -72,25 +82,125 @@ public enum Zoom {
SCALE
}
- protected PanZoom(XYPlot plot, Pan pan, Zoom zoom) {
+ /**
+ * Limits imposed on the zoom.
+ */
+ public enum ZoomLimit {
+ /**
+ * Do not zoom outside the plots outer bounds, if they are defined.
+ */
+ OUTER,
+
+ /**
+ * Additionally to the outer bounds if plot.StepModel defines a value based increment
+ * make sure at least one tick is visible by not zooming in further.
+ */
+ MIN_TICKS
+ }
+
+ // TODO: consider making this immutable / threadsafe
+ public static class State implements Serializable {
+ private Number domainLowerBoundary;
+ private Number domainUpperBoundary;
+ private Number rangeLowerBoundary;
+ private Number rangeUpperBoundary;
+ private BoundaryMode domainBoundaryMode;
+ private BoundaryMode rangeBoundaryMode;
+
+ public void setDomainBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
+ this.domainLowerBoundary = lowerBoundary;
+ this.domainUpperBoundary = upperBoundary;
+ this.domainBoundaryMode = mode;
+ }
+
+ public void setRangeBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
+ this.rangeLowerBoundary = lowerBoundary;
+ this.rangeUpperBoundary = upperBoundary;
+ this.rangeBoundaryMode = mode;
+ }
+
+ public void applyDomainBoundaries(@NonNull XYPlot plot) {
+ plot.setDomainBoundaries(domainLowerBoundary, domainUpperBoundary, domainBoundaryMode);
+ }
+
+ public void applyRangeBoundaries(@NonNull XYPlot plot) {
+ plot.setRangeBoundaries(rangeLowerBoundary, rangeUpperBoundary, rangeBoundaryMode);
+ }
+
+ public void apply(@NonNull XYPlot plot) {
+ applyDomainBoundaries(plot);
+ applyRangeBoundaries(plot);
+ }
+ }
+
+ protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom) {
+ this.plot = plot;
+ this.pan = pan;
+ this.zoom = zoom;
+ this.zoomLimit = ZoomLimit.OUTER;
+ }
+
+ // additional constructor not to break api
+ protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom, ZoomLimit limit) {
this.plot = plot;
this.pan = pan;
this.zoom = zoom;
+ this.zoomLimit = limit;
+ }
+
+ public State getState() {
+ return this.state;
+ }
+
+ public void setState(@NonNull State state) {
+ this.state = state;
+ state.apply(plot);
+ }
+
+ protected void adjustRangeBoundary(Number lower, Number upper, BoundaryMode mode) {
+ state.setRangeBoundaries(lower, upper, mode);
+ state.applyRangeBoundaries(plot);
+ }
+
+ protected void adjustDomainBoundary(Number lower, Number upper, BoundaryMode mode) {
+ state.setDomainBoundaries(lower, upper, mode);
+ state.applyDomainBoundaries(plot);
}
/**
* Convenience method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using
* a default behavior of {@link Pan#BOTH} and {@link Zoom#SCALE}.
- * Use {@link PanZoom#attach(XYPlot, Pan, Zoom)} for finer grain control of this behavior.
+ * Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior.
* @param plot
* @return
*/
- public static PanZoom attach(XYPlot plot) {
+ public static PanZoom attach(@NonNull XYPlot plot) {
return attach(plot, Pan.BOTH, Zoom.SCALE);
}
- public static PanZoom attach(XYPlot plot, Pan pan, Zoom zoom) {
- PanZoom pz = new PanZoom(plot, pan, zoom);
+ /**
+ * Old method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using
+ * the default behavior of {@link ZoomLimit#OUTER}.
+ * Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior.
+ * @param plot
+ * @param pan
+ * @param zoom
+ * @return
+ */
+ public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom) {
+ return attach(plot,pan,zoom, ZoomLimit.OUTER);
+ }
+
+ /**
+ * New method for enabling pan/zoom behavior on an instance of {@link XYPlot}.
+ * @param plot
+ * @param pan
+ * @param zoom
+ * @param limit
+ * @return
+ */
+ public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom, @NonNull ZoomLimit limit) {
+ PanZoom pz = new PanZoom(plot, pan, zoom, limit);
plot.setOnTouchListener(pz);
return pz;
}
@@ -103,55 +213,13 @@ public void setEnabled(boolean enabled) {
isEnabled = enabled;
}
- protected void setDomainBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode,
- final Number upperBoundary, final BoundaryMode upperBoundaryMode) {
- plot.setDomainBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode);
- if(mCalledBySelf) {
- mCalledBySelf = false;
- } else {
- final RectRegion bounds = plot.getBounds();
- minXLimit = lowerBoundaryMode == BoundaryMode.FIXED ?
- lowerBoundary.floatValue() : bounds.getMinX().floatValue();
- maxXLimit = upperBoundaryMode == BoundaryMode.FIXED ?
- upperBoundary.floatValue() : bounds.getMaxX().floatValue();
- lastMinX = minXLimit;
- lastMaxX = maxXLimit;
- }
- }
-
- protected void setRangeBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode,
- final Number upperBoundary, final BoundaryMode upperBoundaryMode) {
- plot.setRangeBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode);
- if(mCalledBySelf) {
- mCalledBySelf = false;
- } else {
- final RectRegion bounds = plot.getBounds();
- minYLimit = lowerBoundaryMode == BoundaryMode.FIXED ?
- lowerBoundary.floatValue() : bounds.getMinY().floatValue();
- maxYLimit = upperBoundaryMode == BoundaryMode.FIXED ?
- upperBoundary.floatValue() : bounds.getMaxY().floatValue();
- lastMinY = minYLimit;
- lastMaxY = maxYLimit;
- }
- }
-
- protected void setDomainBoundaries(final Number lowerBoundary,
-final Number upperBoundary, final BoundaryMode mode) {
- plot.setDomainBoundaries(lowerBoundary, mode, upperBoundary, mode);
- }
-
- protected synchronized void setRangeBoundaries(final Number lowerBoundary,
- final Number upperBoundary, final BoundaryMode mode) {
- plot.setRangeBoundaries(lowerBoundary, mode, upperBoundary, mode);
- }
-
@Override
public boolean onTouch(final View view, final MotionEvent event) {
boolean isConsumed = false;
- if(delegate != null) {
+ if (delegate != null) {
isConsumed = delegate.onTouch(view, event);
}
- if(isEnabled() && !isConsumed) {
+ if (isEnabled() && !isConsumed) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: // start gesture
firstFingerPos = new PointF(event.getX(), event.getY());
@@ -159,9 +227,9 @@ public boolean onTouch(final View view, final MotionEvent event) {
break;
case MotionEvent.ACTION_POINTER_DOWN: // second finger
{
- dist = getDistance(event);
- // the distance check is done to avoid false alarms
- if (dist.width() > MIN_DIST_2_FING || dist.width() < -MIN_DIST_2_FING) {
+ 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;
@@ -169,6 +237,7 @@ public boolean onTouch(final View view, final MotionEvent event) {
case MotionEvent.ACTION_POINTER_UP: // end zoom
dragState = DragState.NONE;
break;
+
case MotionEvent.ACTION_MOVE:
if (dragState == DragState.ONE_FINGER) {
pan(event);
@@ -176,6 +245,10 @@ public boolean onTouch(final View view, final MotionEvent event) {
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:
@@ -184,262 +257,225 @@ public boolean onTouch(final View view, final MotionEvent event) {
/**
* Calculates the distance between two finger motion events.
- * @param evt
+ * @param firstFingerX
+ * @param firstFingerY
+ * @param secondFingerX
+ * @param secondFingerY
* @return
*/
- protected RectF getDistance(final MotionEvent evt) {
- float left;
- float right;
- float top;
- float bottom;
- if(evt.getX(0) > evt.getX(1)) {
- left = evt.getX(1);
- right = evt.getX(0);
- } else {
- left = evt.getX(0);
- right = evt.getX(1);
- }
-
- if(evt.getY(0) > evt.getY(1)) {
- top = evt.getY(1);
- bottom = evt.getY(0);
- } else {
- top = evt.getY(0);
- bottom = evt.getY(1);
- }
-
+ 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);
}
- private float getMinXLimit() {
- if(minXLimit == Float.MAX_VALUE) {
- minXLimit = plot.getBounds().getMinX().floatValue();
- lastMinX = minXLimit;
- }
- return minXLimit;
- }
-
- protected float getMaxXLimit() {
- if(maxXLimit == Float.MAX_VALUE) {
- maxXLimit = plot.getBounds().getMaxX().floatValue();
- lastMaxX = maxXLimit;
- }
- return maxXLimit;
- }
-
- protected float getMinYLimit() {
- if(minYLimit == Float.MAX_VALUE) {
- minYLimit = plot.getBounds().getMinY().floatValue();
- lastMinY = minYLimit;
- }
- return minYLimit;
- }
-
- protected float getMaxYLimit() {
- if(maxYLimit == Float.MAX_VALUE) {
- maxYLimit = plot.getBounds().getMaxY().floatValue();
- lastMaxY = maxYLimit;
- }
- return maxYLimit;
- }
-
- protected float getLastMinX() {
- if(lastMinX == Float.MAX_VALUE) {
- lastMinX = plot.getBounds().getMinX().floatValue();
- }
- return lastMinX;
- }
-
- protected float getLastMaxX() {
- if(lastMaxX == Float.MAX_VALUE) {
- lastMaxX = plot.getBounds().getMaxX().floatValue();
- }
- return lastMaxX;
- }
-
- protected float getLastMinY() {
- if(lastMinY == Float.MAX_VALUE) {
- lastMinY = plot.getBounds().getMinY().floatValue();
- }
- return lastMinY;
+ /**
+ * 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));
}
- private float getLastMaxY() {
- if(lastMaxY == Float.MAX_VALUE) {
- lastMaxY = plot.getBounds().getMaxY().floatValue();
+ protected void pan(final MotionEvent motionEvent) {
+ if (pan == Pan.NONE) {
+ return;
}
- return lastMaxY;
- }
- protected void pan(final MotionEvent motionEvent) {
final PointF oldFirstFinger = firstFingerPos; //save old position of finger
firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position
- PointF newX = new PointF();
- if(EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) {
- calculatePan(oldFirstFinger, newX, true);
- mCalledBySelf = true;
- setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
- lastMinX = newX.x;
- lastMaxX = newX.y;
+ 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)) {
- calculatePan(oldFirstFinger, newX, false);
- mCalledBySelf = true;
- setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
- lastMinY = newX.x;
- lastMaxY = newX.y;
+ 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, PointF newX, final boolean horizontal) {
+ 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) {
- newX.x = getLastMinX();
- newX.y = getLastMaxX();
- offset = (oldFirstFinger.x - firstFingerPos.x) * ((newX.y - newX.x) / plot.getWidth());
+ if (horizontal) {
+ bounds.setMinMax(plot.getBounds().getxRegion());
+ offset = (oldFirstFinger.x - firstFingerPos.x) *
+ ((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getWidth());
} else {
- newX.x = getLastMinY();
- newX.y = getLastMaxY();
- offset = -(oldFirstFinger.y - firstFingerPos.y) * ((newX.y - newX.x) / plot.getHeight());
+ bounds.setMinMax(plot.getBounds().getyRegion());
+ offset = -(oldFirstFinger.y - firstFingerPos.y) *
+ ((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getHeight());
}
// move the calculated offset
- newX.x = newX.x + offset;
- newX.y = newX.y + offset;
+ bounds.setMin(bounds.getMin().floatValue() + offset);
+ bounds.setMax(bounds.getMax().floatValue() + offset);
//get the distance between max and min
- final float diff = newX.y - newX.x;
+ final float diff = bounds.length().floatValue();
- //check if we reached the limit of panning
- if(horizontal) {
- if(newX.x < getMinXLimit()) {
- newX.x = getMinXLimit();
- newX.y = newX.x + diff;
+ //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(newX.y > getMaxXLimit()) {
- newX.y = getMaxXLimit();
- newX.x = newX.y - diff;
+ if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxX().floatValue()) {
+ bounds.setMax(plot.getOuterLimits().getMaxX());
+ bounds.setMin(bounds.getMax().floatValue() - diff);
}
- } else {
- if(newX.x < getMinYLimit()) {
- newX.x = getMinYLimit();
- newX.y = newX.x + 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(newX.y > getMaxYLimit()) {
- newX.y = getMaxYLimit();
- newX.x = newX.y - 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) {
- if(Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) {
- return false;
- }
- return true;
+ return !Float.isInfinite(scale)
+ && !Float.isNaN(scale)
+ && (!(scale > -0.001) || !(scale < 0.001));
}
protected void zoom(final MotionEvent motionEvent) {
- final RectF oldDist = dist;
- final RectF newDist = getDistance(motionEvent);
- dist = newDist;
+ 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) {
+ switch (zoom) {
case STRETCH_HORIZONTAL:
- scaleX = oldDist.width() / dist.width();
- if(!isValidScale(scaleX)) {
+ scaleX = oldFingersRect.width() / getFingersRect().width();
+ if (!isValidScale(scaleX)) {
return;
}
break;
case STRETCH_VERTICAL:
- scaleY = oldDist.height() / dist.height();
- if(!isValidScale(scaleY)) {
+ scaleY = oldFingersRect.height() / getFingersRect().height();
+ if (!isValidScale(scaleY)) {
return;
}
break;
case STRETCH_BOTH:
- scaleX = oldDist.width() / dist.width();
- scaleY = oldDist.height() / dist.height();
- if(!isValidScale(scaleX) || !isValidScale(scaleY)) {
+ scaleX = oldFingersRect.width() / getFingersRect().width();
+ scaleY = oldFingersRect.height() / getFingersRect().height();
+ if (!isValidScale(scaleX) || !isValidScale(scaleY)) {
return;
}
break;
case SCALE:
- scaleX = oldDist.width() / dist.width();
- scaleY = oldDist.height() / dist.height();
-
- // use the greater value to scale each axis evenly:
- if(scaleX > scaleY) {
- scaleY = scaleX;
- } else {
- scaleX = scaleY;
- }
- if(!isValidScale(scaleX) || !isValidScale(scaleY)) {
+ 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(
+ if (EnumSet.of(
Zoom.STRETCH_HORIZONTAL,
Zoom.STRETCH_BOTH,
Zoom.SCALE).contains(zoom)) {
calculateZoom(newRect, scaleX, true);
- mCalledBySelf = true;
- setDomainBoundaries(newRect.left, newRect.right, BoundaryMode.FIXED);
- lastMinX = newRect.left;
- lastMaxX = newRect.right;
+ adjustDomainBoundary(newRect.left, newRect.right, BoundaryMode.FIXED);
}
- if(EnumSet.of(
+ if (EnumSet.of(
Zoom.STRETCH_VERTICAL,
Zoom.STRETCH_BOTH,
Zoom.SCALE).contains(zoom)) {
calculateZoom(newRect, scaleY, false);
- mCalledBySelf = true;
- setRangeBoundaries(newRect.top, newRect.bottom, BoundaryMode.FIXED);
- lastMinY = newRect.top;
- lastMaxY = newRect.bottom;
+ 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;
- if(isHorizontal) {
- calcMax = getLastMaxX();
- span = calcMax - getLastMinX();
+ final RectRegion bounds = plot.getBounds();
+ if (isHorizontal) {
+ calcMax = bounds.getMaxX().floatValue();
+ span = calcMax - bounds.getMinX().floatValue();
} else {
- calcMax = getLastMaxY();
- span = calcMax - getLastMinY();
+ calcMax = bounds.getMaxY().floatValue();
+ span = calcMax - bounds.getMinY().floatValue();
}
final float midPoint = calcMax - (span / 2.0f);
- final float offset = span * scale / 2.0f;
+ float offset = span * scale / 2.0f;
+ final RectRegion limits = plot.getOuterLimits();
+
+ if (isHorizontal ) {
+ // 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);
+ }
+ }
- if(isHorizontal) {
newRect.left = midPoint - offset;
newRect.right = midPoint + offset;
- if(newRect.left < getMinXLimit()) {
- newRect.left = getMinXLimit();
- }
- if(newRect.right > getMaxXLimit()) {
- newRect.right = getMaxXLimit();
+ 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(newRect.top < getMinYLimit()) {
- newRect.top = getMinYLimit();
- }
- if(newRect.bottom > getMaxYLimit()) {
- newRect.bottom = getMaxYLimit();
+ 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();
+ }
}
}
}
@@ -460,6 +496,14 @@ public void setZoom(Zoom zoom) {
this.zoom = zoom;
}
+ public ZoomLimit getZoomLimit() {
+ return zoomLimit;
+ }
+
+ public void setZoomLimit(ZoomLimit zoomLimit) {
+ this.zoomLimit = zoomLimit;
+ }
+
public View.OnTouchListener getDelegate() {
return delegate;
}
@@ -474,4 +518,18 @@ public View.OnTouchListener getDelegate() {
public void setDelegate(View.OnTouchListener delegate) {
this.delegate = delegate;
}
+
+ public void reset() {
+ this.firstFingerPos = null;
+ setFingersRect(null);
+ this.setFingersRect(null);
+ }
+
+ protected RectF getFingersRect() {
+ return fingersRect;
+ }
+
+ protected void setFingersRect(RectF fingersRect) {
+ this.fingersRect = fingersRect;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
index e7a4b09e..0bb8988d 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
@@ -20,6 +20,7 @@
import android.graphics.RectF;
import com.androidplot.Region;
+
import java.util.ArrayList;
import java.util.List;
@@ -118,10 +119,19 @@ 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) {
- float xx = (float) xRegion.transform(x.doubleValue(), region2.left, region2.right, flipX);
- float yy = (float) yRegion.transform(y.doubleValue(), region2.top, region2.bottom, flipY);
- return new PointF(xx, yy);
+ PointF result = new PointF();
+ transform(result, x, y, region2, flipX, flipY);
+ return result;
}
public PointF transformScreen(XYCoords value, RectF region2) {
@@ -141,6 +151,11 @@ public PointF transform(XYCoords value, RectF region2, boolean flipX, boolean fl
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.
@@ -230,10 +245,17 @@ public Number getHeight() {
* @param y
* @return
*/
- private Number distanceBetween(Number x, Number y) {
+ private static Number distanceBetween(Number x, Number y) {
return Math.abs(x.doubleValue() - y.doubleValue());
}
+ public void set(Number minX, Number maxX, Number minY, Number maxY) {
+ setMinX(minX);
+ setMaxX(maxX);
+ setMinY(minY);
+ setMaxY(maxY);
+ }
+
public boolean isMinXSet() {
return xRegion.isMinSet();
}
@@ -313,4 +335,23 @@ public void setyRegion(Region yRegion) {
public boolean isFullyDefined() {
return xRegion.isDefined() && yRegion.isDefined();
}
+
+ /**
+ * True if this region contains the specified coordinates.
+ * @param x
+ * @param y
+ * @return
+ */
+ public boolean contains(Number x, Number y) {
+ return getxRegion().contains(x) && getyRegion().contains(y);
+ }
+
+ @Override
+ public String toString() {
+ return "RectRegion{" +
+ "xRegion=" + xRegion +
+ ", yRegion=" + yRegion +
+ ", label='" + label + '\'' +
+ '}';
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java
new file mode 100644
index 00000000..f795536b
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java
@@ -0,0 +1,216 @@
+package com.androidplot.xy;
+
+import com.androidplot.*;
+import com.androidplot.util.SeriesUtils;
+
+import java.util.*;
+
+/**
+ * An implementation of {@link FastXYSeries} that samples its self into multiple levels to
+ * achieve faster rendering / zoom behavior. By default, uses {@link LTTBSampler} as
+ * it's sampling algorithm. Note that this algorithm does not yet support null values.
+ *
+ * Sampling behavior is controlled by two values:
+ * Ratio: A value greater than 1; controls the sampling ratio of each successive series. For example,
+ * a step of 2 would mean that each successive series contains 2x fewer points than the previous.
+ *
+ * Threshold: A value < the original series size; controls the lower limit at which point sampling should stop.
+ * For example, sampling a series with size 1000 given a ratio of 2 and a threshold of 100,
+ * three sampled resolutions will be generated:
+ *
+ * 500 - 2x sampling
+ * 250 - 4x sampling
+ * 125 - 8x sampling
+ *
+ */
+public class SampledXYSeries implements FastXYSeries, OrderedXYSeries {
+ private int threshold;
+ private Sampler algorithm = new LTTBSampler();
+
+ private XYSeries rawData;
+
+ private List zoomLevels;
+
+ private XYSeries activeSeries;
+
+ private RectRegion bounds;
+ private Exception lastResamplingException;
+
+ private final XOrder xOrder;
+ private float ratio;
+
+ /**
+ *
+ * @param rawData
+ * @param xOrder If your data is in ascending or descending order, specifying it here speed up
+ * optimize render times.
+ * @param ratio The ratio used to determine the size of each new sampled series. Must be > 1.
+ * downsampled series until threshold is reached.
+ * @param threshold The desired size of the smallest sample series. Must be < rawData.size.
+ */
+ public SampledXYSeries(XYSeries rawData, XOrder xOrder, float ratio, int threshold) {
+ this.rawData = rawData;
+ this.xOrder = xOrder;
+ this.setRatio(ratio);
+ this.setThreshold(threshold);
+ resample();
+ }
+
+ /**
+ * Generate a SampledXYSeries from the input series.
+ * @param rawData The original series to be downsampled
+ * @param ratio The ratio used to determine the size of each new sampled series. Must be > 1.
+ * downsampled series until threshold is reached.
+ * @param threshold The desired size of the smallest sample series. Must be < rawData.size.
+ */
+ public SampledXYSeries(XYSeries rawData, float ratio, int threshold) {
+ this(rawData, SeriesUtils.getXYOrder(rawData), ratio, threshold);
+ }
+
+ public void resample() {
+ bounds = null;
+ zoomLevels = new ArrayList<>();
+ int t = (int) Math.ceil(rawData.size() / getRatio());
+ List threads = new ArrayList<>(zoomLevels.size());
+ while (t > threshold) {
+ final int thisThreshold = t;
+ final EditableXYSeries thisSeries = new FixedSizeEditableXYSeries(getTitle(), thisThreshold);
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // TODO: make bounds a param to prevent calculating on each setZoomFactor level:
+ RectRegion b = getAlgorithm()
+ .run(rawData, thisSeries);
+ if (bounds == null) {
+ bounds = b;
+ }
+ } catch(Exception ex) {
+ lastResamplingException = ex;
+ }
+ }
+ }, "Androidplot XY Series Sampler");
+ getZoomLevels().add(thisSeries);
+ threads.add(thread);
+ thread.start();
+
+ t = (int) Math.ceil(t / getRatio());
+ }
+ for(Thread thread : threads) {
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if(lastResamplingException != null) {
+ throw new RuntimeException("Exception encountered during resampling", lastResamplingException);
+ }
+
+ }
+
+ protected List getZoomLevels() {
+ return this.zoomLevels;
+ }
+
+ /**
+ * Set zoom factor; 2.5 = 2.5x zoom, 10.0 = 10x zoom etc. This method will set the zoom level
+ * to the closest available factor to the specified factor; a specified factor of 4.5x may result
+ * in an actual factor of 4x.
+ * @param factor
+ */
+ public void setZoomFactor(double factor) {
+ if(factor <= 1) {
+ activeSeries = rawData;
+ } else {
+ //int i = (int) Math.round(Math.sqrt(factor) - 1);
+ int i = getZoomIndex(factor, getRatio());
+ if (i < zoomLevels.size()) {
+ activeSeries = zoomLevels.get(i);
+ } else {
+ activeSeries = zoomLevels.get(zoomLevels.size() - 1);
+ }
+ }
+ }
+
+ protected static int getZoomIndex(double zoomFactor, double ratio) {
+ final double lhs = Math.log(zoomFactor);
+ final double rhs = Math.log(ratio);
+ final double log = lhs / rhs;
+ final int index = (int) Math.round(log);
+ return index > 0 ? index - 1 : 0;
+ }
+
+ public double getMaxZoomFactor() {
+ return Math.pow(getRatio(), zoomLevels.size());
+ }
+
+ public Sampler getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(Sampler algorithm) {
+ this.algorithm = algorithm;
+ resample();
+ }
+
+ @Override
+ public String getTitle() {
+ return rawData.getTitle();
+ }
+
+ @Override
+ public int size() {
+ return activeSeries.size();
+ }
+
+ @Override
+ public Number getX(int index) {
+ return activeSeries.getX(index);
+ }
+
+ @Override
+ public Number getY(int index) {
+ return activeSeries.getY(index);
+ }
+
+ public int getThreshold() {
+ return threshold;
+ }
+
+ public void setThreshold(int threshold) {
+ if(threshold >= rawData.size()) {
+ throw new IllegalArgumentException("Threshold must be < original series size.");
+ }
+ this.threshold = threshold;
+ }
+
+ public RectRegion getBounds() {
+ return bounds;
+ }
+
+ public void setBounds(RectRegion bounds) {
+ this.bounds = bounds;
+ }
+
+ @Override
+ public RectRegion minMax() {
+ return bounds;
+ }
+
+ @Override
+ public XOrder getXOrder() {
+ return xOrder;
+ }
+
+ public float getRatio() {
+ return ratio;
+ }
+
+ public void setRatio(float ratio) {
+ if(ratio <= 1) {
+ throw new IllegalArgumentException("Ratio must be greater than 1");
+ }
+ this.ratio = ratio;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java b/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java
new file mode 100644
index 00000000..ebbc8238
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java
@@ -0,0 +1,16 @@
+package com.androidplot.xy;
+
+/**
+ * An algorithm used to to resample a larger set of data into a smaller set.
+ */
+public interface Sampler {
+
+ /**
+ *
+ * @param input The original unsampled series
+ * @param output The destination series to contain sampled result.
+ * This series size should be set to the desired sampled size.
+ * @return min/max values encountered while processing input.
+ */
+ RectRegion run(XYSeries input, EditableXYSeries output);
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java
new file mode 100644
index 00000000..2e194f17
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java
@@ -0,0 +1,67 @@
+package com.androidplot.xy;
+
+/**
+ * Wraps an existing {@link XYSeries} allowing easy scaling of that series' xy values.
+ */
+public class ScalingXYSeries implements XYSeries {
+
+ private double scale;
+ private XYSeries series;
+ private Mode mode;
+
+ public enum Mode {
+ X_ONLY,
+ Y_ONLY,
+ X_AND_Y
+ }
+
+ /**
+ *
+ * @param series The {@link XYSeries} to be scaled
+ * @param scale The initial scale to be applied
+ * @param mode Determines which axis (or both) to which scaling will be applied.
+ */
+ public ScalingXYSeries(XYSeries series, double scale, Mode mode) {
+ this.series = series;
+ this.scale = scale;
+ this.mode = mode;
+ }
+
+ @Override
+ public String getTitle() {
+ return series.getTitle();
+ }
+
+ @Override
+ public int size() {
+ return series.size();
+ }
+
+ @Override
+ public Number getX(int index) {
+ Number x = series.getX(index);
+ if(mode == Mode.X_ONLY || mode == Mode.X_AND_Y) {
+ return x == null ? null : x.doubleValue() * scale;
+ } else {
+ return x;
+ }
+ }
+
+ @Override
+ public Number getY(int index) {
+ Number y = series.getY(index);
+ if(mode == Mode.Y_ONLY || mode == Mode.X_AND_Y) {
+ return y == null ? null : y.doubleValue() * scale;
+ } else {
+ return y;
+ }
+ }
+
+ public double getScale() {
+ return scale;
+ }
+
+ public void setScale(double scale) {
+ this.scale = scale;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java
index 1763bc20..b9178123 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java
@@ -17,41 +17,34 @@
package com.androidplot.xy;
import android.graphics.Canvas;
+
import com.androidplot.Plot;
import com.androidplot.PlotListener;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* A convenience class used to create instances of XYPlot generated from Lists of Numbers.
*/
-public class SimpleXYSeries implements XYSeries, PlotListener {
-
- private static final String TAG = SimpleXYSeries.class.getName();
-
- @Override
- public void onBeforeDraw(Plot source, Canvas canvas) {
- lock.readLock().lock();
- }
+public class SimpleXYSeries implements EditableXYSeries, OrderedXYSeries, PlotListener {
+ private volatile LinkedList xVals = new LinkedList<>();
+ private volatile LinkedList yVals = new LinkedList<>();
+ private volatile String title = null;
- @Override
- public void onAfterDraw(Plot source, Canvas canvas) {
- lock.readLock().unlock();
- }
+ private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
+ private XOrder xOrder = XOrder.NONE;
public enum ArrayFormat {
Y_VALS_ONLY,
XY_VALS_INTERLEAVED
}
- private volatile LinkedList xVals = new LinkedList();
- private volatile LinkedList yVals = new LinkedList();
- private volatile String title = null;
- private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
-
-
public SimpleXYSeries(String title) {
this.title = title;
}
@@ -60,11 +53,39 @@ public SimpleXYSeries(ArrayFormat format, String title, Number... model) {
this(asNumberList(model), format, title);
}
+ /**
+ * Retrieve the current x-ordering specified for this series. Default is
+ * {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE}.
+ * @return
+ */
+ @Override
+ public XOrder getXOrder() {
+ return xOrder;
+ }
+
+ /**
+ * If XVals are in strict ascending order, use this method to set
+ * {@link com.androidplot.xy.OrderedXYSeries.XOrder#ASCENDING} to provide an optimization
+ * hint to the renderer.
+ * @param xOrder
+ */
+ public void setXOrder(XOrder xOrder) {
+ this.xOrder = xOrder;
+ }
+
+ @Override
+ public void onBeforeDraw(Plot source, Canvas canvas) {
+ lock.readLock().lock();
+ }
+
+ @Override
+ public void onAfterDraw(Plot source, Canvas canvas) {
+ lock.readLock().unlock();
+ }
+
protected static List asNumberList(Number... model) {
- List numbers = new ArrayList<>();
- for(Number n : model) {
- numbers.add(n);
- }
+ List numbers = new ArrayList<>(model.length);
+ Collections.addAll(numbers, model);
return numbers;
}
@@ -120,7 +141,7 @@ public void setModel(List extends Number> model, ArrayFormat format) {
lock.writeLock().lock();
try {
// empty the current values:
- xVals = null;
+ xVals.clear();
yVals.clear();
// make sure the new model has data:
@@ -132,15 +153,16 @@ public void setModel(List extends Number> model, ArrayFormat format) {
// array containing only y-vals. assume x = index:
case Y_VALS_ONLY:
- for(Number n : model) {
- yVals.add(n);
+ yVals.addAll(model);
+ for(int i = 0; i < yVals.size(); i++) {
+ xVals.add(i);
}
break;
// xy interleaved array:
case XY_VALS_INTERLEAVED:
if (xVals == null) {
- xVals = new LinkedList();
+ xVals = new LinkedList<>();
}
if (model.size() % 2 != 0) {
throw new IndexOutOfBoundsException("Cannot auto-generate series from odd-sized xy List.");
@@ -188,6 +210,26 @@ public void setY(Number value, int index) {
}
}
+ @Override
+ public void resize(int size) {
+ try {
+ lock.writeLock().lock();
+ if (xVals.size() < size) {
+ for (int i = xVals.size(); i < size; i++) {
+ xVals.add(null);
+ yVals.add(null);
+ }
+ } else if(xVals.size() > size) {
+ for(int i = xVals.size(); i > size; i--) {
+ xVals.removeLast();
+ yVals.removeLast();
+ }
+ }
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
/**
* Sets xy values based on index
* @param xVal
@@ -299,7 +341,9 @@ public LinkedList getyVals() {
public void clear() {
lock.writeLock().lock();
try {
- xVals.clear();
+ if (xVals != null) {
+ xVals.clear();
+ }
yVals.clear();
} finally {
lock.writeLock().unlock();
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java b/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java
index d5db2c9d..00f60747 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/StepMode.java
@@ -20,9 +20,11 @@
* INCREMENTAL_VALUE - (default) draw a tick every n values.
* INCREMENTAL_PIXEL - draw a tick every n pixels.
* SUBDIVIDE - draw n number of evenly spaced lines.
+ * INCREMENT_BY_FIT choose increment from a list of possible values
*/
public enum StepMode {
SUBDIVIDE, // default
INCREMENT_BY_VAL,
- INCREMENT_BY_PIXELS
+ INCREMENT_BY_PIXELS,
+ INCREMENT_BY_FIT
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java b/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java
new file mode 100644
index 00000000..d2b12f0b
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/StepModelFit.java
@@ -0,0 +1,85 @@
+package com.androidplot.xy;
+
+import com.androidplot.Region;
+
+import java.util.Arrays;
+
+/**
+ * Subclass of StepModel that chooses from predefined step values. Depending on the currently
+ * displayed range (by value) choose increment so that the number of lines
+ * is closest to StepModel.value
+ */
+public class StepModelFit extends StepModel {
+
+ private double[] steps; // list of steps to choose from
+ private Region scale; // axis region on display
+
+ public StepModelFit(Region axisRegion, double[] increments, double numLines) {
+ super(StepMode.INCREMENT_BY_FIT, numLines);
+
+ setSteps(increments);
+ setScale(axisRegion);
+ }
+
+ public double[] getSteps() {
+ return steps;
+ }
+
+ public void setSteps(double[] steps) {
+
+ // sanity checks: no null, 0 or negative
+ if (steps == null || steps.length == 0)
+ return;
+
+ for (double step : steps) {
+ if (step <= 0.0d)
+ return;
+ }
+
+ this.steps = steps;
+ }
+
+ public Region getScale() {
+ return scale;
+ }
+
+ public void setScale(Region scale) {
+ this.scale = scale;
+ }
+
+ // does not return StepModel.value instead calculates best fit
+ @Override
+ public double getValue() {
+
+ // no possible increments where supplied
+ // or no region defined
+ if (steps == null || scale == null || !scale.isDefined())
+ return super.getValue();
+
+ double curStep = steps[0];
+ double oldDistance = Math.abs((scale.length().doubleValue() / curStep)-super.getValue() );
+
+ // determine which step size comes closest to the desired number of steps
+ // since steps[] is a small array brute force search is ok
+ for (double step : steps) {
+
+ double newDistance = Math.abs((scale.length().doubleValue() / step)-super.getValue() );
+
+ // closer than previous stepping?
+ if (newDistance < oldDistance){
+ curStep = step;
+ oldDistance = newDistance;
+ }
+ }
+ return curStep;
+ }
+
+ @Override
+ public String toString() {
+ return "StepModelFit{" +
+ "steps=" + Arrays.toString(steps) +
+ ", scale=" + scale +
+ ", current stepping=" + getValue() +
+ '}';
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java
index 04f0449e..f14608d7 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/StepRenderer.java
@@ -23,7 +23,6 @@
* Renders a point as a line with the vertices marked. Requires 2 or more points to
* be rendered.
*/
-
public class StepRenderer extends LineAndPointRenderer {
public StepRenderer(XYPlot plot) {
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java b/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java
index 30e9aacd..de0436b2 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/ValueMarker.java
@@ -16,10 +16,14 @@
package com.androidplot.xy;
+import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
+import android.graphics.RectF;
+
import com.androidplot.ui.PositionMetric;
import com.androidplot.ui.TextOrientation;
+import com.androidplot.util.FontUtils;
/**
* Encapsulates a single axis line marker drawn onto an XYPlot at a specified value.
@@ -27,6 +31,8 @@
*/
public abstract class ValueMarker {
+ private static final int MARKER_LABEL_SPACING = 2;
+
public String getText() {
return text;
}
@@ -135,4 +141,37 @@ public PositionMetricType getTextPosition() {
public void setTextPosition(PositionMetricType textPosition) {
this.textPosition = textPosition;
}
+
+ /**
+ * Renders the text associated with user defined markers
+ *
+ * @param canvas
+ * @param text
+ * @param gridRect
+ * @param x
+ * @param y
+ */
+ protected void drawMarkerText(Canvas canvas, String text, RectF gridRect,
+ float x, float y) {
+ if (getText() != null) {
+ x += MARKER_LABEL_SPACING;
+ y -= MARKER_LABEL_SPACING;
+ RectF textRect = new RectF(FontUtils.getStringDimensions(text, getTextPaint()
+ ));
+ textRect.offsetTo(x, y - textRect.height());
+
+ if (textRect.right > gridRect.right) {
+ textRect.offset(-(textRect.right - gridRect.right), 0);
+ }
+
+ if (textRect.top < gridRect.top) {
+ textRect.offset(0, gridRect.top - textRect.top);
+ }
+
+ canvas.drawText(text, textRect.left, textRect.bottom, getTextPaint()
+ );
+ }
+ }
+
+ public abstract void draw(Canvas canvas, XYPlot plot, RectF gridRect);
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java b/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java
index af02ca64..a4c12438 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XValueMarker.java
@@ -16,7 +16,10 @@
package com.androidplot.xy;
+import android.graphics.Canvas;
import android.graphics.Paint;
+import android.graphics.RectF;
+
import com.androidplot.ui.VerticalPositioning;
import com.androidplot.ui.VerticalPosition;
@@ -55,4 +58,19 @@ public XValueMarker(Number value, String text, VerticalPosition textPosition, Pa
public XValueMarker(Number value, String text, VerticalPosition textPosition, int linePaint, int textPaint) {
super(value, text, textPosition, linePaint, textPaint);
}
+
+ @Override
+ public void draw(Canvas canvas, XYPlot plot, RectF gridRect) {
+ if (getValue() != null) {
+ float xPix = (float) plot.getBounds().xRegion
+ .transform(getValue().doubleValue(), gridRect.left, gridRect.right, false);
+ canvas.drawLine(xPix, gridRect.top, xPix, gridRect.bottom, getLinePaint()
+ );
+ float yPix = getTextPosition().getPixelValue(gridRect.height());
+ yPix += gridRect.top;
+ if (getText() != null) {
+ drawMarkerText(canvas, getText(), gridRect, xPix, yPix);
+ }
+ }
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
index ae22a4ea..c4452d33 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
@@ -16,8 +16,12 @@
package com.androidplot.xy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
/**
* Calculates the min/max constraints for an xy plane.
+ *
* @since 0.9.7
*/
public class XYConstraints {
@@ -44,113 +48,148 @@ public XYConstraints() {
this(null, null, null, null);
}
- public XYConstraints(Number minX, Number maxX, Number minY, Number maxY) {
+ public XYConstraints(@Nullable Number minX, @Nullable Number maxX, @Nullable Number minY, @Nullable Number maxY) {
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
+ public boolean contains(@NonNull RectRegion rectRegion) {
+ return contains(rectRegion.getMinY(), rectRegion.getMinY())
+ && contains(rectRegion.getMaxX(), rectRegion.getMaxY());
+ }
+
public boolean contains(Number x, Number y) {
- if(x == null || y == null) {
+ if (x == null || y == null) {
// this is essentially an invisible point:
return false;
- } else {
- final double dx = x.doubleValue();
-
- if(minX != null && dx < minX.doubleValue()) {
- return false;
- } else if(maxX != null && dx > maxX.doubleValue()) {
- return false;
- } else {
- final double dy = y.doubleValue();
- if(minY != null && dy < minY.doubleValue()) {
- return false;
- } else if(maxY != null && dy > maxY.doubleValue()) {
- return false;
- }
- }
+ }
+
+ if (minX == null && maxX == null && minY == null && maxY == null) {
+ //there are no constraints
return true;
}
+
+ final double dx = x.doubleValue();
+ if (minX != null && dx < minX.doubleValue()) {
+ return false;
+ } else if (maxX != null && dx > maxX.doubleValue()) {
+ return false;
+ }
+
+ final double dy = y.doubleValue();
+ if (minY != null && dy < minY.doubleValue()) {
+ return false;
+ } else if (maxY != null && dy > maxY.doubleValue()) {
+ return false;
+ }
+
+ return true;
}
+ @Nullable
public Number getMinX() {
return minX;
}
+ @Nullable
public Number getMaxX() {
return maxX;
}
+ @Nullable
public Number getMinY() {
return minY;
}
+ @Nullable
public Number getMaxY() {
return maxY;
}
+ public void setMinX(@Nullable Number minX) {
+ this.minX = minX;
+ }
+
+ public void setMaxX(@Nullable Number maxX) {
+ this.maxX = maxX;
+ }
+
+ public void setMinY(@Nullable Number minY) {
+ this.minY = minY;
+ }
+
+ public void setMaxY(@Nullable Number maxY) {
+ this.maxY = maxY;
+ }
+
+ @NonNull
public XYFramingModel getDomainFramingModel() {
return domainFramingModel;
}
- public void setDomainFramingModel(XYFramingModel domainFramingModel) {
+ public void setDomainFramingModel(@NonNull XYFramingModel domainFramingModel) {
this.domainFramingModel = domainFramingModel;
}
+ @NonNull
public XYFramingModel getRangeFramingModel() {
return rangeFramingModel;
}
- public void setRangeFramingModel(XYFramingModel rangeFramingModel) {
+ public void setRangeFramingModel(@NonNull XYFramingModel rangeFramingModel) {
this.rangeFramingModel = rangeFramingModel;
}
+ @NonNull
public BoundaryMode getDomainUpperBoundaryMode() {
return domainUpperBoundaryMode;
}
- public void setDomainUpperBoundaryMode(BoundaryMode domainUpperBoundaryMode) {
+ public void setDomainUpperBoundaryMode(@NonNull BoundaryMode domainUpperBoundaryMode) {
this.domainUpperBoundaryMode = domainUpperBoundaryMode;
}
+ @NonNull
public BoundaryMode getDomainLowerBoundaryMode() {
return domainLowerBoundaryMode;
}
- public void setDomainLowerBoundaryMode(BoundaryMode domainLowerBoundaryMode) {
+ public void setDomainLowerBoundaryMode(@NonNull BoundaryMode domainLowerBoundaryMode) {
this.domainLowerBoundaryMode = domainLowerBoundaryMode;
}
+ @NonNull
public BoundaryMode getRangeUpperBoundaryMode() {
return rangeUpperBoundaryMode;
}
- public void setRangeUpperBoundaryMode(BoundaryMode rangeUpperBoundaryMode) {
+ public void setRangeUpperBoundaryMode(@NonNull BoundaryMode rangeUpperBoundaryMode) {
this.rangeUpperBoundaryMode = rangeUpperBoundaryMode;
}
+ @NonNull
public BoundaryMode getRangeLowerBoundaryMode() {
return rangeLowerBoundaryMode;
}
- public void setRangeLowerBoundaryMode(BoundaryMode rangeLowerBoundaryMode) {
+ public void setRangeLowerBoundaryMode(@NonNull BoundaryMode rangeLowerBoundaryMode) {
this.rangeLowerBoundaryMode = rangeLowerBoundaryMode;
}
- public void setMinX(Number minX) {
- this.minX = minX;
- }
-
- public void setMaxX(Number maxX) {
- this.maxX = maxX;
- }
-
- public void setMinY(Number minY) {
- this.minY = minY;
- }
-
- public void setMaxY(Number maxY) {
- this.maxY = maxY;
+ @Override
+ public String toString() {
+ return "XYConstraints{" + "domainFramingModel=" + domainFramingModel +
+ ", rangeFramingModel=" + rangeFramingModel +
+ ", domainUpperBoundaryMode=" + domainUpperBoundaryMode +
+ ", domainLowerBoundaryMode=" + domainLowerBoundaryMode +
+ ", rangeUpperBoundaryMode=" + rangeUpperBoundaryMode +
+ ", rangeLowerBoundaryMode=" + rangeLowerBoundaryMode +
+ ", minX=" + minX +
+ ", maxX=" + maxX +
+ ", minY=" + minY +
+ ", maxY=" + maxY +
+ '}';
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java
index 63a74f5b..8fa0571f 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java
@@ -16,23 +16,38 @@
package com.androidplot.xy;
-import android.content.res.*;
-import android.graphics.*;
-
-import com.androidplot.*;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.androidplot.R;
import com.androidplot.Region;
-import com.androidplot.exception.PlotRenderException;
-import com.androidplot.ui.*;
+import com.androidplot.ui.Insets;
+import com.androidplot.ui.LayoutManager;
+import com.androidplot.ui.RenderStack;
+import com.androidplot.ui.Size;
import com.androidplot.ui.widget.Widget;
-import com.androidplot.util.*;
+import com.androidplot.util.AttrUtils;
+import com.androidplot.util.FontUtils;
+import com.androidplot.util.PixelUtils;
+import com.androidplot.util.RectFUtils;
import java.text.DecimalFormat;
import java.text.Format;
-import java.util.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Map;
/**
- * Displays graphical data (lines, points, etc.) annotated with domain and range tick markers.
- * The inner area of the graph upon which grid lines and points are rendered is called the "grid" area.
+ * Displays graphical data (lines, points, etc.) annotated with domain and range tick markers. The
+ * inner area of the graph upon which grid lines and points are rendered is called the "grid" area.
*/
public class XYGraphWidget extends Widget {
@@ -47,8 +62,6 @@ public class XYGraphWidget extends Widget {
private static final float DEFAULT_LINE_LABEL_TEXT_SIZE_PX = PixelUtils.spToPix(15);
- private static final int MARKER_LABEL_SPACING = TWO;
-
/**
* Line interval per range label
*/
@@ -112,8 +125,8 @@ public class XYGraphWidget extends Widget {
private Paint domainOriginLinePaint;
private Paint rangeOriginLinePaint;
- private float domainCursorPosition;
- private float rangeCursorPosition;
+ private Float domainCursorPosition;
+ private Float rangeCursorPosition;
private boolean drawMarkersEnabled = true;
private boolean drawGridOnTop;
@@ -121,21 +134,26 @@ public class XYGraphWidget extends Widget {
/**
* Set of edges for which line labels should be displayed
*/
- private Set lineLabelEdges = new HashSet<>();
+ private EnumSet lineLabelEdges = EnumSet.noneOf(Edge.class);
private RenderStack extends XYSeries, ? extends XYSeriesFormatter> renderStack;
private CursorLabelFormatter cursorLabelFormatter;
- private HashMap lineLabelStyles = getDefaultLineLabelStyles();
- private HashMap lineLabelRenderers = getDefaultLineLabelRenderers();
+ private Map lineLabelStyles = getDefaultLineLabelStyles();
+ private Map lineLabelRenderers = getDefaultLineLabelRenderers();
public static class LineLabelRenderer {
- public void drawLabel(Canvas canvas, LineLabelStyle style, Number val, float x, float y, boolean isOrigin) {
+ public void drawLabel(Canvas canvas,
+ LineLabelStyle style,
+ Number val,
+ float x,
+ float y,
+ boolean isOrigin) {
final int canvasState = canvas.save();
try {
- final String txt = style.format.format(val.doubleValue());
+ final String txt = style.format.format(val);
canvas.rotate(style.getRotation(), x, y);
drawLabel(canvas, txt, style.getPaint(), x, y, isOrigin);
} finally {
@@ -143,7 +161,12 @@ public void drawLabel(Canvas canvas, LineLabelStyle style, Number val, float x,
}
}
- protected void drawLabel(Canvas canvas, String text, Paint paint, float x, float y, boolean isOrigin) {
+ protected void drawLabel(Canvas canvas,
+ String text,
+ Paint paint,
+ float x,
+ float y,
+ boolean isOrigin) {
canvas.drawText(text, x, y, paint);
}
}
@@ -159,6 +182,7 @@ public static class LineLabelStyle {
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(DEFAULT_LINE_LABEL_TEXT_SIZE_PX);
}
+
public Format getFormat() {
return format;
}
@@ -187,17 +211,16 @@ public void setPaint(Paint paint) {
public interface CursorLabelFormatter {
/**
- *
* @return The Paint to be used to draw the cursor text label.
*/
Paint getTextPaint();
/**
- *
- * @return Null if no background should be drawn,
- * the Paint used to draw the background otherwise.
+ * @return Null if no background should be drawn, the Paint used to draw the background
+ * otherwise.
*/
Paint getBackgroundPaint();
+
String getLabelText(Number x, Number y);
}
@@ -256,18 +279,21 @@ public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) {
/**
* Apply xml attrs
+ *
* @param attrs
*/
public void processAttrs(TypedArray attrs) {
setDrawGridOnTop(attrs.getBoolean(R.styleable.xy_XYPlot_drawGridOnTop, isDrawGridOnTop()));
int tlp = attrs.getInt(R.styleable.xy_XYPlot_lineLabels, 0);
- if(tlp != 0) {
+ if (tlp != 0) {
setLineLabelEdges(tlp);
}
- setGridClippingEnabled(attrs.getBoolean(R.styleable.xy_XYPlot_gridClippingEnabled,
- isGridClippingEnabled()));
+ setGridClippingEnabled(attrs.getBoolean(
+ R.styleable.xy_XYPlot_gridClippingEnabled,
+ isGridClippingEnabled()
+ ));
final LineLabelStyle lineLabelStyleTop = getLineLabelStyle(Edge.TOP);
final LineLabelStyle lineLabelStyleBottom = getLineLabelStyle(Edge.BOTTOM);
@@ -276,19 +302,23 @@ public void processAttrs(TypedArray attrs) {
lineLabelStyleTop.setRotation(attrs.getFloat(
R.styleable.xy_XYPlot_lineLabelRotationTop,
- lineLabelStyleTop.getRotation()));
+ lineLabelStyleTop.getRotation()
+ ));
lineLabelStyleBottom.setRotation(attrs.getFloat(
R.styleable.xy_XYPlot_lineLabelRotationBottom,
- lineLabelStyleBottom.getRotation()));
+ lineLabelStyleBottom.getRotation()
+ ));
lineLabelStyleLeft.setRotation(attrs.getFloat(
R.styleable.xy_XYPlot_lineLabelRotationLeft,
- lineLabelStyleLeft.getRotation()));
+ lineLabelStyleLeft.getRotation()
+ ));
lineLabelStyleRight.setRotation(attrs.getFloat(
R.styleable.xy_XYPlot_lineLabelRotationRight,
- lineLabelStyleRight.getRotation()));
+ lineLabelStyleRight.getRotation()
+ ));
setLineExtensionTop(attrs.getDimension(
R.styleable.xy_XYPlot_lineExtensionTop, getLineExtensionTop()));
@@ -302,34 +332,40 @@ public void processAttrs(TypedArray attrs) {
AttrUtils.configureTextPaint(attrs, lineLabelStyleTop.getPaint(),
R.styleable.xy_XYPlot_lineLabelTextColorTop,
R.styleable.xy_XYPlot_lineLabelTextSizeTop,
- R.styleable.xy_XYPlot_lineLabelAlignTop);
+ R.styleable.xy_XYPlot_lineLabelAlignTop
+ );
AttrUtils.configureTextPaint(attrs, lineLabelStyleBottom.getPaint(),
R.styleable.xy_XYPlot_lineLabelTextColorBottom,
R.styleable.xy_XYPlot_lineLabelTextSizeBottom,
- R.styleable.xy_XYPlot_lineLabelAlignBottom);
+ R.styleable.xy_XYPlot_lineLabelAlignBottom
+ );
AttrUtils.configureTextPaint(attrs, lineLabelStyleLeft.getPaint(),
R.styleable.xy_XYPlot_lineLabelTextColorLeft,
R.styleable.xy_XYPlot_lineLabelTextSizeLeft,
- R.styleable.xy_XYPlot_lineLabelAlignLeft);
+ R.styleable.xy_XYPlot_lineLabelAlignLeft
+ );
AttrUtils.configureTextPaint(attrs, lineLabelStyleRight.getPaint(),
R.styleable.xy_XYPlot_lineLabelTextColorRight,
R.styleable.xy_XYPlot_lineLabelTextSizeRight,
- R.styleable.xy_XYPlot_lineLabelAlignRight);
+ R.styleable.xy_XYPlot_lineLabelAlignRight
+ );
AttrUtils.configureInsets(attrs, getGridInsets(),
R.styleable.xy_XYPlot_gridInsetTop,
R.styleable.xy_XYPlot_gridInsetBottom,
R.styleable.xy_XYPlot_gridInsetLeft,
- R.styleable.xy_XYPlot_gridInsetRight);
+ R.styleable.xy_XYPlot_gridInsetRight
+ );
AttrUtils.configureInsets(attrs, getLineLabelInsets(),
R.styleable.xy_XYPlot_lineLabelInsetTop,
R.styleable.xy_XYPlot_lineLabelInsetBottom,
R.styleable.xy_XYPlot_lineLabelInsetLeft,
- R.styleable.xy_XYPlot_lineLabelInsetRight);
+ R.styleable.xy_XYPlot_lineLabelInsetRight
+ );
// graph size & position
AttrUtils.configureWidget(attrs, this,
@@ -337,7 +373,8 @@ public void processAttrs(TypedArray attrs) {
R.styleable.xy_XYPlot_graphWidthMode, R.styleable.xy_XYPlot_graphWidth,
R.styleable.xy_XYPlot_graphHorizontalPositioning, R.styleable.xy_XYPlot_graphHorizontalPosition,
R.styleable.xy_XYPlot_graphVerticalPositioning, R.styleable.xy_XYPlot_graphVerticalPosition,
- R.styleable.xy_XYPlot_graphAnchor, R.styleable.xy_XYPlot_graphVisible);
+ R.styleable.xy_XYPlot_graphAnchor, R.styleable.xy_XYPlot_graphVisible
+ );
// domainLabel size & position
AttrUtils.configureWidget(attrs, this,
@@ -345,7 +382,8 @@ public void processAttrs(TypedArray attrs) {
R.styleable.xy_XYPlot_domainTitleWidthMode, R.styleable.xy_XYPlot_domainTitleWidth,
R.styleable.xy_XYPlot_domainTitleHorizontalPositioning, R.styleable.xy_XYPlot_domainTitleHorizontalPosition,
R.styleable.xy_XYPlot_domainTitleVerticalPositioning, R.styleable.xy_XYPlot_domainTitleVerticalPosition,
- R.styleable.xy_XYPlot_domainTitleAnchor, R.styleable.xy_XYPlot_domainTitleVisible);
+ R.styleable.xy_XYPlot_domainTitleAnchor, R.styleable.xy_XYPlot_domainTitleVisible
+ );
// rangeLabel size & position
AttrUtils.configureWidget(attrs, this,
@@ -353,81 +391,100 @@ public void processAttrs(TypedArray attrs) {
R.styleable.xy_XYPlot_rangeTitleWidthMode, R.styleable.xy_XYPlot_rangeTitleWidth,
R.styleable.xy_XYPlot_rangeTitleHorizontalPositioning, R.styleable.xy_XYPlot_rangeTitleHorizontalPosition,
R.styleable.xy_XYPlot_rangeTitleVerticalPositioning, R.styleable.xy_XYPlot_rangeTitleVerticalPosition,
- R.styleable.xy_XYPlot_rangeTitleAnchor, R.styleable.xy_XYPlot_rangeTitleVisible);
+ R.styleable.xy_XYPlot_rangeTitleAnchor, R.styleable.xy_XYPlot_rangeTitleVisible
+ );
+
+ // rotation
+ AttrUtils.configureWidgetRotation(attrs, this, R.styleable.xy_XYPlot_graphRotation);
- // graphWidget
+ // padding & margin
AttrUtils.configureBoxModelable(attrs, this,
R.styleable.xy_XYPlot_graphMarginTop, R.styleable.xy_XYPlot_graphMarginBottom,
R.styleable.xy_XYPlot_graphMarginLeft, R.styleable.xy_XYPlot_graphMarginRight,
R.styleable.xy_XYPlot_graphPaddingTop, R.styleable.xy_XYPlot_graphPaddingBottom,
- R.styleable.xy_XYPlot_graphPaddingLeft, R.styleable.xy_XYPlot_graphPaddingRight);
+ R.styleable.xy_XYPlot_graphPaddingLeft, R.styleable.xy_XYPlot_graphPaddingRight
+ );
// domainOriginLinePaint
AttrUtils.configureLinePaint(attrs, getDomainOriginLinePaint(),
R.styleable.xy_XYPlot_domainOriginLineColor,
- R.styleable.xy_XYPlot_domainOriginLineThickness);
+ R.styleable.xy_XYPlot_domainOriginLineThickness
+ );
// rangeOriginLinePaint
AttrUtils.configureLinePaint(attrs, getRangeOriginLinePaint(),
R.styleable.xy_XYPlot_rangeOriginLineColor,
- R.styleable.xy_XYPlot_rangeOriginLineThickness);
+ R.styleable.xy_XYPlot_rangeOriginLineThickness
+ );
AttrUtils.configureLinePaint(attrs, getDomainGridLinePaint(),
R.styleable.xy_XYPlot_domainLineColor,
- R.styleable.xy_XYPlot_domainLineThickness);
+ R.styleable.xy_XYPlot_domainLineThickness
+ );
AttrUtils.configureLinePaint(attrs, getRangeGridLinePaint(),
R.styleable.xy_XYPlot_rangeLineColor,
- R.styleable.xy_XYPlot_rangeLineThickness);
+ R.styleable.xy_XYPlot_rangeLineThickness
+ );
AttrUtils.setColor(attrs, getBackgroundPaint(),
- R.styleable.xy_XYPlot_graphBackgroundColor);
+ R.styleable.xy_XYPlot_graphBackgroundColor
+ );
AttrUtils.setColor(attrs, getGridBackgroundPaint(),
- R.styleable.xy_XYPlot_gridBackgroundColor);
+ R.styleable.xy_XYPlot_gridBackgroundColor
+ );
}
/**
- * Convenience method. Wraps getYVal(float)
+ * Convenience method. Wraps screenToSeriesY(float)
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
* @param point
* @return
*/
- public Number getYVal(PointF point) {
- return getYVal(point.y);
+ protected XYCoords screenToSeries(PointF point) {
+ if (!plot.getBounds().isFullyDefined()) {
+ return null;
+ }
+ return new RectRegion(gridRect)
+ .transform(point.x, point.y, plot.getBounds(), false, true);
}
/**
- * Converts a y pixel to a y value.
+ * Convenience method. Wraps screenToSeriesX(float)
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
- * @param yPix
+ * @param point
* @return
*/
- public Number getYVal(float yPix) {
- if (!plot.getBounds().getyRegion().isDefined()) {
- return null;
- }
- return new Region(gridRect.top, gridRect.bottom)
- .transform(yPix, plot.getBounds().getyRegion(), true);
+ protected Number screenToSeriesX(PointF point) {
+ return screenToSeriesX(point.x);
}
/**
- * Convenience method. Wraps getXVal(float)
+ * Convenience method. Wraps screenToSeriesY(float)
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
* @param point
* @return
*/
- public Number getXVal(PointF point) {
- return getXVal(point.x);
+ protected Number screenToSeriesY(PointF point) {
+ return screenToSeriesY(point.y);
}
/**
* Converts an x pixel into an x value.
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
* @param xPix
* @return
*/
- public Number getXVal(float xPix) {
+ protected Number screenToSeriesX(float xPix) {
if (!plot.getBounds().xRegion.isDefined()) {
return null;
}
@@ -435,17 +492,54 @@ public Number getXVal(float xPix) {
.transform(xPix, plot.getBounds().getxRegion());
}
- @Override
- protected void doOnDraw(Canvas canvas, RectF widgetRect)
- throws PlotRenderException {
+ /**
+ * Converts a y pixel to a y value.
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
+ *
+ * @param yPix
+ * @return
+ */
+ protected Number screenToSeriesY(float yPix) {
+ if (!plot.getBounds().getyRegion().isDefined()) {
+ return null;
+ }
+ return new Region(gridRect.top, gridRect.bottom)
+ .transform(yPix, plot.getBounds().getyRegion(), true);
+ }
- if(gridRect == null) {
- gridRect = RectFUtils.applyInsets(widgetRect, gridInsets);
+ protected PointF seriesToScreen(XYCoords xy) {
+ if (!plot.getBounds().isFullyDefined()) {
+ return null;
}
+ return plot.getBounds().transform(xy, gridRect, false, true);
+ }
+
+ protected float seriesToScreenX(Number x) {
+ return (float) plot.getBounds().getxRegion().
+ transform(x.doubleValue(), gridRect.left, gridRect.right, false);
+ }
+
+ protected float seriesToScreenY(Number y) {
+ return (float) plot.getBounds().getyRegion().
+ transform(y.doubleValue(), gridRect.bottom, gridRect.top, true);
+ }
+
+ @Override
+ protected void onResize(@Nullable RectF oldRect, @NonNull RectF newRect) {
+ recalculateSizes(newRect);
+ }
- if(labelRect == null) {
- labelRect = RectFUtils.applyInsets(widgetRect, lineLabelInsets);
+ protected void recalculateSizes(@Nullable RectF rect) {
+ if(rect == null) {
+ rect = getWidgetDimensions().paddedRect;
}
+ gridRect = RectFUtils.applyInsets(rect, gridInsets);
+ labelRect = RectFUtils.applyInsets(rect, lineLabelInsets);
+ }
+
+ @Override
+ protected void doOnDraw(Canvas canvas, RectF widgetRect) {
// don't draw if we have no space to draw into
if (gridRect.height() > ZERO && gridRect.width() > ZERO) {
@@ -454,7 +548,7 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect)
&& bounds.getMaxX() != null
&& bounds.getMinY() != null
&& bounds.getMaxY() != null) {
- if(drawGridOnTop) {
+ if (drawGridOnTop) {
drawData(canvas);
drawGrid(canvas);
} else {
@@ -470,101 +564,104 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect)
}
protected void drawDomainLine(Canvas canvas, float xPix, Number xVal,
- Paint linePaint, boolean isOrigin) {
+ Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) {
// lines
if (linePaint != null) {
- canvas.drawLine(xPix, gridRect.top - lineExtensionTop,
- xPix, gridRect.bottom + lineExtensionBottom, linePaint);
+ canvas.drawLine(xPix, gridRect.top - lineExtensionTop,
+ xPix, gridRect.bottom + lineExtensionBottom, linePaint
+ );
}
// labels
- drawLineLabel(canvas, Edge.TOP, xVal, xPix, labelRect.top, isOrigin);
- drawLineLabel(canvas, Edge.BOTTOM, xVal, xPix, labelRect.bottom, isOrigin);
+ if(shouldDrawLabel) {
+ if (isLineLabelEnabled(Edge.TOP)) {
+ drawLineLabel(canvas, Edge.TOP, xVal, xPix, labelRect.top, isOrigin);
+ }
+
+ if (isLineLabelEnabled(Edge.BOTTOM)) {
+ drawLineLabel(canvas, Edge.BOTTOM, xVal, xPix, labelRect.bottom, isOrigin);
+ }
+ }
}
protected void drawRangeLine(Canvas canvas, float yPix, Number yVal,
- Paint linePaint, boolean isOrigin) {
+ Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) {
// lines
if (linePaint != null) {
canvas.drawLine(gridRect.left - lineExtensionLeft, yPix,
- gridRect.right + lineExtensionRight, yPix, linePaint);
+ gridRect.right + lineExtensionRight, yPix, linePaint
+ );
}
- // labels
- drawLineLabel(canvas, Edge.LEFT, yVal, labelRect.left, yPix, isOrigin);
- drawLineLabel(canvas, Edge.RIGHT, yVal, labelRect.right, yPix, isOrigin);
+ if(shouldDrawLabel) {
+ // labels
+ if (isLineLabelEnabled(Edge.LEFT)) {
+ drawLineLabel(canvas, Edge.LEFT, yVal, labelRect.left, yPix, isOrigin);
+ }
+ if (isLineLabelEnabled(Edge.RIGHT)) {
+ drawLineLabel(canvas, Edge.RIGHT, yVal, labelRect.right, yPix, isOrigin);
+ }
+ }
}
- protected void drawLineLabel(Canvas canvas, Edge edge, Number val, float x, float y, boolean isOrigin) {
- if(isLineLabelEnabled(edge)) {
- getLineLabelRenderer(edge).drawLabel(canvas, getLineLabelStyle(edge), val, x, y, isOrigin);
- }
+ protected void drawLineLabel(Canvas canvas,
+ Edge edge,
+ Number val,
+ float x,
+ float y,
+ boolean isOrigin) {
+ getLineLabelRenderer(edge).drawLabel(canvas, getLineLabelStyle(edge), val, x, y, isOrigin);
}
/**
- * Draws the drid and domain/range labels for the plot.
+ * Draws the grid and domain/range labels for the plot.
*
* @param canvas
*/
protected void drawGrid(Canvas canvas) {
- if(!drawGridOnTop) {
+ if (!drawGridOnTop) {
drawGridBackground(canvas);
}
Number domainOrigin = plot.getDomainOrigin();
- double domainOriginPix;
+ final double domainOriginPix;
if (domainOrigin != null) {
domainOriginPix = plot.getBounds().getxRegion().transform(
plot.getDomainOrigin().doubleValue(), gridRect.left, gridRect.right, false);
} else {
// if no domain origin is set, use the leftmost value visible on the grid:
domainOriginPix = gridRect.left;
- domainOrigin=plot.getBounds().getMinX();
+ domainOrigin = plot.getBounds().getMinX();
}
Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect);
- // draw domain origin:
- if (domainOriginPix >= gridRect.left
- && domainOriginPix <= gridRect.right) {
- drawDomainLine(canvas, (float) domainOriginPix,
- domainOrigin, domainOriginLinePaint, true);
- }
-
- // draw lines LEFT of origin:
- double xPix = domainOriginPix - domainStep.getStepPix();
- for (int i = ONE; xPix >= gridRect.left - FUDGE; xPix = domainOriginPix
- - (i * domainStep.getStepPix())) {
- double xVal = domainOrigin.doubleValue() - i
- * domainStep.getStepVal();
-
- if (xPix <= gridRect.right) {
- final boolean isDomainTick = i% getLinesPerDomainLabel() == ZERO;
- final Paint lp = isDomainTick ? domainGridLinePaint : domainSubGridLinePaint;
- drawDomainLine(canvas, (float) xPix, xVal, lp, false);
- }
- i++;
- }
-
- // draw lines RIGHT of origin:
- xPix = domainOriginPix + domainStep.getStepPix();
- for (int i = ONE; xPix <= gridRect.right + FUDGE; xPix = domainOriginPix
- + (i * domainStep.getStepPix())) {
- double xVal = domainOrigin.doubleValue() + i
- * domainStep.getStepVal();
-
- if (xPix >= gridRect.left) {
- final boolean isDomainTick = i% getLinesPerDomainLabel() == ZERO;
- final Paint lp = isDomainTick ? domainGridLinePaint : domainSubGridLinePaint;
- drawDomainLine(canvas, (float) xPix, xVal, lp, false);
+ // Draw Domain Lines:
+
+ final double domainStepPix = domainStep.getStepPix();
+ final double iMin = (gridRect.left - domainOriginPix - FUDGE) / domainStepPix;
+ final double iMax = (gridRect.right - domainOriginPix + FUDGE) / domainStepPix;
+
+ for (int i = (int) Math.ceil(iMin); i <= iMax; i++) {
+ double xVal = domainOrigin.doubleValue() + i * domainStep.getStepVal();
+ double xPix = domainOriginPix + i * domainStepPix;
+ boolean isMajorTick = i % getLinesPerDomainLabel() == ZERO;
+ boolean isOrigin = i == 0;
+ Paint linePaint;
+ if (isOrigin) {
+ linePaint = domainOriginLinePaint;
+ } else if (isMajorTick) {
+ linePaint = domainGridLinePaint;
+ } else {
+ linePaint = domainSubGridLinePaint;
}
- i++;
+ drawDomainLine(canvas, (float) xPix, xVal, linePaint, isOrigin, isMajorTick);
}
Number rangeOrigin = plot.getRangeOrigin();
- double rangeOriginPix;
+ final double rangeOriginPix;
if (rangeOrigin != null) {
rangeOriginPix = plot.getBounds().getyRegion().transform(
rangeOrigin.doubleValue(), gridRect.top, gridRect.bottom, true);
@@ -576,106 +673,42 @@ protected void drawGrid(Canvas canvas) {
Step rangeStep = XYStepCalculator.getStep(plot, Axis.RANGE, gridRect);
- // draw range origin:
- if (rangeOriginPix >= gridRect.top && rangeOriginPix <= gridRect.bottom) {
- drawRangeLine(canvas, (float) rangeOriginPix,
- rangeOrigin, rangeOriginLinePaint, true);
- }
+ // Draw Range Lines:
final double rangeStepPix = rangeStep.getStepPix();
-
- // draw lines ABOVE origin:
- double yPix = rangeOriginPix - rangeStep.getStepPix();
- for (int i = ONE; yPix >= gridRect.top - FUDGE; yPix = rangeOriginPix - (i * rangeStepPix)) {
- double yVal = rangeOrigin.doubleValue() + i
- * rangeStep.getStepVal();
-
- if (yPix <= gridRect.bottom) {
- final boolean isRangeTick = i% getLinesPerRangeLabel() == ZERO;
- final Paint lp = isRangeTick ? rangeGridLinePaint : rangeSubGridLinePaint;
- drawRangeLine(canvas, (float)yPix, yVal, lp, false);
- }
- i++;
- }
-
- // draw lines BENEATH origin:
- yPix = rangeOriginPix + rangeStep.getStepPix();
- for (int i = ONE; yPix <= gridRect.bottom + FUDGE; yPix = rangeOriginPix + (i * rangeStepPix)) {
- double yVal = rangeOrigin.doubleValue() - i
- * rangeStep.getStepVal();
- if (yPix >= gridRect.top) {
- final boolean isRangeTick = i% getLinesPerRangeLabel() == ZERO;
- final Paint lp = isRangeTick ? rangeGridLinePaint : rangeSubGridLinePaint;
- drawRangeLine(canvas, (float)yPix, yVal, lp, false);
+ final double kMin = (gridRect.top - rangeOriginPix - FUDGE) / rangeStepPix;
+ final double kMax = (gridRect.bottom - rangeOriginPix + FUDGE) / rangeStepPix;
+
+ for (int k = (int) Math.ceil(kMin); k <= kMax; k++) {
+ // Android vertical coordinates (zero at the top of the screen) are the opposite
+ // direction of default range values (lowest on bottom of screen) so we subtract when
+ // calculating yVal
+ double yVal = rangeOrigin.doubleValue() - k * rangeStep.getStepVal();
+ double yPix = rangeOriginPix + k * rangeStepPix;
+ boolean isMajorTick = k % getLinesPerRangeLabel() == ZERO;
+ boolean isOrigin = k == 0;
+ Paint linePaint;
+ if (isOrigin) {
+ linePaint = rangeOriginLinePaint;
+ } else if (isMajorTick) {
+ linePaint = rangeGridLinePaint;
+ } else {
+ linePaint = rangeSubGridLinePaint;
}
- i++;
+ drawRangeLine(canvas, (float) yPix, yVal, linePaint, isOrigin, isMajorTick);
}
}
- /**
- * Renders the text associated with user defined markers
- *
- * @param canvas
- * @param text
- * @param marker
- * @param x
- * @param y
- */
- private void drawMarkerText(Canvas canvas, String text, ValueMarker marker,
- float x, float y) {
- x += MARKER_LABEL_SPACING;
- y -= MARKER_LABEL_SPACING;
- RectF textRect = new RectF(FontUtils.getStringDimensions(text,
- marker.getTextPaint()));
- textRect.offsetTo(x, y - textRect.height());
-
- if (textRect.right > gridRect.right) {
- textRect.offset(-(textRect.right - gridRect.right), ZERO);
- }
-
- if (textRect.top < gridRect.top) {
- textRect.offset(0, gridRect.top - textRect.top);
- }
-
- canvas.drawText(text, textRect.left, textRect.bottom,
- marker.getTextPaint());
- }
-
protected void drawMarkers(Canvas canvas) {
- if(plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) {
+ if (plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) {
for (YValueMarker marker : plot.getYValueMarkers()) {
- if (marker.getValue() != null) {
- float yPix = (float) plot.getBounds().yRegion
- .transform(marker.getValue()
- .doubleValue(), gridRect.top, gridRect.bottom, true);
- canvas.drawLine(gridRect.left, yPix,
- gridRect.right, yPix, marker.getLinePaint());
-
- float xPix = marker.getTextPosition().getPixelValue(
- gridRect.width());
- xPix += gridRect.left;
-
- if (marker.getText() != null) {
- drawMarkerText(canvas, marker.getText(), marker, xPix, yPix);
- }
- }
+ marker.draw(canvas, plot, gridRect);
}
}
- if(plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) {
+ if (plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) {
for (XValueMarker marker : plot.getXValueMarkers()) {
- if (marker.getValue() != null) {
- float xPix = (float) plot.getBounds().xRegion
- .transform(marker.getValue()
- .doubleValue(), gridRect.left, gridRect.right, false);
- canvas.drawLine(xPix, gridRect.top, xPix, gridRect.bottom,
- marker.getLinePaint());
- float yPix = marker.getTextPosition().getPixelValue(gridRect.height());
- yPix += gridRect.top;
- if (marker.getText() != null) {
- drawMarkerText(canvas, marker.getText(), marker, xPix, yPix);
- }
- }
+ marker.draw(canvas, plot, gridRect);
}
}
}
@@ -684,25 +717,29 @@ protected void drawCursors(Canvas canvas) {
boolean hasDomainCursor = false;
// draw the domain cursor:
if (domainCursorPaint != null
+ && domainCursorPosition != null
&& domainCursorPosition <= gridRect.right
&& domainCursorPosition >= gridRect.left) {
hasDomainCursor = true;
canvas.drawLine(domainCursorPosition, gridRect.top,
domainCursorPosition, gridRect.bottom,
- domainCursorPaint);
+ domainCursorPaint
+ );
}
boolean hasRangeCursor = false;
// draw the range cursor:
if (rangeCursorPaint != null
+ && rangeCursorPosition != null
&& rangeCursorPosition >= gridRect.top
&& rangeCursorPosition <= gridRect.bottom) {
hasRangeCursor = true;
canvas.drawLine(gridRect.left, rangeCursorPosition,
- gridRect.right, rangeCursorPosition, rangeCursorPaint);
+ gridRect.right, rangeCursorPosition, rangeCursorPaint
+ );
}
- if(getCursorLabelFormatter() != null && hasRangeCursor && hasDomainCursor) {
+ if (getCursorLabelFormatter() != null && hasRangeCursor && hasDomainCursor) {
drawCursorLabel(canvas);
}
}
@@ -720,8 +757,10 @@ protected void drawCursorLabel(Canvas canvas) {
// if we are too close to the right edge of the plot, we will move
// the label to the left side of our cursor:
if (cursorRect.right >= gridRect.right) {
- cursorRect.offsetTo(domainCursorPosition - cursorRect.width(),
- cursorRect.top);
+ cursorRect.offsetTo(
+ domainCursorPosition - cursorRect.width(),
+ cursorRect.top
+ );
}
// same thing for the top edge of the plot:
@@ -735,11 +774,12 @@ protected void drawCursorLabel(Canvas canvas) {
}
canvas.drawText(label, cursorRect.left, cursorRect.bottom,
- getCursorLabelFormatter().getTextPaint());
+ getCursorLabelFormatter().getTextPaint()
+ );
}
protected void drawGridBackground(Canvas canvas) {
- if(gridBackgroundPaint != null) {
+ if (gridBackgroundPaint != null) {
canvas.drawRect(gridRect, gridBackgroundPaint);
}
}
@@ -748,22 +788,21 @@ protected void drawGridBackground(Canvas canvas) {
* Draws lines and points for each element in the series.
*
* @param canvas
- * @throws PlotRenderException
*/
- protected void drawData(Canvas canvas) throws PlotRenderException {
+ protected void drawData(Canvas canvas) {
if (drawGridOnTop) {
drawGridBackground(canvas);
}
try {
- if(isGridClippingEnabled) {
- canvas.save(Canvas.ALL_SAVE_FLAG);
+ if (isGridClippingEnabled) {
+ canvas.save();
canvas.clipRect(gridRect, android.graphics.Region.Op.INTERSECT);
}
renderStack.sync();
- for(RenderStack.StackElement thisElement : renderStack.getElements()) {
- if(thisElement.isEnabled()) {
+ for (RenderStack.StackElement thisElement : renderStack.getElements()) {
+ if (thisElement.isEnabled()) {
Class extends XYSeriesRenderer> rendererClass =
thisElement.get().getFormatter().getRendererClass();
plot.getRenderer(rendererClass).render(
@@ -772,7 +811,7 @@ protected void drawData(Canvas canvas) throws PlotRenderException {
}
} finally {
- if(isGridClippingEnabled) {
+ if (isGridClippingEnabled) {
canvas.restore();
}
}
@@ -799,6 +838,7 @@ public Paint getDomainGridLinePaint() {
/**
* Set the paint used to draw the domain grid line.
+ *
* @param gridLinePaint
*/
public void setDomainGridLinePaint(Paint gridLinePaint) {
@@ -821,6 +861,7 @@ public Paint getDomainSubGridLinePaint() {
/**
* Set the paint used to draw the domain grid line.
+ *
* @param gridLinePaint
*/
public void setDomainSubGridLinePaint(Paint gridLinePaint) {
@@ -829,6 +870,7 @@ public void setDomainSubGridLinePaint(Paint gridLinePaint) {
/**
* Set the Paint used to draw the range grid line.
+ *
* @param gridLinePaint
*/
public void setRangeGridLinePaint(Paint gridLinePaint) {
@@ -844,6 +886,7 @@ public Paint getRangeSubGridLinePaint() {
/**
* Set the Paint used to draw the range grid line.
+ *
* @param gridLinePaint
*/
public void setRangeSubGridLinePaint(Paint gridLinePaint) {
@@ -882,36 +925,57 @@ public void setRangeOriginLinePaint(Paint rangeOriginLinePaint) {
this.rangeOriginLinePaint = rangeOriginLinePaint;
}
- public void setCursorPosition(float x, float y) {
+ /**
+ * Set domain and range cursor position using screen coordinates
+ *
+ * @param x
+ * @param y
+ */
+ public void setCursorPosition(Float x, Float y) {
setDomainCursorPosition(x);
setRangeCursorPosition(y);
}
+ /**
+ * Set domain and range cursor position using screen coordinates
+ *
+ * @param point
+ */
public void setCursorPosition(PointF point) {
setCursorPosition(point.x, point.y);
}
- public float getDomainCursorPosition() {
+ public Float getDomainCursorPosition() {
return domainCursorPosition;
}
public Number getDomainCursorVal() {
- return getXVal(getDomainCursorPosition());
+ return screenToSeriesX(getDomainCursorPosition());
}
- public void setDomainCursorPosition(float domainCursorPosition) {
+ /**
+ * Set domain cursor position using screen coordinates
+ *
+ * @param domainCursorPosition
+ */
+ public void setDomainCursorPosition(Float domainCursorPosition) {
this.domainCursorPosition = domainCursorPosition;
}
- public float getRangeCursorPosition() {
+ public Float getRangeCursorPosition() {
return rangeCursorPosition;
}
public Number getRangeCursorVal() {
- return getYVal(getRangeCursorPosition());
+ return screenToSeriesY(getRangeCursorPosition());
}
- public void setRangeCursorPosition(float rangeCursorPosition) {
+ /**
+ * Set range cursor position using screen coordinates
+ *
+ * @param rangeCursorPosition
+ */
+ public void setRangeCursorPosition(Float rangeCursorPosition) {
this.rangeCursorPosition = rangeCursorPosition;
}
@@ -936,9 +1000,8 @@ public Paint getDomainCursorPaint() {
}
/**
- *
- * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line.
- * Set to null (default) to disable.
+ * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line. Set to null
+ * (default) to disable.
*/
public void setDomainCursorPaint(Paint domainCursorPaint) {
this.domainCursorPaint = domainCursorPaint;
@@ -949,9 +1012,8 @@ public Paint getRangeCursorPaint() {
}
/**
- *
- * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line.
- * Set to null (default) to disable.
+ * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line. Set to null
+ * (default) to disable.
*/
public void setRangeCursorPaint(Paint rangeCursorPaint) {
this.rangeCursorPaint = rangeCursorPaint;
@@ -989,8 +1051,8 @@ public void setLineExtensionRight(float lineExtensionRight) {
this.lineExtensionRight = lineExtensionRight;
}
- protected HashMap getDefaultLineLabelStyles() {
- HashMap defaults = new HashMap<>();
+ protected Map getDefaultLineLabelStyles() {
+ EnumMap defaults = new EnumMap<>(Edge.class);
defaults.put(Edge.TOP, new LineLabelStyle());
defaults.put(Edge.BOTTOM, new LineLabelStyle());
defaults.put(Edge.LEFT, new LineLabelStyle());
@@ -998,8 +1060,8 @@ protected HashMap getDefaultLineLabelStyles() {
return defaults;
}
- protected HashMap getDefaultLineLabelRenderers() {
- HashMap defaults = new HashMap<>();
+ protected Map getDefaultLineLabelRenderers() {
+ EnumMap defaults = new EnumMap<>(Edge.class);
defaults.put(Edge.TOP, new LineLabelRenderer());
defaults.put(Edge.BOTTOM, new LineLabelRenderer());
defaults.put(Edge.LEFT, new LineLabelRenderer());
@@ -1041,6 +1103,7 @@ public Insets getGridInsets() {
public void setGridInsets(Insets gridInsets) {
this.gridInsets = gridInsets;
+ recalculateSizes(null);
}
/**
@@ -1052,6 +1115,7 @@ public Insets getLineLabelInsets() {
public void setLineLabelInsets(Insets lineLabelInsets) {
this.lineLabelInsets = lineLabelInsets;
+ recalculateSizes(null);
}
public RectF getGridRect() {
@@ -1083,22 +1147,20 @@ public boolean isLineLabelEnabled(Edge position) {
}
public void setLineLabelEdges(Edge... positions) {
- Set positionSet = new HashSet<>();
- if(positions != null) {
- for(Edge position : positions) {
- positionSet.add(position);
- }
+ EnumSet positionSet = EnumSet.noneOf(Edge.class);
+ if (positions != null) {
+ Collections.addAll(positionSet, positions);
}
- setLineLabelEdges(positionSet);
+ this.lineLabelEdges = positionSet;
}
- public void setLineLabelEdges(Set positions) {
- this.lineLabelEdges = positions;
+ public void setLineLabelEdges(Collection positions) {
+ this.lineLabelEdges = EnumSet.copyOf(positions);
}
protected void setLineLabelEdges(int bitfield) {
- for(Edge tp : Edge.values()) {
- if((tp.value & bitfield) == tp.value) {
+ for (Edge tp : Edge.values()) {
+ if ((tp.value & bitfield) == tp.value) {
lineLabelEdges.add(tp);
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java
new file mode 100644
index 00000000..51c67159
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java
@@ -0,0 +1,28 @@
+package com.androidplot.xy;
+
+import androidx.annotation.NonNull;
+
+import com.androidplot.ui.widget.LegendItem;
+
+public class XYLegendItem implements LegendItem {
+
+ public enum Type {
+ SERIES,
+ REGION
+ }
+
+ public final Type type;
+ public final Object item;
+ private final String text;
+
+ public XYLegendItem(@NonNull Type cellType, @NonNull Object item, @NonNull String text) {
+ this.type = cellType;
+ this.item = item;
+ this.text = text;
+ }
+
+ @Override
+ public String getTitle() {
+ return this.text;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java
index cb1feb79..680fd4b2 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java
@@ -17,233 +17,80 @@
package com.androidplot.xy;
import android.graphics.*;
+import androidx.annotation.NonNull;
+
import com.androidplot.ui.LayoutManager;
-import com.androidplot.ui.SeriesAndFormatter;
+import com.androidplot.ui.SeriesBundle;
import com.androidplot.ui.Size;
import com.androidplot.ui.TableModel;
-import com.androidplot.ui.widget.Widget;
-import com.androidplot.util.FontUtils;
+import com.androidplot.ui.widget.LegendWidget;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map.Entry;
/**
* Displays a legend for each series added to the owning {@link XYPlot}.
*/
-public class XYLegendWidget extends Widget {
-
- /**
- * This class is of no use outside of XYLegendWidget. It's just used to alphabetically sort
- * Region legend entries.
- */
- private static class RegionEntryComparator implements Comparator> {
- @Override
- public int compare(Map.Entry o1, Map.Entry o2) {
- return o1.getValue().compareTo(o2.getValue());
- }
- }
-
- private enum CellType {
- SERIES,
- REGION
- }
+public class XYLegendWidget extends LegendWidget {
private XYPlot plot;
- //private float iconWidth = 12;
- private Paint textPaint;
- private Paint iconBorderPaint;
- private TableModel tableModel;
- private boolean drawIconBackgroundEnabled = true;
- private boolean drawIconBorderEnabled = true;
-
- private Size iconSize;
- private static final RegionEntryComparator regionEntryComparator = new RegionEntryComparator();
- //private RectF iconRect = new RectF(0, 0, ICON_WIDTH_DEFAULT, ICON_HEIGHT_DEFAULT);
-
- {
- textPaint = new Paint();
- textPaint.setColor(Color.LTGRAY);
- textPaint.setAntiAlias(true);
-
- iconBorderPaint = new Paint();
- iconBorderPaint.setStyle(Paint.Style.STROKE);
- //regionEntryComparator = new RegionEntryComparator();
- }
public XYLegendWidget(LayoutManager layoutManager, XYPlot plot,
Size widgetSize,
TableModel tableModel,
Size iconSize) {
- super(layoutManager, widgetSize);
+ super(tableModel, layoutManager, widgetSize, iconSize);
this.plot = plot;
- setTableModel(tableModel);
- this.iconSize = iconSize;
- }
-
- public synchronized void setTableModel(TableModel tableModel) {
- this.tableModel = tableModel;
- }
- private RectF getIconRect(RectF cellRect) {
- float cellRectCenterY = cellRect.top + (cellRect.height()/2);
- RectF iconRect = iconSize.getRectF(cellRect);
-
- // center the icon rect vertically
- float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2);
- iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY);
- return iconRect;
- }
-
- private static float getRectCenterY(RectF cellRect) {
- return cellRect.top + (cellRect.height()/2);
- }
-
- private void beginDrawingCell(Canvas canvas, RectF iconRect) {
-
- Paint bgPaint = plot.getGraph().getGridBackgroundPaint();
- if(drawIconBackgroundEnabled && bgPaint != null) {
- canvas.drawRect(iconRect, bgPaint);
- }
- }
-
- private void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, String text) {
-
- Paint bgPaint = plot.getGraph().getGridBackgroundPaint();
- if(drawIconBorderEnabled && bgPaint != null) {
- iconBorderPaint.setColor(bgPaint.getColor());
- canvas.drawRect(iconRect, iconBorderPaint);
- }
-
- float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2);
-
- if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) {
- canvas.drawText(text, iconRect.left - 2, centeredTextOriginY, textPaint);
- } else {
- canvas.drawText(text, iconRect.right + 2, centeredTextOriginY, textPaint);
- }
+ // Set a default comparator that sorts by type and then alphabetically
+ setLegendItemComparator(new Comparator() {
+ @Override
+ public int compare(XYLegendItem o1, XYLegendItem o2) {
+ if(o1.type == o2.type) {
+ return o1.getTitle().compareTo(o2.getTitle());
+ } else {
+ return(o1.type.compareTo(o2.type));
+ }
+ }
+ });
}
protected void drawRegionLegendIcon(Canvas canvas, RectF rect, XYRegionFormatter formatter) {
- canvas.drawRect(rect, formatter.getPaint());
- }
-
- private void drawRegionLegendCell(Canvas canvas, XYRegionFormatter formatter, RectF cellRect, String text) {
- RectF iconRect = getIconRect(cellRect);
- beginDrawingCell(canvas, iconRect);
-
- drawRegionLegendIcon(
- canvas,
- iconRect,
- formatter
- );
- finishDrawingCell(canvas, cellRect, iconRect, text);
- }
-
- private void drawSeriesLegendCell(Canvas canvas, XYSeriesRenderer renderer, XYSeriesFormatter formatter, RectF cellRect, String seriesTitle) {
- RectF iconRect = getIconRect(cellRect);
- beginDrawingCell(canvas, iconRect);
-
- renderer.drawSeriesLegendIcon(
- canvas,
- iconRect,
- formatter);
- finishDrawingCell(canvas, cellRect, iconRect, seriesTitle);
+ canvas.drawRect(rect, formatter.getPaint());
}
- protected List> getLegendEnabledSeriesAndFormatterList() {
- List> sfList = new ArrayList<>();
- ListIterator> it = plot.getSeriesRegistry().listIterator();
- while(it.hasNext()) {
- SeriesAndFormatter thisSf = it.next();
- if(thisSf.getFormatter().isLegendIconEnabled()) {
- sfList.add(thisSf);
- }
+ @Override
+ protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull XYLegendItem XYLegendItem) {
+ switch (XYLegendItem.type) {
+ case REGION:
+ drawRegionLegendIcon(canvas, iconRect, (XYRegionFormatter) XYLegendItem.item);
+ break;
+ case SERIES:
+ final XYSeriesFormatter formatter = (XYSeriesFormatter) XYLegendItem.item;
+ plot.getRenderer(formatter.getRendererClass()).drawSeriesLegendIcon(canvas, iconRect, formatter);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unexpected item type: " + XYLegendItem.type);
}
- return sfList;
}
@Override
- protected synchronized void doOnDraw(Canvas canvas, RectF widgetRect) {
- if(plot.isEmpty()) {
- return;
+ protected List getLegendItems() {
+ final ArrayList items = new ArrayList<>();
+ for (SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) {
+ items.add(new XYLegendItem(XYLegendItem.Type.SERIES, sfPair.getFormatter(), sfPair.getSeries().getTitle()));
}
- // Keep an alphabetically sorted list of regions:
- TreeSet> sortedRegions = new TreeSet>(new RegionEntryComparator());
-
- // Calculate the number of cells needed to draw the Legend:
- int seriesCount = plot.getSeriesRegistry().size();
-
- for(XYSeriesRenderer renderer : plot.getRendererList()) {
+ for (XYSeriesRenderer renderer : plot.getRendererList()) {
Hashtable urf = renderer.getUniqueRegionFormatters();
- sortedRegions.addAll(urf.entrySet());
- }
-
- seriesCount += sortedRegions.size();
-
- // Create an iterator specially created to draw the number of cells we calculated:
- Iterator it = tableModel.getIterator(widgetRect, seriesCount);
-
- RectF cellRect;
-
- // draw each series legend item:
- for(SeriesAndFormatter sfPair : getLegendEnabledSeriesAndFormatterList()) {
- //for(SeriesAndFormatter sfPair : plot.getSeriesRegistry()) {
- cellRect = it.next();
- XYSeriesFormatter format = sfPair.getFormatter();
- drawSeriesLegendCell(canvas, plot.getRenderer(sfPair.getFormatter().getRendererClass()),
- format, cellRect, sfPair.getSeries().getTitle());
- }
-
- // draw each region legend item:
- for(Map.Entry entry : sortedRegions) {
- if(!it.hasNext()) {
- break;
+ for (Entry entry : urf.entrySet()) {
+ items.add(new XYLegendItem(XYLegendItem.Type.REGION, entry.getKey(), entry.getValue()));
}
- cellRect = it.next();
- XYRegionFormatter formatter = entry.getKey();
- drawRegionLegendCell(canvas, formatter, cellRect, entry.getValue());
}
- }
-
-
- public Paint getTextPaint() {
- return textPaint;
- }
-
- public void setTextPaint(Paint textPaint) {
- this.textPaint = textPaint;
- }
-
- public boolean isDrawIconBackgroundEnabled() {
- return drawIconBackgroundEnabled;
- }
-
- public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) {
- this.drawIconBackgroundEnabled = drawIconBackgroundEnabled;
- }
-
- public boolean isDrawIconBorderEnabled() {
- return drawIconBorderEnabled;
- }
-
- public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) {
- this.drawIconBorderEnabled = drawIconBorderEnabled;
- }
-
- public TableModel getTableModel() {
- return tableModel;
- }
-
- public Size getIconSize() {
- return iconSize;
- }
- /**
- * Set the size of each legend's icon. Note that when using relative sizing,
- * the size is calculated against the countaining cell's size, not the plot's size.
- * @param iconSize
- */
- public void setIconSize(Size iconSize) {
- this.iconSize = iconSize;
+ return items;
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java
index 4b709de0..e519b5a0 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java
@@ -22,11 +22,18 @@
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
+import androidx.annotation.NonNull;
import android.util.AttributeSet;
+
import com.androidplot.Plot;
import com.androidplot.R;
-import com.androidplot.ui.*;
+import com.androidplot.ui.Anchor;
+import com.androidplot.ui.DynamicTableModel;
+import com.androidplot.ui.HorizontalPositioning;
+import com.androidplot.ui.Size;
+import com.androidplot.ui.SizeMode;
import com.androidplot.ui.TextOrientation;
+import com.androidplot.ui.VerticalPositioning;
import com.androidplot.ui.widget.TextLabelWidget;
import com.androidplot.util.AttrUtils;
import com.androidplot.util.PixelUtils;
@@ -39,10 +46,7 @@
/**
* A View to graphically display x/y coordinates.
*/
-public class XYPlot extends Plot {
-
- private static final int DEFAULT_LEGEND_WIDGET_H_DP = 10;
- private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 7;
+public class XYPlot extends Plot {
private static final int DEFAULT_GRAPH_WIDGET_H_DP = 18;
private static final int DEFAULT_GRAPH_WIDGET_W_DP = 10;
@@ -53,8 +57,11 @@ public class XYPlot extends Plot
private static final int DEFAULT_RANGE_LABEL_WIDGET_H_DP = 50;
private static final int DEFAULT_RANGE_LABEL_WIDGET_W_DP = 10;
+ private static final int DEFAULT_LEGEND_WIDGET_H_DP = 10;
+ private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 7;
private static final int DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP = 0;
private static final int DEFAULT_LEGEND_WIDGET_X_OFFSET_DP = 40;
+
private static final int DEFAULT_GRAPH_WIDGET_Y_OFFSET_DP = 0;
private static final int DEFAULT_GRAPH_WIDGET_X_OFFSET_DP = 0;
@@ -83,11 +90,7 @@ public class XYPlot extends Plot
private XYConstraints constraints = new XYConstraints();
- // these are the final min/max used for dispplaying data
-// private Number calculatedMinX;
-// private Number calculatedMaxX;
-// private Number calculatedMinY;
-// private Number calculatedMaxY;
+ // min/max used for displaying data
private RectRegion bounds = RectRegion.withDefaults(new RectRegion(-1, 1, -1, 1));
// previous calculated min/max vals.
@@ -97,16 +100,14 @@ public class XYPlot extends Plot
private Number prevMinY;
private Number prevMaxY;
- // uses set boundary min and max values
- // should be null if not used.
- private Number rangeTopMin = null;
- private Number rangeTopMax = null;
- private Number rangeBottomMin = null;
- private Number rangeBottomMax = null;
- private Number domainLeftMin = null;
- private Number domainLeftMax = null;
- private Number domainRightMin = null;
- private Number domainRightMax = null;
+ /**
+ * The inner and outer limits define a kind of picture-frame shape area that is used as the valid
+ * region for setting domain/range boundaries. If the set boundaries exceed one of these limits
+ * then the limit value is used instead of the boundary. This is most commonly used to constrain
+ * panning & zooming to a specific range on both axes.
+ */
+ private final RectRegion innerLimits = new RectRegion();
+ private final RectRegion outerLimits = new RectRegion();
private Number userDomainOrigin;
private Number userRangeOrigin;
@@ -123,6 +124,7 @@ public class XYPlot extends Plot
private ArrayList xValueMarkers;
private PreviewMode previewMode;
+
public enum PreviewMode {
LineAndPoint,
Candlestick,
@@ -330,10 +332,12 @@ protected void processAttrs(TypedArray attrs) {
protected void notifyListenersBeforeDraw(Canvas canvas) {
super.notifyListenersBeforeDraw(canvas);
+ calculateMinMaxVals();
+
// this call must be AFTER the notify so that if the listener
// is a synchronized series, it has the opportunity to
// place a read lock on it's data.
- calculateMinMaxVals();
+ getRegistry().estimate(this); // TODO: clean this mechanism up!!!
}
/**
@@ -365,12 +369,91 @@ public void setCursorPosition(float x, float y) {
getGraph().setCursorPosition(x, y);
}
+ /**
+ * Convert a screen xVal into a series xVal.
+ * @param xPix
+ * @return
+ * @deprecated Use {@link #screenToSeriesY(float)}.
+ */
+ @Deprecated
+ public Number getXVal(float xPix) {
+ return getGraph().screenToSeriesX(xPix);
+ }
+
+ /**
+ * Convert a screen yVal into a series yVal.
+ * @param yPix
+ * @return
+ * @deprecated Use {@link #screenToSeriesY(float)}.
+ */
+ public Number getYVal(float yPix) {
+ return getGraph().screenToSeriesY(yPix);
+ }
+
+ /**
+ * Convert the y coord of a PointF into a series yVal.
+ * @param point
+ * @return
+ * @deprecated Use {@link #screenToSeriesY(float)}.
+ */
+ @Deprecated
public Number getYVal(PointF point) {
- return getGraph().getYVal(point);
+ return getGraph().screenToSeriesY(point);
}
+ /**
+ * Convert the x coord of a PointF into a series xVal.
+ * @param point
+ * @return
+ * @deprecated Use {@link #screenToSeriesY(float)}.
+ */
+ @Deprecated
public Number getXVal(PointF point) {
- return getGraph().getXVal(point);
+ return getGraph().screenToSeriesX(point);
+ }
+
+ public Number screenToSeriesX(float x) {
+ return getGraph().screenToSeriesX(x);
+ }
+
+ public Number screenToSeriesY(float y) {
+ return getGraph().screenToSeriesY(y);
+ }
+
+ /**
+ * Convert a series xVal into a screen x coord
+ * @param x
+ * @return
+ */
+ public float seriesToScreenX(Number x) {
+ return getGraph().seriesToScreenX(x);
+ }
+
+ /**
+ * Convert a series yVal into a screen y coord
+ * @param y
+ * @return
+ */
+ public float seriesToScreenY(Number y) {
+ return getGraph().seriesToScreenY(y);
+ }
+
+ /**
+ * Convert a series xy value into a screen point.
+ * @param xy
+ * @return
+ */
+ public PointF seriesToScreen(XYCoords xy) {
+ return getGraph().seriesToScreen(xy);
+ }
+
+ /**
+ * Convert a screen point into a series xy value.
+ * @param point
+ * @return
+ */
+ public XYCoords screentoSeries(PointF point) {
+ return getGraph().screenToSeries(point);
}
public void calculateMinMaxVals() {
@@ -387,7 +470,7 @@ public void calculateMinMaxVals() {
// only calculate if we must:
if(!bounds.isFullyDefined()) {
- RectRegion b = SeriesUtils.minMax(constraints, getSeriesRegistry().getSeriesList());
+ RectRegion b = SeriesUtils.minMax(constraints, getRegistry().getSeriesList());
if(!bounds.isMinXSet()) {
bounds.setMinX(b.getMinX());
@@ -414,11 +497,11 @@ public void calculateMinMaxVals() {
case EDGE:
bounds.setMaxX(applyUserMinMax(getCalculatedUpperBoundary(
constraints.getDomainUpperBoundaryMode(), prevMaxX, bounds.getMaxX()),
- domainRightMin, domainRightMax));
+ innerLimits.getMaxX(), outerLimits.getMaxX()));
bounds.setMinX(applyUserMinMax(getCalculatedLowerBoundary(
constraints.getDomainLowerBoundaryMode(),
prevMinX, bounds.getMinX()),
- domainLeftMin, domainLeftMax));
+ outerLimits.getMinX(), innerLimits.getMinX()));
break;
default:
throw new UnsupportedOperationException(
@@ -430,13 +513,13 @@ public void calculateMinMaxVals() {
updateRangeMinMaxForOriginModel();
break;
case EDGE:
- if (getSeriesRegistry().size() > 0) {
+ if (getRegistry().size() > 0) {
bounds.setMaxY(applyUserMinMax(getCalculatedUpperBoundary(
constraints.getRangeUpperBoundaryMode(),
- prevMaxY, bounds.getMaxY()), rangeTopMin, rangeTopMax));
+ prevMaxY, bounds.getMaxY()), innerLimits.getMaxY(), outerLimits.getMaxY()));
bounds.setMinY(applyUserMinMax(getCalculatedLowerBoundary(
constraints.getRangeLowerBoundaryMode(),
- prevMinY, bounds.getMinY()), rangeBottomMin, rangeBottomMax));
+ prevMinY, bounds.getMinY()), outerLimits.getMinY(), innerLimits.getMinY()));
}
break;
default:
@@ -504,7 +587,7 @@ protected Number getCalculatedLowerBoundary(BoundaryMode mode, Number previousMi
* @param min
* @param max
*/
- private Number applyUserMinMax(Number value, Number min, Number max) {
+ private static Number applyUserMinMax(Number value, Number min, Number max) {
value = (((min == null) || (value == null) || (value.doubleValue() > min.doubleValue()))
? value
: min);
@@ -519,7 +602,7 @@ private Number applyUserMinMax(Number value, Number min, Number max) {
*
* @param origin
*/
- public void centerOnDomainOrigin(Number origin) {
+ public void centerOnDomainOrigin(@NonNull Number origin) {
centerOnDomainOrigin(origin, null, BoundaryMode.AUTO);
}
@@ -531,9 +614,9 @@ public void centerOnDomainOrigin(Number origin) {
* @param extent
* @param mode
*/
- public void centerOnDomainOrigin(Number origin, Number extent, BoundaryMode mode) {
+ public void centerOnDomainOrigin(@NonNull Number origin, Number extent, BoundaryMode mode) {
if (origin == null) {
- throw new NullPointerException("Origin param cannot be null.");
+ throw new IllegalArgumentException("Origin param cannot be null.");
}
constraints.setDomainFramingModel(XYFramingModel.ORIGIN);
setUserDomainOrigin(origin);
@@ -550,7 +633,7 @@ public void centerOnDomainOrigin(Number origin, Number extent, BoundaryMode mode
*
* @param origin
*/
- public void centerOnRangeOrigin(Number origin) {
+ public void centerOnRangeOrigin(@NonNull Number origin) {
centerOnRangeOrigin(origin, null, BoundaryMode.AUTO);
}
@@ -563,9 +646,9 @@ public void centerOnRangeOrigin(Number origin) {
* @param mode
*/
@SuppressWarnings("SameParameterValue")
- public void centerOnRangeOrigin(Number origin, Number extent, BoundaryMode mode) {
+ public void centerOnRangeOrigin(@NonNull Number origin, Number extent, BoundaryMode mode) {
if (origin == null) {
- throw new NullPointerException("Origin param cannot be null.");
+ throw new IllegalArgumentException("Origin param cannot be null.");
}
constraints.setRangeFramingModel(XYFramingModel.ORIGIN);
setUserRangeOrigin(origin);
@@ -601,7 +684,7 @@ protected Number[] getOriginMinMax(BoundaryMode mode, Number origin, Number exte
* @param y
* @return
*/
- private double distance(double x, double y) {
+ private static double distance(double x, double y) {
if (x > y) {
return x - y;
} else {
@@ -611,15 +694,15 @@ private double distance(double x, double y) {
public void updateDomainMinMaxForOriginModel() {
double origin = userDomainOrigin.doubleValue();
- double maxXDelta = distance(bounds.getMaxX().doubleValue(), origin);
- double minXDelta = distance(bounds.getMinX().doubleValue(), origin);
- double delta = maxXDelta > minXDelta ? maxXDelta : minXDelta;
- double dlb = origin - delta;
- double dub = origin + delta;
+ double maxDelta = distance(bounds.getMaxX().doubleValue(), origin);
+ double minDelta = distance(bounds.getMinX().doubleValue(), origin);
+ double delta = maxDelta > minDelta ? maxDelta : minDelta;
+ double lowerBoundary = origin - delta;
+ double upperBoundary = origin + delta;
switch (domainOriginBoundaryMode) {
case AUTO:
- bounds.setMinX(dlb);
- bounds.setMaxX(dub);
+ bounds.setMinX(lowerBoundary);
+ bounds.setMaxX(upperBoundary);
break;
// if fixed, then the value already exists within "user" vals.
@@ -627,28 +710,28 @@ public void updateDomainMinMaxForOriginModel() {
break;
case GROW: {
- if (prevMinX == null || dlb < prevMinX.doubleValue()) {
- bounds.setMinX(dlb);
+ if (prevMinX == null || lowerBoundary < prevMinX.doubleValue()) {
+ bounds.setMinX(lowerBoundary);
} else {
bounds.setMinX(prevMinX);
}
- if (prevMaxX == null || dub > prevMaxX.doubleValue()) {
- bounds.setMaxX(dub);
+ if (prevMaxX == null || upperBoundary > prevMaxX.doubleValue()) {
+ bounds.setMaxX(upperBoundary);
} else {
bounds.setMaxX(prevMaxX);
}
}
break;
case SHRINK:
- if (prevMinX == null || dlb > prevMinX.doubleValue()) {
- bounds.setMinX(dlb);
+ if (prevMinX == null || lowerBoundary > prevMinX.doubleValue()) {
+ bounds.setMinX(lowerBoundary);
} else {
bounds.setMinX(prevMinX);
}
- if (prevMaxX == null || dub < prevMaxX.doubleValue()) {
- bounds.setMaxX(dub);
+ if (prevMaxX == null || upperBoundary < prevMaxX.doubleValue()) {
+ bounds.setMaxX(upperBoundary);
} else {
bounds.setMaxX(prevMaxX);
}
@@ -662,14 +745,14 @@ public void updateRangeMinMaxForOriginModel() {
switch (rangeOriginBoundaryMode) {
case AUTO:
double origin = userRangeOrigin.doubleValue();
- double maxYDelta = distance(bounds.getMaxY().doubleValue(), origin);
- double minYDelta = distance(bounds.getMinY().doubleValue(), origin);
- if (maxYDelta > minYDelta) {
- bounds.setMinY(origin - maxYDelta);
- bounds.setMaxY(origin + maxYDelta);
+ double maxDelta = distance(bounds.getMaxY().doubleValue(), origin);
+ double minDelta = distance(bounds.getMinY().doubleValue(), origin);
+ if (maxDelta > minDelta) {
+ bounds.setMinY(origin - maxDelta);
+ bounds.setMaxY(origin + maxDelta);
} else {
- bounds.setMinY(origin - minYDelta);
- bounds.setMaxY(origin + minYDelta);
+ bounds.setMinY(origin - minDelta);
+ bounds.setMaxY(origin + minDelta);
}
break;
case FIXED:
@@ -960,12 +1043,12 @@ public synchronized void setUserRangeOrigin(Number origin) {
}
@SuppressWarnings("SameParameterValue")
- protected void setDomainFramingModel(XYFramingModel model) {
+ protected void setDomainFramingModel(@NonNull XYFramingModel model) {
constraints.setDomainFramingModel(model);
}
@SuppressWarnings("SameParameterValue")
- protected void setRangeFramingModel(XYFramingModel model) {
+ protected void setRangeFramingModel(@NonNull XYFramingModel model) {
constraints.setRangeFramingModel(model);
}
@@ -1013,9 +1096,7 @@ public YValueMarker removeMarker(YValueMarker marker) {
* @return
*/
public int removeMarkers() {
- int removed = removeXMarkers();
- removed += removeYMarkers();
- return removed;
+ return removeXMarkers() + removeYMarkers();
}
/**
@@ -1073,124 +1154,12 @@ protected List getXValueMarkers() {
return xValueMarkers;
}
-// public RectRegion getDefaultBounds() {
-// return defaultBounds;
-// }
-//
-// public void setDefaultBounds(RectRegion defaultBounds) {
-// this.defaultBounds = defaultBounds;
-// }
-
- /**
- * @return the rangeTopMin
- */
- public Number getRangeTopMin() {
- return rangeTopMin;
- }
-
- /**
- * @param rangeTopMin the rangeTopMin to set
- */
- public synchronized void setRangeTopMin(Number rangeTopMin) {
- this.rangeTopMin = rangeTopMin;
- }
-
- /**
- * @return the rangeTopMax
- */
- public Number getRangeTopMax() {
- return rangeTopMax;
- }
-
- /**
- * @param rangeTopMax the rangeTopMax to set
- */
- public synchronized void setRangeTopMax(Number rangeTopMax) {
- this.rangeTopMax = rangeTopMax;
+ public RectRegion getInnerLimits() {
+ return innerLimits;
}
- /**
- * @return the rangeBottomMin
- */
- public Number getRangeBottomMin() {
- return rangeBottomMin;
- }
-
- /**
- * @param rangeBottomMin the rangeBottomMin to set
- */
- public synchronized void setRangeBottomMin(Number rangeBottomMin) {
- this.rangeBottomMin = rangeBottomMin;
- }
-
- /**
- * @return the rangeBottomMax
- */
- public Number getRangeBottomMax() {
- return rangeBottomMax;
- }
-
- /**
- * @param rangeBottomMax the rangeBottomMax to set
- */
- public synchronized void setRangeBottomMax(Number rangeBottomMax) {
- this.rangeBottomMax = rangeBottomMax;
- }
-
- /**
- * @return the domainLeftMin
- */
- public Number getDomainLeftMin() {
- return domainLeftMin;
- }
-
- /**
- * @param domainLeftMin the domainLeftMin to set
- */
- public synchronized void setDomainLeftMin(Number domainLeftMin) {
- this.domainLeftMin = domainLeftMin;
- }
-
- /**
- * @return the domainLeftMax
- */
- public Number getDomainLeftMax() {
- return domainLeftMax;
- }
-
- /**
- * @param domainLeftMax the domainLeftMax to set
- */
- public synchronized void setDomainLeftMax(Number domainLeftMax) {
- this.domainLeftMax = domainLeftMax;
- }
-
- /**
- * @return the domainRightMin
- */
- public Number getDomainRightMin() {
- return domainRightMin;
- }
-
- /**
- * @param domainRightMin the domainRightMin to set
- */
- public synchronized void setDomainRightMin(Number domainRightMin) {
- this.domainRightMin = domainRightMin;
- }
-
- /**
- * @return the domainRightMax
- */
- public Number getDomainRightMax() {
- return domainRightMax;
- }
-
- /**
- * @param domainRightMax the domainRightMax to set
- */
- public synchronized void setDomainRightMax(Number domainRightMax) {
- this.domainRightMax = domainRightMax;
+ public RectRegion getOuterLimits() {
+ return outerLimits;
}
public StepModel getDomainStepModel() {
@@ -1208,4 +1177,10 @@ public StepModel getRangeStepModel() {
public void setRangeStepModel(StepModel rangeStepModel) {
this.rangeStepModel = rangeStepModel;
}
+
+ @Override
+ protected XYSeriesRegistry getRegistryInstance() {
+ XYSeriesRegistry registry = new XYSeriesRegistry();
+ return registry;
+ }
}
\ No newline at end of file
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java
index b0a3186f..0492c6c2 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java
@@ -43,7 +43,11 @@ public class XYRegionFormatter {
public XYRegionFormatter(Context ctx, int xmlCfgId) {
// prevent configuration of classes derived from this one:
if (getClass().equals(XYRegionFormatter.class)) {
- Fig.configure(ctx, this, xmlCfgId);
+ try {
+ Fig.configure(ctx, this, xmlCfgId);
+ } catch (FigException e) {
+ throw new RuntimeException(e);
+ }
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java
index 8eb2d226..5afb8963 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java
@@ -47,5 +47,4 @@ public interface XYSeries extends Series {
* @return The y-value.
*/
Number getY(int index);
-
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java
new file mode 100644
index 00000000..fce15b59
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java
@@ -0,0 +1,13 @@
+package com.androidplot.xy;
+
+import com.androidplot.ui.*;
+
+/**
+ * Created by halfhp on 10/6/16.
+ */
+public class XYSeriesBundle extends SeriesBundle {
+
+ public XYSeriesBundle(XYSeries series, XYSeriesFormatter formatter) {
+ super(series, formatter);
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java
index 0c18d2e9..27924ab1 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java
@@ -16,7 +16,7 @@
package com.androidplot.xy;
-import android.content.*;
+import android.content.Context;
import com.androidplot.ui.Formatter;
import com.androidplot.util.LayerHash;
@@ -28,7 +28,7 @@ public abstract class XYSeriesFormatter {
+
+ private Estimator estimator;
+
+ public void estimate(XYPlot plot) {
+ if(estimator != null) {
+ for (XYSeriesBundle sf : getSeriesAndFormatterList()) {
+ getEstimator().run(plot, sf);
+ }
+ }
+ }
+
+ @Override
+ protected XYSeriesBundle newSeriesBundle(XYSeries series, XYSeriesFormatter formatter) {
+ return new XYSeriesBundle(series, formatter);
+ }
+
+ /**
+ *
+ * @return The currently active Estimator, or null if none is set.
+ */
+ public Estimator getEstimator() {
+ return estimator;
+ }
+
+ public void setEstimator(Estimator estimator) {
+ this.estimator = estimator;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java
index c77832cd..5617d411 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java
@@ -16,7 +16,7 @@
package com.androidplot.xy;
-import com.androidplot.ui.SeriesAndFormatter;
+import com.androidplot.ui.SeriesBundle;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.util.Layerable;
@@ -40,7 +40,7 @@ public XYSeriesRenderer(XYPlot plot) {
public Hashtable getUniqueRegionFormatters() {
Hashtable found = new Hashtable<>();
- for(SeriesAndFormatter sfPair : getSeriesAndFormatterList()) {
+ for(SeriesBundle sfPair : getSeriesAndFormatterList()) {
Layerable regionIndexer = sfPair.getFormatter().getRegions();
for (RectRegion region : regionIndexer.elements()) {
XYRegionFormatter f = sfPair.getFormatter().getRegionFormatter(region);
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java b/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java
index d90d9172..34de518d 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java
@@ -59,6 +59,7 @@ public static Step getStep(StepMode typeXY, double stepValue, Region realBounds,
double stepCount = 0;
switch(typeXY) {
case INCREMENT_BY_VAL:
+ case INCREMENT_BY_FIT:
stepVal = stepValue;
stepPix = stepValue / realBounds.ratio(pixelBounds).doubleValue();
stepCount = pixelBounds.length().doubleValue() / stepPix;
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/YValueMarker.java b/androidplot-core/src/main/java/com/androidplot/xy/YValueMarker.java
index f76dadd5..0e4aa435 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/YValueMarker.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/YValueMarker.java
@@ -16,7 +16,10 @@
package com.androidplot.xy;
+import android.graphics.Canvas;
import android.graphics.Paint;
+import android.graphics.RectF;
+
import com.androidplot.ui.HorizontalPositioning;
import com.androidplot.ui.HorizontalPosition;
@@ -55,4 +58,21 @@ public YValueMarker(Number value, String text, HorizontalPosition textPosition,
public YValueMarker(Number value, String text, HorizontalPosition textPosition, int linePaint, int textPaint) {
super(value, text, textPosition, linePaint, textPaint);
}
+
+ @Override
+ public void draw(Canvas canvas, XYPlot plot, RectF gridRect) {
+ if (getValue() != null) {
+ float yPix = (float) plot.getBounds().yRegion
+ .transform(getValue()
+ .doubleValue(), gridRect.top, gridRect.bottom, true);
+ canvas.drawLine(gridRect.left, yPix,
+ gridRect.right, yPix, getLinePaint()
+ );
+
+ float xPix = getTextPosition().getPixelValue(
+ gridRect.width());
+ xPix += gridRect.left;
+ drawMarkerText(canvas, getText(), gridRect, xPix, yPix);
+ }
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java b/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java
new file mode 100644
index 00000000..14b7583b
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java
@@ -0,0 +1,26 @@
+package com.androidplot.xy;
+
+/**
+ * Estimates optimal zoom level to be applied to a {@link SampledXYSeries} based on the current
+ * visible bounds of the owning {@link XYPlot}.
+ */
+public class ZoomEstimator extends Estimator {
+
+ @Override
+ public void run(XYPlot plot, XYSeriesBundle sf) {
+ if(sf.getSeries() instanceof SampledXYSeries) {
+ SampledXYSeries oxy = (SampledXYSeries) sf.getSeries();
+ final double factor = calculateZoom(oxy, plot.getBounds());
+ oxy.setZoomFactor(factor);
+ }
+ }
+
+ protected double calculateZoom(SampledXYSeries series, RectRegion visibleBounds) {
+ RectRegion seriesBounds = series.getBounds();
+ final double ratio = seriesBounds.getxRegion().ratio(visibleBounds.getxRegion()).doubleValue();
+ final double maxFactor = series.getMaxZoomFactor();
+ final double factor = Math.abs(Math.round(maxFactor / ratio));
+ return factor > 0 ? factor : 1;
+ }
+
+}
diff --git a/androidplot-core/src/main/res/values/attrs.xml b/androidplot-core/src/main/res/values/attrs.xml
index 76560ef0..5c714912 100644
--- a/androidplot-core/src/main/res/values/attrs.xml
+++ b/androidplot-core/src/main/res/values/attrs.xml
@@ -1,5 +1,5 @@
-
+
+
-
+
@@ -48,36 +60,35 @@
-
+
-
-
-
+
+
+
-
+
-
+
-
+
-
+
@@ -186,6 +197,15 @@
+
+
+
+
+
+
+
+
+
@@ -258,180 +278,857 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
+
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
-
-
+
+
+
+
+
+Enable line labels on one or more edge of the graph. For example, to enable labels on the left
+and bottom edges:
+```
+ap:lineLabels="left|bottom"
+```
+-->
+
+Text alignment of line labels drawn on top edge of the plot. This alignment is applied relative
+to the line label insets defined by `lineLabelInsetTop`.
+-->
+
+
-
+
+Text alignment of line labels drawn on right edge of the plot. This alignment is applied relative
+to the line label insets defined by `lineLabelInsetRight`.
+-->
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
+
\ No newline at end of file
diff --git a/androidplot-core/src/test/java/com/androidplot/PlotTest.java b/androidplot-core/src/test/java/com/androidplot/PlotTest.java
index 789207ea..9e4753bf 100644
--- a/androidplot-core/src/test/java/com/androidplot/PlotTest.java
+++ b/androidplot-core/src/test/java/com/androidplot/PlotTest.java
@@ -20,13 +20,11 @@
import android.graphics.*;
import android.util.*;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.test.*;
-import com.androidplot.ui.RenderStack;
-import com.androidplot.ui.SeriesRenderer;
-import com.androidplot.ui.Formatter;
+import com.androidplot.ui.*;
import com.halfhp.fig.*;
import org.junit.Test;
+import org.mockito.Mock;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.HashMap;
@@ -35,121 +33,20 @@
import static junit.framework.Assert.assertNotSame;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
public class PlotTest extends AndroidplotTest {
- static class MockPlotListener implements PlotListener {
-
- public void onBeforeDraw(Plot source, Canvas canvas) {}
-
- public void onAfterDraw(Plot source, Canvas canvas) {}
- }
-
- static class MockSeries implements Series {
-
- public String getTitle() {
- return null;
- }
-
- }
-
- static class MockSeries2 implements Series {
-
- public String getTitle() {
- return null;
- }
- }
-
- static class MockSeries3 implements Series {
-
- public String getTitle() {
- return null;
- }
- }
-
- static class MockRenderer1 extends SeriesRenderer {
-
- public MockRenderer1(Plot plot) {
- super(plot);
- }
-
- @Override
- public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) throws PlotRenderException {
-
- }
-
- @Override
- public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
-
- }
- }
- static class MockRenderer2 extends SeriesRenderer {
-
- public MockRenderer2(Plot plot) {
- super(plot);
- }
-
- @Override
- public void onRender(Canvas canvas, RectF plotArea, Series series, Formatter formatter, RenderStack stack) throws PlotRenderException {
-
- }
-
- @Override
- public void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
-
- }
- }
-
- static class MockFormatter1 extends Formatter {
-
- @Override
- public Class extends SeriesRenderer> getRendererClass() {
- return MockRenderer1.class;
- }
-
- @Override
- public SeriesRenderer doGetRendererInstance(MockPlot plot) {
- return new MockRenderer1(plot);
- }
- }
-
- static class MockFormatter2 extends Formatter {
-
- @Override
- public Class extends SeriesRenderer> getRendererClass() {
- return MockRenderer2.class;
- }
-
- @Override
- public SeriesRenderer doGetRendererInstance(MockPlot plot) {
- return new MockRenderer2(plot);
- }
- }
-
- public static class MockPlot extends Plot {
- public MockPlot(String title) {
- super(RuntimeEnvironment.application, title);
- }
-
- @Override
- protected void onPreInit() {
-
- }
-
- @Override
- protected void processAttrs(TypedArray attrs) {
-
- }
- }
+ @Mock
+ SeriesRegistry