> 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 f03e3e26..f33501d2 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java
@@ -16,6 +16,7 @@
package com.androidplot.xy;
+import android.content.*;
import android.graphics.Color;
import android.graphics.Paint;
import com.androidplot.ui.SeriesRenderer;
@@ -30,14 +31,6 @@ public class LineAndPointFormatter extends XYSeriesFormatter
private static final float DEFAULT_LINE_STROKE_WIDTH_DP = 1.5f;
private static final float DEFAULT_VERTEX_STROKE_WIDTH_DP = 4.5f;
- // default implementation prints point's yVal:
- private PointLabeler pointLabeler = new PointLabeler() {
- @Override
- public String getLabel(XYSeries series, int index) {
- return series.getY(index) + "";
- }
- };
-
public FillDirection getFillDirection() {
return fillDirection;
}
@@ -56,10 +49,9 @@ public void setFillDirection(FillDirection fillDirection) {
protected Paint vertexPaint;
protected Paint fillPaint;
protected InterpolationParams interpolationParams;
- private PointLabelFormatter pointLabelFormatter;
- {
- initLinePaint(Color.BLACK);
+ public LineAndPointFormatter(Context context, int xmlCfgId) {
+ super(context, xmlCfgId);
}
/**
@@ -69,12 +61,13 @@ public LineAndPointFormatter() {
this(Color.RED, Color.GREEN, Color.BLUE, null);
}
- public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, PointLabelFormatter plf) {
+ public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor,
+ PointLabelFormatter plf) {
this(lineColor, vertexColor, fillColor, plf, FillDirection.BOTTOM);
}
- public LineAndPointFormatter(Integer lineColor, Integer vertexColor,
- Integer fillColor, PointLabelFormatter plf, FillDirection fillDir) {
+ public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor,
+ PointLabelFormatter plf, FillDirection fillDir) {
initLinePaint(lineColor);
initVertexPaint(vertexColor);
initFillPaint(fillColor);
@@ -88,7 +81,7 @@ public Class extends SeriesRenderer> getRendererClass() {
}
@Override
- public SeriesRenderer getRendererInstance(XYPlot plot) {
+ public SeriesRenderer doGetRendererInstance(XYPlot plot) {
return new LineAndPointRenderer(plot);
}
@@ -127,20 +120,23 @@ protected void initFillPaint(Integer fillColor) {
}
/**
- * Enables the shadow layer on linePaint and shadowPaint by calling
- * setShadowLayer() with preset values.
+ *
+ * @return True if linePaint has been set, false otherwise.
*/
- public void enableShadows() {
- linePaint.setShadowLayer(1, 3, 3, Color.BLACK);
- vertexPaint.setShadowLayer(1, 3, 3, Color.BLACK);
- }
-
- public void disableShadows() {
- linePaint.setShadowLayer(0, 0, 0, Color.BLACK);
- vertexPaint.setShadowLayer(0, 0, 0, Color.BLACK);
+ public boolean hasLinePaint() {
+ return linePaint != null;
}
+ /**
+ * Get the {@link Paint} used to draw lines. Will instantiate and a new default instance
+ * if it is currently null. To run whether or not line paint has been set, use
+ * {@link #hasLinePaint()}.
+ * @return
+ */
public Paint getLinePaint() {
+ if(linePaint == null) {
+ initLinePaint(Color.TRANSPARENT);
+ }
return linePaint;
}
@@ -148,7 +144,24 @@ public void setLinePaint(Paint linePaint) {
this.linePaint = linePaint;
}
+ /**
+ *
+ * @return True if vertexPaint has been set, false otherwise.
+ */
+ public boolean hasVertexPaint() {
+ return vertexPaint != null;
+ }
+
+ /**
+ * Get the {@link Paint} used to draw vertices (points). Will instantiate and a new default instance
+ * if it is currently null. To run whether or not vertex paint has been set, use
+ * {@link #hasVertexPaint()}.
+ * @return
+ */
public Paint getVertexPaint() {
+ if(vertexPaint == null) {
+ initVertexPaint(Color.TRANSPARENT);
+ }
return vertexPaint;
}
@@ -156,7 +169,23 @@ public void setVertexPaint(Paint vertexPaint) {
this.vertexPaint = vertexPaint;
}
+ /**
+ *
+ * @return True if fillPaint has been set, false otherwise.
+ */
+ public boolean hasFillPaint() {
+ return fillPaint != null;
+ }
+ /**
+ * Get the {@link Paint} used to fill series areas. Will instantiate and a new default instance
+ * if it is currently null. To run whether or not fill paint has been set, use
+ * {@link #hasFillPaint()}.
+ * @return
+ */
public Paint getFillPaint() {
+ if(fillPaint == null) {
+ initFillPaint(Color.TRANSPARENT);
+ }
return fillPaint;
}
@@ -164,22 +193,6 @@ public void setFillPaint(Paint fillPaint) {
this.fillPaint = fillPaint;
}
- public PointLabelFormatter getPointLabelFormatter() {
- return pointLabelFormatter;
- }
-
- public void setPointLabelFormatter(PointLabelFormatter pointLabelFormatter) {
- this.pointLabelFormatter = pointLabelFormatter;
- }
-
- public PointLabeler getPointLabeler() {
- return pointLabeler;
- }
-
- public void setPointLabeler(PointLabeler pointLabeler) {
- this.pointLabeler = pointLabeler;
- }
-
public InterpolationParams getInterpolationParams() {
return interpolationParams;
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
index bfe019fb..f50d6186 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java
@@ -16,12 +16,21 @@
package com.androidplot.xy;
-import android.graphics.*;
-import com.androidplot.exception.PlotRenderException;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import com.androidplot.Plot;
+import com.androidplot.PlotListener;
+import com.androidplot.Region;
import com.androidplot.ui.RenderStack;
-import com.androidplot.util.ValPixConverter;
+import com.androidplot.util.*;
+
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Renders a point as a line with the vertices marked. Requires 2 or more points to
@@ -32,12 +41,28 @@ public class LineAndPointRenderer e
protected static final int ZERO = 0;
protected static final int ONE = 1;
+ private final Path path = new Path();
+
+ protected final ConcurrentHashMap> pointsCaches
+ = new ConcurrentHashMap<>(2, 0.75f, 2);
+
public LineAndPointRenderer(XYPlot plot) {
super(plot);
+ plot.addListener(new PlotListener() {
+ @Override
+ public void onBeforeDraw(Plot source, Canvas canvas) {
+ cullPointsCache();
+ }
+
+ @Override
+ public void onAfterDraw(Plot source, Canvas canvas) {
+
+ }
+ });
}
@Override
- public void onRender(Canvas canvas, RectF plotArea, XYSeries series, FormatterType formatter, RenderStack stack) throws PlotRenderException {
+ public void onRender(Canvas canvas, RectF plotArea, XYSeries series, FormatterType formatter, RenderStack stack) {
drawSeries(canvas, plotArea, series, formatter);
}
@@ -50,11 +75,11 @@ public void doDrawLegendIcon(Canvas canvas, RectF rect, LineAndPointFormatter fo
if(formatter.getFillPaint() != null) {
canvas.drawRect(rect, formatter.getFillPaint());
}
- if(formatter.getLinePaint() != null) {
+ if(formatter.hasLinePaint()) {
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
}
- if(formatter.getVertexPaint() != null) {
+ if(formatter.hasVertexPaint()) {
canvas.drawPoint(centerX, centerY, formatter.getVertexPaint());
}
}
@@ -68,38 +93,86 @@ protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) {
path.lineTo(thisPoint.x, thisPoint.y);
}
+ /**
+ * Retrieves or initializes a list for storing calculated screen-coords to render as points.
+ * Also handles automatic resizing and culling of unused caches.
+ * Should only be called once per render cycle.
+ * @param series
+ * @return
+ */
+ protected ArrayList getPointsCache(XYSeries series) {
+ ArrayList pointsCache = pointsCaches.get(series);
+ final int seriesSize = series.size();
+ if(pointsCache == null) {
+ pointsCache = new ArrayList<>(seriesSize);
+ pointsCaches.put(series, pointsCache);
+ }
+
+ if(pointsCache.size() < seriesSize) {
+ while(pointsCache.size() < seriesSize) {
+ pointsCache.add(null);
+ }
+ } else if(pointsCache.size() > seriesSize) {
+ while(pointsCache.size() > seriesSize) {
+ pointsCache.remove(0);
+ }
+ }
+ return pointsCache;
+ }
+
+ protected void cullPointsCache() {
+ for(XYSeries series : pointsCaches.keySet()) {
+ if(!getPlot().getRegistry().contains(series, LineAndPointFormatter.class)) {
+ pointsCaches.remove(series);
+ }
+ }
+ }
protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) {
PointF thisPoint;
PointF lastPoint = null;
PointF firstPoint = null;
- Paint linePaint = formatter.getLinePaint();
- Path path = null;
- ArrayList points = new ArrayList<>(series.size());
- for (int i = 0; i < series.size(); i++) {
- Number y = series.getY(i);
- Number x = series.getX(i);
+ path.reset();
+ final List points = getPointsCache(series);
+
+ int iStart = 0;
+ int iEnd = series.size();
+ if(SeriesUtils.getXYOrder(series) == OrderedXYSeries.XOrder.ASCENDING) {
+ final Region iBounds = SeriesUtils.iBounds(series, getPlot().getBounds());
+ iStart = iBounds.getMin().intValue();
+ if(iStart > 0) {
+ iStart--;
+ }
+ iEnd = iBounds.getMax().intValue() + 1;
+ if(iEnd < series.size() - 1) {
+ iEnd++;
+ }
+ }
+ for (int i = iStart; i < iEnd; i++) {
+ final Number y = series.getY(i);
+ final Number x = series.getX(i);
+ PointF iPoint = points.get(i);
if (y != null && x != null) {
- thisPoint = ValPixConverter.valToPix(
- x, y,
- plotArea,
- getPlot().getCalculatedMinX(),
- getPlot().getCalculatedMaxX(),
- getPlot().getCalculatedMinY(),
- getPlot().getCalculatedMaxY());
- points.add(thisPoint);
+ if(iPoint == null) {
+ iPoint = new PointF();
+ points.set(i, iPoint);
+ }
+ thisPoint = iPoint;
+ getPlot().getBounds().transformScreen(thisPoint, x, y, plotArea);
} else {
thisPoint = null;
+ iPoint = null;
+ points.set(i, iPoint);
}
// don't need to do any of this if the line isnt going to be drawn:
- if(linePaint != null && formatter.getInterpolationParams() == null) {
+ if(formatter.hasLinePaint() && formatter.getInterpolationParams() == null) {
if (thisPoint != null) {
// record the first point of the new Path
if (firstPoint == null) {
- path = new Path();
+ path.reset();
firstPoint = thisPoint;
// create our first point at the bottom/x position so filling will look good:
@@ -120,14 +193,15 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn
}
}
}
- if(linePaint != null) {
+
+ if(formatter.hasLinePaint()) {
if(formatter.getInterpolationParams() != null) {
List interpolatedPoints = getInterpolator(
formatter.getInterpolationParams()).interpolate(series,
formatter.getInterpolationParams());
firstPoint = convertPoint(interpolatedPoints.get(ZERO), plotArea);
lastPoint = convertPoint(interpolatedPoints.get(interpolatedPoints.size()-ONE), plotArea);
- path = new Path();
+ path.reset();
path.moveTo(firstPoint.x, firstPoint.y);
for(int i = 1; i < interpolatedPoints.size(); i++) {
thisPoint = convertPoint(interpolatedPoints.get(i), plotArea);
@@ -139,7 +213,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn
renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter);
}
}
- renderPoints(canvas, plotArea, series, points, formatter);
+ renderPoints(canvas, plotArea, series, iStart, iEnd, points, formatter);
}
/**
@@ -158,36 +232,31 @@ protected Interpolator getInterpolator(InterpolationParams params) {
}
protected PointF convertPoint(XYCoords coord, RectF plotArea) {
- return ValPixConverter.valToPix(
- coord.x.doubleValue(),
- coord.y.doubleValue(),
- plotArea,
- getPlot().getCalculatedMinX(),
- getPlot().getCalculatedMaxX(),
- getPlot().getCalculatedMinY(),
- getPlot().getCalculatedMaxY());
+ return getPlot().getBounds().transformScreen(coord, plotArea);
}
- protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, List points,
+ protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, int iStart, int iEnd, List points,
LineAndPointFormatter formatter) {
- Paint vertexPaint = formatter.getVertexPaint();
- PointLabelFormatter plf = formatter.getPointLabelFormatter();
- if (vertexPaint != null || plf != null) {
- int i = 0;
- for (PointF p : points) {
- PointLabeler pointLabeler = formatter.getPointLabeler();
-
- // if vertexPaint is available, draw vertex:
- if (vertexPaint != null) {
- canvas.drawPoint(p.x, p.y, formatter.getVertexPaint());
- }
+ if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) {
+ final Paint vertexPaint = formatter.hasVertexPaint() ? formatter.getVertexPaint() : null;
+ final boolean hasPointLabelFormatter = formatter.hasPointLabelFormatter();
+ final PointLabelFormatter plf = hasPointLabelFormatter ? formatter.getPointLabelFormatter() : null;
+ final PointLabeler pointLabeler = hasPointLabelFormatter ? formatter.getPointLabeler() : null;
+ for(int i = iStart; i < iEnd; i++) {
+ PointF p = points.get(i);
+ if(p != null) {
+
+ // if vertexPaint is available, draw vertex:
+ if (vertexPaint != null) {
+ canvas.drawPoint(p.x, p.y, vertexPaint);
+ }
- // if textPaint and pointLabeler are available, draw point's text label:
- if (plf != null && pointLabeler != null) {
- canvas.drawText(pointLabeler.getLabel(series, i),
- p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint());
+ // if textPaint and pointLabeler are available, draw point's text label:
+ if (pointLabeler != null) {
+ canvas.drawText(pointLabeler.getLabel(series, i),
+ p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint());
+ }
}
- i++;
}
}
}
@@ -210,49 +279,47 @@ protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF first
path.close();
break;
case RANGE_ORIGIN:
- float originPix = (float) ValPixConverter.valToPix(
- getPlot().getRangeOrigin().doubleValue(),
- getPlot().getCalculatedMinY().doubleValue(),
- getPlot().getCalculatedMaxY().doubleValue(),
- plotArea.height(),
- true);
- originPix += plotArea.top;
-
+ float originPix = (float) getPlot().getBounds().getxRegion()
+ .transform(getPlot().getRangeOrigin()
+ .doubleValue(), plotArea.top, plotArea.bottom, true);
path.lineTo(lastPoint.x, originPix);
path.lineTo(firstPoint.x, originPix);
path.close();
break;
default:
- throw new UnsupportedOperationException("Fill direction not yet implemented: " + formatter.getFillDirection());
+ throw new UnsupportedOperationException(
+ "Fill direction not yet implemented: " + formatter.getFillDirection());
}
if (formatter.getFillPaint() != null) {
canvas.drawPath(path, formatter.getFillPaint());
}
- // draw any visible regions on top of the base region:
- double minX = getPlot().getCalculatedMinX().doubleValue();
- double maxX = getPlot().getCalculatedMaxX().doubleValue();
- double minY = getPlot().getCalculatedMinY().doubleValue();
- double maxY = getPlot().getCalculatedMaxY().doubleValue();
+ final RectRegion bounds = getPlot().getBounds();
+ final RectRegion plotRegion = new RectRegion(plotArea);
// draw each region:
- for (RectRegion r : RectRegion.regionsWithin(formatter.getRegions().elements(), minX, maxX, minY, maxY)) {
- XYRegionFormatter f = formatter.getRegionFormatter(r);
- RectF regionRect = r.getRectF(plotArea, minX, maxX, minY, maxY);
- if (regionRect != null) {
- try {
- canvas.save(Canvas.ALL_SAVE_FLAG);
- canvas.clipPath(path);
- canvas.drawRect(regionRect, f.getPaint());
- } finally {
- canvas.restore();
+ for (RectRegion thisRegion : bounds.intersects(formatter.getRegions().elements())) {
+ XYRegionFormatter regionFormatter = formatter.getRegionFormatter(thisRegion);
+ RectRegion thisRegionTransformed = bounds
+ .transform(thisRegion, plotRegion, false, true);
+ thisRegionTransformed.intersect(plotRegion);
+ if(thisRegion.isFullyDefined()) {
+ RectF thisRegionRectF = thisRegionTransformed.asRectF();
+ if (thisRegionRectF != null) {
+ try {
+ canvas.save();
+ canvas.clipPath(path);
+ canvas.drawRect(thisRegionRectF, regionFormatter.getPaint());
+ } finally {
+ canvas.restore();
+ }
}
}
}
// finally we draw the outline path on top of everything else:
- if(formatter.getLinePaint() != null) {
+ if(formatter.hasLinePaint()) {
canvas.drawPath(outlinePath, formatter.getLinePaint());
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java
new file mode 100644
index 00000000..03760b39
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java
@@ -0,0 +1,136 @@
+package com.androidplot.xy;
+
+import com.androidplot.Region;
+import com.androidplot.util.SeriesUtils;
+
+/**
+ * Wrapper implementation of {@link XYSeries} that wraps another XYSeries, normalizing values in the range of 0 to 1.
+ * Note that it's possible to push normed values outside of the standard 0, 1 range by applying
+ * a sufficiently large offset.
+ */
+public class NormedXYSeries implements XYSeries {
+
+ private XYSeries rawData;
+
+ private Region minMaxX;
+ private Region minMaxY;
+
+ private Region transformX;
+ private Region transformY;
+
+ public static class Norm {
+
+ final Region minMax;
+ final double offset;
+ final boolean useOffsetCompression;
+
+ public Norm(Region minMax) {
+ this(minMax, 0, false);
+ }
+
+ /**
+ *
+ * @param minMax Boundary to use when calculating the norm coefficient. Set to null to let
+ * Androidplot auto calculate the bounds. (Very inefficient)
+ * @param offset An extra offset to apply, generally within the range of -1 and 1.
+ * This value is useful for adjusting the positioning of a series relative to another normalized series.
+ * @param useOffsetCompression If true, the offset value will result in further scaling down
+ * of the series data in order to ensure that all points within the specified bounds remain
+ * visible on the screen. If set to true, the specified offset MUST be > -1 and < 1. Will be
+ * ignored if bounds != null.
+ */
+ public Norm(Region minMax, double offset, boolean useOffsetCompression) {
+ this.minMax = minMax;
+ this.offset = offset;
+ this.useOffsetCompression = useOffsetCompression;
+
+ if (useOffsetCompression && (offset <= -1 || offset >= 1)) {
+ throw new IllegalArgumentException(
+ "When useOffsetCompression is true, offset must be > -1 and < 1.");
+ }
+ }
+ }
+
+ /**
+ * Normalizes yVals only, auto calculating min/max.
+ * @param rawData
+ */
+ public NormedXYSeries(XYSeries rawData) {
+ this(rawData, null, new Norm(null, 0, false));
+ }
+
+ /**
+ *
+ * @param rawData The XYSeries to be normalized.
+ * @param x Normalization to apply to xVals. Set to null to disable normalization on the x axis.
+ * @param y Normalization to apply to yVals. Set to null to disable normalization on the y axis.
+ */
+ public NormedXYSeries(XYSeries rawData, Norm x, Norm y) {
+ this.rawData = rawData;
+ normalize(x, y);
+ }
+
+ protected void normalize(Norm x, Norm y) {
+ if( x != null) {
+ this.minMaxX = x.minMax != null ? x.minMax : SeriesUtils.minMaxX(rawData);
+ this.transformX = calculateTransform(x);
+ }
+
+ if( y != null) {
+ this.minMaxY = y.minMax != null ? y.minMax : SeriesUtils.minMaxY(rawData);
+ this.transformY = calculateTransform(y);
+ }
+ }
+
+ protected Region calculateTransform(Norm norm) {
+ if(norm.useOffsetCompression) {
+ return new Region(
+ norm.offset > 0 ? norm.offset : 0,
+ norm.offset < 0 ? 1 + norm.offset : 1);
+ } else {
+ return new Region(0 + norm.offset, 1 + norm.offset);
+ }
+ }
+
+ @Override
+ public String getTitle() {
+ return rawData.getTitle();
+ }
+
+ @Override
+ public int size() {
+ return rawData.size();
+ }
+
+ public Number denormalizeXVal(Number xVal) {
+ if(xVal != null) {
+ return transformX.transform(xVal.doubleValue(), minMaxX);
+ }
+ return null;
+ }
+
+ public Number denormalizeYVal(Number yVal) {
+ if(yVal != null) {
+ return transformY.transform(yVal.doubleValue(), minMaxY);
+ }
+ return null;
+ }
+
+ @Override
+ public Number getX(int index) {
+ final Number xVal = rawData.getX(index);
+ if(xVal != null && transformX != null) {
+ return minMaxX.transform(xVal.doubleValue(), transformX);
+ }
+ return xVal;
+ }
+
+ @Override
+ public Number getY(int index) {
+ final Number yVal = rawData.getY(index);
+ if(yVal != null && transformY != null) {
+ return minMaxY.transform(yVal.doubleValue(), transformY);
+ }
+ return yVal;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java
new file mode 100644
index 00000000..820e4330
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java
@@ -0,0 +1,33 @@
+package com.androidplot.xy;
+
+/**
+ * An implementation of {@link XYSeries} that gives hints to it's renderer about the order
+ * of the data being rendered.
+ */
+public interface OrderedXYSeries extends XYSeries {
+
+ enum XOrder {
+ /**
+ * XVals are in strict ascending order such that:
+ * x(i) < x(i+1) == true
+ */
+ ASCENDING,
+
+ /**
+ * XVals are in strict descending order such that:
+ * x(i) > x(i+1) == true
+ */
+ DESCENDING,
+
+ /**
+ * XVals appear in no particular order.
+ */
+ NONE
+ }
+
+ /**
+ * The order of XVals as they appear in this series.
+ * @return
+ */
+ XOrder getXOrder();
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java
index c1fe5e98..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 {
- private static final float MIN_DIST_2_FING = 5f;
+ 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,73 +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 {
- minXLimit = lowerBoundaryMode == BoundaryMode.FIXED ?
- lowerBoundary.floatValue() : plot.getCalculatedMinX().floatValue();
- maxXLimit = upperBoundaryMode == BoundaryMode.FIXED ?
- upperBoundary.floatValue() : plot.getCalculatedMaxX().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 {
- minYLimit = lowerBoundaryMode == BoundaryMode.FIXED ?
- lowerBoundary.floatValue() : plot.getCalculatedMinY().floatValue();
- maxYLimit = upperBoundaryMode == BoundaryMode.FIXED ?
- upperBoundary.floatValue() : plot.getCalculatedMaxY().floatValue();
- lastMinY = minYLimit;
- lastMaxY = maxYLimit;
- }
- }
-
- protected void setDomainBoundaries(final Number lowerBoundary,
- final Number upperBoundary, final BoundaryMode mode) {
- plot.setDomainBoundaries(lowerBoundary, upperBoundary, mode);
- if(mCalledBySelf) {
- mCalledBySelf = false;
- } else {
- minXLimit = mode == BoundaryMode.FIXED ?
- lowerBoundary.floatValue() : plot.getCalculatedMinX().floatValue();
- maxXLimit = mode == BoundaryMode.FIXED ?
- upperBoundary.floatValue() : plot.getCalculatedMaxX().floatValue();
- lastMinX = minXLimit;
- lastMaxX = maxXLimit;
- }
- }
-
- protected synchronized void setRangeBoundaries(final Number lowerBoundary,
- final Number upperBoundary, final BoundaryMode mode) {
- plot.setRangeBoundaries(lowerBoundary, upperBoundary, mode);
- if(mCalledBySelf) {
- mCalledBySelf = false;
- } else {
- minYLimit = mode == BoundaryMode.FIXED ?
- lowerBoundary.floatValue() : plot.getCalculatedMinY().floatValue();
- maxYLimit = mode == BoundaryMode.FIXED ?
- upperBoundary.floatValue() : plot.getCalculatedMaxY().floatValue();
- lastMinY = minYLimit;
- lastMaxY = maxYLimit;
- }
- }
-
@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());
@@ -177,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;
@@ -187,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);
@@ -194,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:
@@ -202,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
*/
- private 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.getCalculatedMinX().floatValue();
- lastMinX = minXLimit;
- }
- return minXLimit;
- }
-
- protected float getMaxXLimit() {
- if(maxXLimit == Float.MAX_VALUE) {
- maxXLimit = plot.getCalculatedMaxX().floatValue();
- lastMaxX = maxXLimit;
- }
- return maxXLimit;
- }
-
- protected float getMinYLimit() {
- if(minYLimit == Float.MAX_VALUE) {
- minYLimit = plot.getCalculatedMinY().floatValue();
- lastMinY = minYLimit;
- }
- return minYLimit;
- }
-
- protected float getMaxYLimit() {
- if(maxYLimit == Float.MAX_VALUE) {
- maxYLimit = plot.getCalculatedMaxY().floatValue();
- lastMaxY = maxYLimit;
- }
- return maxYLimit;
- }
-
- protected float getLastMinX() {
- if(lastMinX == Float.MAX_VALUE) {
- lastMinX = plot.getCalculatedMinX().floatValue();
- }
- return lastMinX;
- }
-
- protected float getLastMaxX() {
- if(lastMaxX == Float.MAX_VALUE) {
- lastMaxX = plot.getCalculatedMaxX().floatValue();
- }
- return lastMaxX;
- }
-
- protected float getLastMinY() {
- if(lastMinY == Float.MAX_VALUE) {
- lastMinY = plot.getCalculatedMinY().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.getCalculatedMaxY().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();
+ }
}
}
}
@@ -478,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;
}
@@ -492,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/PointLabelFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java
index 1d9fe520..4f5f762d 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java
@@ -16,10 +16,8 @@
package com.androidplot.xy;
-import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
-import android.graphics.PointF;
import com.androidplot.util.PixelUtils;
public class PointLabelFormatter {
@@ -52,7 +50,14 @@ public PointLabelFormatter(int textColor, float hOffset, float vOffset) {
this.vOffset = vOffset;
}
+ public boolean hasTextPaint() {
+ return textPaint != null;
+ }
+
public Paint getTextPaint() {
+ if(textPaint == null) {
+ initTextPaint(Color.TRANSPARENT);
+ }
return textPaint;
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java b/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java
index 205d62a5..12504dff 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java
@@ -16,7 +16,7 @@
package com.androidplot.xy;
-public interface PointLabeler {
+public interface PointLabeler {
- String getLabel(XYSeries series, int index);
+ String getLabel(SeriesType series, int index);
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
index c406d9e2..0bb8988d 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java
@@ -18,8 +18,8 @@
import android.graphics.PointF;
import android.graphics.RectF;
-import com.androidplot.LineRegion;
-import com.androidplot.util.ValPixConverter;
+
+import com.androidplot.Region;
import java.util.ArrayList;
import java.util.List;
@@ -31,49 +31,148 @@
*/
public class RectRegion {
- LineRegion xLineRegion;
- LineRegion yLineRegion;
+ Region xRegion;
+ Region yRegion;
private String label;
+ public RectRegion() {
+ xRegion = new Region();
+ yRegion = new Region();
+ }
+
+ public static RectRegion withDefaults(RectRegion defaults) {
+ if(defaults == null || !defaults.isFullyDefined()) {
+ throw new IllegalArgumentException("When specifying defaults, RectRegion param must contain no null values.");
+ }
+
+ RectRegion r = new RectRegion();
+ r.xRegion = Region.withDefaults(defaults.getxRegion());
+ r.yRegion = Region.withDefaults(defaults.getyRegion());
+ return r;
+ }
+
+ public RectRegion(XYCoords min, XYCoords max) {
+ this(min.x, max.x, min.y, max.y);
+ }
+
/**
- *
* @param minX
* @param maxX
* @param minY
* @param maxY
*/
public RectRegion(Number minX, Number maxX, Number minY, Number maxY, String label) {
- xLineRegion = new LineRegion(minX, maxX);
- yLineRegion = new LineRegion(minY, maxY);
+ xRegion = new Region(minX, maxX);
+ yRegion = new Region(minY, maxY);
this.setLabel(label);
}
+ public RectRegion(RectF rect) {
+ this(rect.left < rect.right ? rect.left : rect.right,
+ rect.right > rect.left ? rect.right : rect.left,
+ rect.bottom < rect.top ? rect.bottom : rect.top,
+ rect.top > rect.bottom ? rect.top : rect.bottom);
+ }
+
@SuppressWarnings("SameParameterValue")
public RectRegion(Number minX, Number maxX, Number minY, Number maxY) {
this(minX, maxX, minY, maxY, null);
}
- public boolean containsPoint(PointF point) {
- throw new UnsupportedOperationException("Not yet implemented.");
+ public XYCoords transform(Number x, Number y, RectRegion region2, boolean flipX, boolean flipY) {
+ Number xx = xRegion.transform(x.doubleValue(), region2.xRegion, flipX);
+ Number yy = yRegion.transform(y.doubleValue(), region2.yRegion, flipY);
+ return new XYCoords(xx, yy);
+ }
+
+ public XYCoords transform(Number x, Number y, RectRegion region2) {
+ return transform(x, y, region2, false, false);
}
- public boolean containsValue(Number x, Number y) {
- throw new UnsupportedOperationException("Not yet implemented.");
+ public XYCoords transform(XYCoords value, RectRegion region2) {
+ return transform(value.x, value.y, region2);
}
- public boolean containsDomainValue(Number value) {
- return xLineRegion.contains(value);
+ /**
+ * Transform a region (r) from the current region space (this) into the specified one (r2)
+ * @param r The region to which the transformation applies
+ * @param r2 The region into which r is being transformed
+ * @return
+ */
+ public RectRegion transform(RectRegion r, RectRegion r2, boolean flipX, boolean flipY) {
+ return new RectRegion(
+ transform(r.getMinX(), r.getMinY(), r2, flipX, flipY),
+ transform(r.getMaxX(), r.getMaxY(), r2, flipX, flipY)
+ );
}
- public boolean containsRangeValue(Number value) {
- return yLineRegion.contains(value);
+ /**
+ * Convenience method to transform into screen coordinate space. Equivalent to invoking
+ * {@link #transform(Number, Number, RectF, boolean, boolean)} with
+ * flipX = false and flipY = true.
+ * @param x
+ * @param y
+ * @param region2
+ * @return
+ */
+ public PointF transformScreen(Number x, Number y, RectF region2) {
+ return transform(x, y, region2, false, true);
+ }
+
+ public void transformScreen(PointF result, Number x, Number y, RectF region2) {
+ transform(result, x, y, region2, false, true);
+ }
+
+ public void transform(PointF result, Number x, Number y, RectF region2, boolean flipX, boolean flipY) {
+ result.x = (float) xRegion.transform(x.doubleValue(), region2.left, region2.right, flipX);
+ result.y = (float) yRegion.transform(y.doubleValue(), region2.top, region2.bottom, flipY);
+ }
+
+ public PointF transform(Number x, Number y, RectF region2, boolean flipX, boolean flipY) {
+ PointF result = new PointF();
+ transform(result, x, y, region2, flipX, flipY);
+ return result;
+ }
+
+ public PointF transformScreen(XYCoords value, RectF region2) {
+ return transform(value, region2, false, true);
+ }
+
+ /**
+ * Convenience method to transform into screen coordinate space. Equivalent to invoking
+ * {@link #transform(XYCoords, RectF, boolean, boolean)} with flipX = false and flipY = true.
+ * @param value
+ * @param region2
+ * @param flipX
+ * @param flipY
+ * @return
+ */
+ public PointF transform(XYCoords value, RectF region2, boolean flipX, boolean flipY) {
+ return transform(value.x, value.y, region2, flipX, flipY);
+ }
+
+ public void union(Number x, Number y) {
+ xRegion.union(x);
+ yRegion.union(y);
+ }
+
+ /**
+ * Compares the input bounds xy min/max vals against this instance's current xy min/max vals.
+ * If the input.min is less than this.min then this.min will be set to input.min.
+ * If the input.max is greater than this.max then this.max will be set to input.max
+ *
+ * The result will always have equal or greater area than the inputs.
+ * @param input
+ */
+ public void union(RectRegion input) {
+ xRegion.union(input.xRegion);
+ yRegion.union(input.yRegion);
}
public boolean intersects(RectRegion region) {
return intersects(region.getMinX(), region.getMaxX(), region.getMinY(), region.getMaxY());
}
-
/**
* Tests whether this region intersects the region defined by params. Use
* null to represent infinity. Negative and positive infinity is implied by
@@ -86,51 +185,40 @@ public boolean intersects(RectRegion region) {
* @return
*/
public boolean intersects(Number minX, Number maxX, Number minY, Number maxY) {
- return xLineRegion.intersects(minX, maxX) && yLineRegion.intersects(minY, maxY);
+ return xRegion.intersects(minX, maxX) && yRegion.intersects(minY, maxY);
}
- public boolean intersects(RectF region, Number visMinX, Number visMaxX, Number visMinY, Number visMaxY) {
-
- RectF thisRegion = getRectF(region, visMinX.doubleValue(), visMaxX.doubleValue(),
- visMinY.doubleValue(), visMaxY.doubleValue());
- return RectF.intersects(thisRegion, region);
+ public RectF asRectF() {
+ return new RectF(getMinX().floatValue(), getMinY().floatValue(),
+ getMaxX().floatValue(), getMaxY().floatValue());
}
- public RectF getRectF(RectF plotRect, Number visMinX, Number visMaxX, Number visMinY, Number visMaxY) {
- PointF topLeftPoint = ValPixConverter.valToPix(
- xLineRegion.getMinVal().doubleValue() != Double.NEGATIVE_INFINITY ? xLineRegion.getMinVal() : visMinX,
- yLineRegion.getMaxVal().doubleValue() != Double.POSITIVE_INFINITY ? yLineRegion.getMaxVal() : visMaxY,
- plotRect,
- visMinX,
- visMaxX,
- visMinY,
- visMaxY);
- PointF bottomRightPoint = ValPixConverter.valToPix(
- xLineRegion.getMaxVal().doubleValue() != Double.POSITIVE_INFINITY ? xLineRegion.getMaxVal() : visMaxX,
- yLineRegion.getMinVal().doubleValue() != Double.NEGATIVE_INFINITY ? yLineRegion.getMinVal() : visMinY,
- plotRect,
- visMinX,
- visMaxX,
- visMinY,
- visMaxY);
- // TODO: figure out why the y-values are inverted
- return new RectF(topLeftPoint.x, topLeftPoint.y, bottomRightPoint.x, bottomRightPoint.y);
+ /**
+ * The result of an intersect is always a RectRegion with an equal or smaller area.
+ * @param clippingBounds
+ */
+ public void intersect(RectRegion clippingBounds) {
+ if(intersects(clippingBounds)) {
+ xRegion.intersect(clippingBounds.xRegion);
+ yRegion.intersect(clippingBounds.yRegion);
+ } else {
+ setMinY(null);
+ setMaxY(null);
+ setMinX(null);
+ setMaxX(null);
+ }
}
/**
* Returns a list of XYRegions that either completely or partially intersect the area
* defined by params. A null value for any parameter represents infinity / no boundary.
* @param regions The list of regions to search through
- * @param minX
- * @param maxX
- * @param minY
- * @param maxY
* @return
*/
- public static List regionsWithin(List regions, Number minX, Number maxX, Number minY, Number maxY) {
- ArrayList intersectingRegions = new ArrayList();
- for(RectRegion r : regions) {
- if(r.intersects(minX, maxX, minY, maxY)) {
+ public List intersects(List regions) {
+ ArrayList intersectingRegions = new ArrayList<>();
+ for (RectRegion r : regions) {
+ if (r.intersects(getMinX(), getMaxX(), getMinY(), getMaxY())) {
intersectingRegions.add(r);
}
}
@@ -138,7 +226,6 @@ public static List regionsWithin(List regions, Number mi
}
/**
- *
* @return Width of this region, in native units
*/
public Number getWidth() {
@@ -146,7 +233,6 @@ public Number getWidth() {
}
/**
- *
* @return Height of this region, in native units
*/
public Number getHeight() {
@@ -159,40 +245,63 @@ public Number getHeight() {
* @param y
* @return
*/
- private Number distanceBetween(Number x, Number y) {
+ private static Number distanceBetween(Number x, Number y) {
return Math.abs(x.doubleValue() - y.doubleValue());
}
+ public void set(Number minX, Number maxX, Number minY, Number maxY) {
+ setMinX(minX);
+ setMaxX(maxX);
+ setMinY(minY);
+ setMaxY(maxY);
+ }
+
+ public boolean isMinXSet() {
+ return xRegion.isMinSet();
+ }
+
public Number getMinX() {
- return xLineRegion.getMinVal();
+ return xRegion.getMin();
+ }
+
+ public void setMinX(Number minX) {
+ xRegion.setMin(minX);
}
- public void setMinX(double minX) {
- xLineRegion.setMinVal(minX);
+ public boolean isMaxXSet() {
+ return xRegion.isMaxSet();
}
public Number getMaxX() {
- return xLineRegion.getMaxVal();
+ return xRegion.getMax();
}
public void setMaxX(Number maxX) {
- xLineRegion.setMaxVal(maxX);
+ xRegion.setMax(maxX);
+ }
+
+ public boolean isMinYSet() {
+ return yRegion.isMinSet();
}
public Number getMinY() {
- return yLineRegion.getMinVal();
+ return yRegion.getMin();
}
public void setMinY(Number minY) {
- yLineRegion.setMinVal(minY);
+ yRegion.setMin(minY);
+ }
+
+ public boolean isMaxYSet() {
+ return yRegion.isMaxSet();
}
public Number getMaxY() {
- return yLineRegion.getMaxVal();
+ return yRegion.getMax();
}
public void setMaxY(Number maxY) {
- yLineRegion.setMaxVal(maxY);
+ yRegion.setMax(maxY);
}
public String getLabel() {
@@ -202,4 +311,47 @@ public String getLabel() {
public void setLabel(String label) {
this.label = label;
}
+
+ public Region getxRegion() {
+ return xRegion;
+ }
+
+ public void setxRegion(Region xRegion) {
+ this.xRegion = xRegion;
+ }
+
+ public Region getyRegion() {
+ return yRegion;
+ }
+
+ public void setyRegion(Region yRegion) {
+ this.yRegion = yRegion;
+ }
+
+ /**
+ *
+ * @return True if both xRegion and yRegion are defined, false otherwise
+ */
+ public boolean isFullyDefined() {
+ return xRegion.isDefined() && yRegion.isDefined();
+ }
+
+ /**
+ * True if this region contains the specified coordinates.
+ * @param x
+ * @param y
+ * @return
+ */
+ public boolean contains(Number x, Number y) {
+ return getxRegion().contains(x) && getyRegion().contains(y);
+ }
+
+ @Override
+ public String toString() {
+ return "RectRegion{" +
+ "xRegion=" + xRegion +
+ ", yRegion=" + yRegion +
+ ", label='" + label + '\'' +
+ '}';
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java
new file mode 100644
index 00000000..f795536b
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java
@@ -0,0 +1,216 @@
+package com.androidplot.xy;
+
+import com.androidplot.*;
+import com.androidplot.util.SeriesUtils;
+
+import java.util.*;
+
+/**
+ * An implementation of {@link FastXYSeries} that samples its self into multiple levels to
+ * achieve faster rendering / zoom behavior. By default, uses {@link LTTBSampler} as
+ * it's sampling algorithm. Note that this algorithm does not yet support null values.
+ *
+ * Sampling behavior is controlled by two values:
+ * Ratio: A value greater than 1; controls the sampling ratio of each successive series. For example,
+ * a step of 2 would mean that each successive series contains 2x fewer points than the previous.
+ *
+ * Threshold: A value < the original series size; controls the lower limit at which point sampling should stop.
+ * For example, sampling a series with size 1000 given a ratio of 2 and a threshold of 100,
+ * three sampled resolutions will be generated:
+ *
+ * 500 - 2x sampling
+ * 250 - 4x sampling
+ * 125 - 8x sampling
+ *
+ */
+public class SampledXYSeries implements FastXYSeries, OrderedXYSeries {
+ private int threshold;
+ private Sampler algorithm = new LTTBSampler();
+
+ private XYSeries rawData;
+
+ private List zoomLevels;
+
+ private XYSeries activeSeries;
+
+ private RectRegion bounds;
+ private Exception lastResamplingException;
+
+ private final XOrder xOrder;
+ private float ratio;
+
+ /**
+ *
+ * @param rawData
+ * @param xOrder If your data is in ascending or descending order, specifying it here speed up
+ * optimize render times.
+ * @param ratio The ratio used to determine the size of each new sampled series. Must be > 1.
+ * downsampled series until threshold is reached.
+ * @param threshold The desired size of the smallest sample series. Must be < rawData.size.
+ */
+ public SampledXYSeries(XYSeries rawData, XOrder xOrder, float ratio, int threshold) {
+ this.rawData = rawData;
+ this.xOrder = xOrder;
+ this.setRatio(ratio);
+ this.setThreshold(threshold);
+ resample();
+ }
+
+ /**
+ * Generate a SampledXYSeries from the input series.
+ * @param rawData The original series to be downsampled
+ * @param ratio The ratio used to determine the size of each new sampled series. Must be > 1.
+ * downsampled series until threshold is reached.
+ * @param threshold The desired size of the smallest sample series. Must be < rawData.size.
+ */
+ public SampledXYSeries(XYSeries rawData, float ratio, int threshold) {
+ this(rawData, SeriesUtils.getXYOrder(rawData), ratio, threshold);
+ }
+
+ public void resample() {
+ bounds = null;
+ zoomLevels = new ArrayList<>();
+ int t = (int) Math.ceil(rawData.size() / getRatio());
+ List threads = new ArrayList<>(zoomLevels.size());
+ while (t > threshold) {
+ final int thisThreshold = t;
+ final EditableXYSeries thisSeries = new FixedSizeEditableXYSeries(getTitle(), thisThreshold);
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // TODO: make bounds a param to prevent calculating on each setZoomFactor level:
+ RectRegion b = getAlgorithm()
+ .run(rawData, thisSeries);
+ if (bounds == null) {
+ bounds = b;
+ }
+ } catch(Exception ex) {
+ lastResamplingException = ex;
+ }
+ }
+ }, "Androidplot XY Series Sampler");
+ getZoomLevels().add(thisSeries);
+ threads.add(thread);
+ thread.start();
+
+ t = (int) Math.ceil(t / getRatio());
+ }
+ for(Thread thread : threads) {
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if(lastResamplingException != null) {
+ throw new RuntimeException("Exception encountered during resampling", lastResamplingException);
+ }
+
+ }
+
+ protected List getZoomLevels() {
+ return this.zoomLevels;
+ }
+
+ /**
+ * Set zoom factor; 2.5 = 2.5x zoom, 10.0 = 10x zoom etc. This method will set the zoom level
+ * to the closest available factor to the specified factor; a specified factor of 4.5x may result
+ * in an actual factor of 4x.
+ * @param factor
+ */
+ public void setZoomFactor(double factor) {
+ if(factor <= 1) {
+ activeSeries = rawData;
+ } else {
+ //int i = (int) Math.round(Math.sqrt(factor) - 1);
+ int i = getZoomIndex(factor, getRatio());
+ if (i < zoomLevels.size()) {
+ activeSeries = zoomLevels.get(i);
+ } else {
+ activeSeries = zoomLevels.get(zoomLevels.size() - 1);
+ }
+ }
+ }
+
+ protected static int getZoomIndex(double zoomFactor, double ratio) {
+ final double lhs = Math.log(zoomFactor);
+ final double rhs = Math.log(ratio);
+ final double log = lhs / rhs;
+ final int index = (int) Math.round(log);
+ return index > 0 ? index - 1 : 0;
+ }
+
+ public double getMaxZoomFactor() {
+ return Math.pow(getRatio(), zoomLevels.size());
+ }
+
+ public Sampler getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(Sampler algorithm) {
+ this.algorithm = algorithm;
+ resample();
+ }
+
+ @Override
+ public String getTitle() {
+ return rawData.getTitle();
+ }
+
+ @Override
+ public int size() {
+ return activeSeries.size();
+ }
+
+ @Override
+ public Number getX(int index) {
+ return activeSeries.getX(index);
+ }
+
+ @Override
+ public Number getY(int index) {
+ return activeSeries.getY(index);
+ }
+
+ public int getThreshold() {
+ return threshold;
+ }
+
+ public void setThreshold(int threshold) {
+ if(threshold >= rawData.size()) {
+ throw new IllegalArgumentException("Threshold must be < original series size.");
+ }
+ this.threshold = threshold;
+ }
+
+ public RectRegion getBounds() {
+ return bounds;
+ }
+
+ public void setBounds(RectRegion bounds) {
+ this.bounds = bounds;
+ }
+
+ @Override
+ public RectRegion minMax() {
+ return bounds;
+ }
+
+ @Override
+ public XOrder getXOrder() {
+ return xOrder;
+ }
+
+ public float getRatio() {
+ return ratio;
+ }
+
+ public void setRatio(float ratio) {
+ if(ratio <= 1) {
+ throw new IllegalArgumentException("Ratio must be greater than 1");
+ }
+ this.ratio = ratio;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java b/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java
new file mode 100644
index 00000000..ebbc8238
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java
@@ -0,0 +1,16 @@
+package com.androidplot.xy;
+
+/**
+ * An algorithm used to to resample a larger set of data into a smaller set.
+ */
+public interface Sampler {
+
+ /**
+ *
+ * @param input The original unsampled series
+ * @param output The destination series to contain sampled result.
+ * This series size should be set to the desired sampled size.
+ * @return min/max values encountered while processing input.
+ */
+ RectRegion run(XYSeries input, EditableXYSeries output);
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java
new file mode 100644
index 00000000..2e194f17
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java
@@ -0,0 +1,67 @@
+package com.androidplot.xy;
+
+/**
+ * Wraps an existing {@link XYSeries} allowing easy scaling of that series' xy values.
+ */
+public class ScalingXYSeries implements XYSeries {
+
+ private double scale;
+ private XYSeries series;
+ private Mode mode;
+
+ public enum Mode {
+ X_ONLY,
+ Y_ONLY,
+ X_AND_Y
+ }
+
+ /**
+ *
+ * @param series The {@link XYSeries} to be scaled
+ * @param scale The initial scale to be applied
+ * @param mode Determines which axis (or both) to which scaling will be applied.
+ */
+ public ScalingXYSeries(XYSeries series, double scale, Mode mode) {
+ this.series = series;
+ this.scale = scale;
+ this.mode = mode;
+ }
+
+ @Override
+ public String getTitle() {
+ return series.getTitle();
+ }
+
+ @Override
+ public int size() {
+ return series.size();
+ }
+
+ @Override
+ public Number getX(int index) {
+ Number x = series.getX(index);
+ if(mode == Mode.X_ONLY || mode == Mode.X_AND_Y) {
+ return x == null ? null : x.doubleValue() * scale;
+ } else {
+ return x;
+ }
+ }
+
+ @Override
+ public Number getY(int index) {
+ Number y = series.getY(index);
+ if(mode == Mode.Y_ONLY || mode == Mode.X_AND_Y) {
+ return y == null ? null : y.doubleValue() * scale;
+ } else {
+ return y;
+ }
+ }
+
+ public double getScale() {
+ return scale;
+ }
+
+ public void setScale(double scale) {
+ this.scale = scale;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java
index 0dc9c88e..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
@@ -284,4 +326,27 @@ public Number getX(int index) {
public Number getY(int index) {
return yVals.get(index);
}
+
+ public LinkedList getxVals() {
+ return xVals;
+ }
+
+ public LinkedList getyVals() {
+ return yVals;
+ }
+
+ /**
+ * Remove all values from the series
+ */
+ public void clear() {
+ lock.writeLock().lock();
+ try {
+ if (xVals != null) {
+ xVals.clear();
+ }
+ yVals.clear();
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java
index 37e64701..daec3449 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java
@@ -16,9 +16,7 @@
package com.androidplot.xy;
-import android.content.Context;
import com.androidplot.ui.SeriesRenderer;
-import com.androidplot.util.Configurator;
public class StepFormatter extends LineAndPointFormatter {
@@ -38,7 +36,7 @@ public Class extends SeriesRenderer> getRendererClass() {
}
@Override
- public SeriesRenderer getRendererInstance(XYPlot plot) {
+ public SeriesRenderer doGetRendererInstance(XYPlot plot) {
return new StepRenderer(plot);
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/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/XYBounds.java b/androidplot-core/src/main/java/com/androidplot/xy/XYBounds.java
deleted file mode 100644
index 1d503ad6..00000000
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYBounds.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.androidplot.xy;
-
-import com.androidplot.Bounds;
-
-/**
- * Defines a rectangle using xy min/max values. XYBounds differs from {@link RectRegion} in that
- * it accepts null values.
- * @since 0.9.7
- */
-public class XYBounds {
-
- private Bounds xBounds;
- private Bounds yBounds;
-
- public XYBounds() {
- this(null, null, null, null);
- }
-
- public XYBounds(Number minX, Number maxX, Number minY, Number maxY) {
- xBounds = new Bounds(minX, maxX);
- yBounds = new Bounds(minY, maxY);
- }
-
- /**
- * Compares the input bounds xy min/max vals against this instance's current xy min/max vals.
- * If the input.min is less than this.min then this.min will be set to input.min.
- * If the input.max is greater than this.max then this.max will be set to input.max
- * @param input
- */
- public void union(XYBounds input) {
- xBounds.union(input.getXBounds());
- yBounds.union(input.getYBounds());
- }
-
- public void setXBounds(Bounds xBounds) {
- this.xBounds = xBounds;
- }
-
- public void setYBounds(Bounds yBounds) {
- this.yBounds = yBounds;
- }
-
- public Bounds getXBounds() {
- return xBounds;
- }
-
- public Bounds getYBounds() {
- return yBounds;
- }
-
- public Number getMinX() {
- return xBounds.getMin();
- }
-
- public void setMinX(Number minX) {
- xBounds.setMin(minX);
- }
-
- public Number getMaxX() {
- return xBounds.getMax();
- }
-
- public void setMaxX(Number maxX) {
- xBounds.setMax(maxX);
- }
-
- public Number getMinY() {
- return yBounds.getMin();
- }
-
- public void setMinY(Number minY) {
- yBounds.setMin(minY);
- }
-
- public Number getMaxY() {
- return yBounds.getMax();
- }
-
- public void setMaxY(Number maxY) {
- yBounds.setMax(maxY);
- }
-}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
index 398d0379..c4452d33 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java
@@ -16,16 +16,20 @@
package com.androidplot.xy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
/**
* Calculates the min/max constraints for an xy plane.
+ *
* @since 0.9.7
*/
public class XYConstraints {
// used for calculating the domain/range extents that will be displayed on the plot.
- // using boundaries and origins are mutually exclusive. because of this,
- // setting one will disable the other. when only setting the FramingModel,
- // the origin or boundary is set to the current value of the plot.
+ // using boundaries and origins are mutually exclusive and enabling one will disable
+ // the other. when only setting the FramingModel, the origin or boundary is set to
+ // the current value of the plot.
private XYFramingModel domainFramingModel = XYFramingModel.EDGE;
private XYFramingModel rangeFramingModel = XYFramingModel.EDGE;
@@ -44,113 +48,148 @@ public XYConstraints() {
this(null, null, null, null);
}
- public XYConstraints(Number minX, Number maxX, Number minY, Number maxY) {
+ public XYConstraints(@Nullable Number minX, @Nullable Number maxX, @Nullable Number minY, @Nullable Number maxY) {
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
+ public boolean contains(@NonNull RectRegion rectRegion) {
+ return contains(rectRegion.getMinY(), rectRegion.getMinY())
+ && contains(rectRegion.getMaxX(), rectRegion.getMaxY());
+ }
+
public boolean contains(Number x, Number y) {
- if(x == null || y == null) {
+ if (x == null || y == null) {
// this is essentially an invisible point:
return false;
- } else {
- final double dx = x.doubleValue();
-
- if(minX != null && dx < minX.doubleValue()) {
- return false;
- } else if(maxX != null && dx > maxX.doubleValue()) {
- return false;
- } else {
- final double dy = y.doubleValue();
- if(minY != null && dy < minY.doubleValue()) {
- return false;
- } else if(maxY != null && dy > maxY.doubleValue()) {
- return false;
- }
- }
+ }
+
+ if (minX == null && maxX == null && minY == null && maxY == null) {
+ //there are no constraints
return true;
}
+
+ final double dx = x.doubleValue();
+ if (minX != null && dx < minX.doubleValue()) {
+ return false;
+ } else if (maxX != null && dx > maxX.doubleValue()) {
+ return false;
+ }
+
+ final double dy = y.doubleValue();
+ if (minY != null && dy < minY.doubleValue()) {
+ return false;
+ } else if (maxY != null && dy > maxY.doubleValue()) {
+ return false;
+ }
+
+ return true;
}
+ @Nullable
public Number getMinX() {
return minX;
}
+ @Nullable
public Number getMaxX() {
return maxX;
}
+ @Nullable
public Number getMinY() {
return minY;
}
+ @Nullable
public Number getMaxY() {
return maxY;
}
+ public void setMinX(@Nullable Number minX) {
+ this.minX = minX;
+ }
+
+ public void setMaxX(@Nullable Number maxX) {
+ this.maxX = maxX;
+ }
+
+ public void setMinY(@Nullable Number minY) {
+ this.minY = minY;
+ }
+
+ public void setMaxY(@Nullable Number maxY) {
+ this.maxY = maxY;
+ }
+
+ @NonNull
public XYFramingModel getDomainFramingModel() {
return domainFramingModel;
}
- public void setDomainFramingModel(XYFramingModel domainFramingModel) {
+ public void setDomainFramingModel(@NonNull XYFramingModel domainFramingModel) {
this.domainFramingModel = domainFramingModel;
}
+ @NonNull
public XYFramingModel getRangeFramingModel() {
return rangeFramingModel;
}
- public void setRangeFramingModel(XYFramingModel rangeFramingModel) {
+ public void setRangeFramingModel(@NonNull XYFramingModel rangeFramingModel) {
this.rangeFramingModel = rangeFramingModel;
}
+ @NonNull
public BoundaryMode getDomainUpperBoundaryMode() {
return domainUpperBoundaryMode;
}
- public void setDomainUpperBoundaryMode(BoundaryMode domainUpperBoundaryMode) {
+ public void setDomainUpperBoundaryMode(@NonNull BoundaryMode domainUpperBoundaryMode) {
this.domainUpperBoundaryMode = domainUpperBoundaryMode;
}
+ @NonNull
public BoundaryMode getDomainLowerBoundaryMode() {
return domainLowerBoundaryMode;
}
- public void setDomainLowerBoundaryMode(BoundaryMode domainLowerBoundaryMode) {
+ public void setDomainLowerBoundaryMode(@NonNull BoundaryMode domainLowerBoundaryMode) {
this.domainLowerBoundaryMode = domainLowerBoundaryMode;
}
+ @NonNull
public BoundaryMode getRangeUpperBoundaryMode() {
return rangeUpperBoundaryMode;
}
- public void setRangeUpperBoundaryMode(BoundaryMode rangeUpperBoundaryMode) {
+ public void setRangeUpperBoundaryMode(@NonNull BoundaryMode rangeUpperBoundaryMode) {
this.rangeUpperBoundaryMode = rangeUpperBoundaryMode;
}
+ @NonNull
public BoundaryMode getRangeLowerBoundaryMode() {
return rangeLowerBoundaryMode;
}
- public void setRangeLowerBoundaryMode(BoundaryMode rangeLowerBoundaryMode) {
+ public void setRangeLowerBoundaryMode(@NonNull BoundaryMode rangeLowerBoundaryMode) {
this.rangeLowerBoundaryMode = rangeLowerBoundaryMode;
}
- public void setMinX(Number minX) {
- this.minX = minX;
- }
-
- public void setMaxX(Number maxX) {
- this.maxX = maxX;
- }
-
- public void setMinY(Number minY) {
- this.minY = minY;
- }
-
- public void setMaxY(Number maxY) {
- this.maxY = maxY;
+ @Override
+ public String toString() {
+ return "XYConstraints{" + "domainFramingModel=" + domainFramingModel +
+ ", rangeFramingModel=" + rangeFramingModel +
+ ", domainUpperBoundaryMode=" + domainUpperBoundaryMode +
+ ", domainLowerBoundaryMode=" + domainLowerBoundaryMode +
+ ", rangeUpperBoundaryMode=" + rangeUpperBoundaryMode +
+ ", rangeLowerBoundaryMode=" + rangeLowerBoundaryMode +
+ ", minX=" + minX +
+ ", maxX=" + maxX +
+ ", minY=" + minY +
+ ", maxY=" + maxY +
+ '}';
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java b/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java
index 67edd506..1bd6c0c8 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java
@@ -23,6 +23,8 @@ public class XYCoords {
public Number x;
public Number y;
+ public XYCoords() {}
+
public XYCoords(Number x, Number y) {
this.x = x;
this.y = y;
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java
index 59b0b394..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,22 +16,38 @@
package com.androidplot.xy;
-import android.content.res.*;
-import android.graphics.*;
-
-import com.androidplot.*;
-import com.androidplot.exception.PlotRenderException;
-import com.androidplot.ui.*;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.androidplot.R;
+import com.androidplot.Region;
+import com.androidplot.ui.Insets;
+import com.androidplot.ui.LayoutManager;
+import com.androidplot.ui.RenderStack;
+import com.androidplot.ui.Size;
import com.androidplot.ui.widget.Widget;
-import com.androidplot.util.*;
+import com.androidplot.util.AttrUtils;
+import com.androidplot.util.FontUtils;
+import com.androidplot.util.PixelUtils;
+import com.androidplot.util.RectFUtils;
import java.text.DecimalFormat;
import java.text.Format;
-import java.util.*;
+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 {
@@ -46,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
*/
@@ -111,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;
@@ -120,53 +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();
-
- public float getLineExtensionTop() {
- return lineExtensionTop;
- }
-
- public void setLineExtensionTop(float lineExtensionTop) {
- this.lineExtensionTop = lineExtensionTop;
- }
-
- public float getLineExtensionBottom() {
- return lineExtensionBottom;
- }
-
- public void setLineExtensionBottom(float lineExtensionBottom) {
- this.lineExtensionBottom = lineExtensionBottom;
- }
-
- public float getLineExtensionLeft() {
- return lineExtensionLeft;
- }
-
- public void setLineExtensionLeft(float lineExtensionLeft) {
- this.lineExtensionLeft = lineExtensionLeft;
- }
-
- public float getLineExtensionRight() {
- return lineExtensionRight;
- }
-
- public void setLineExtensionRight(float lineExtensionRight) {
- this.lineExtensionRight = lineExtensionRight;
- }
+ 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 {
@@ -174,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);
}
}
@@ -190,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;
}
@@ -215,80 +208,92 @@ public void setPaint(Paint paint) {
}
}
- protected HashMap getDefaultLineLabelStyles() {
- HashMap defaults = new HashMap<>();
- defaults.put(Edge.TOP, new LineLabelStyle());
- defaults.put(Edge.BOTTOM, new LineLabelStyle());
- defaults.put(Edge.LEFT, new LineLabelStyle());
- defaults.put(Edge.RIGHT, new LineLabelStyle());
- return defaults;
- }
+ public interface CursorLabelFormatter {
- protected HashMap getDefaultLineLabelRenderers() {
- HashMap defaults = new HashMap<>();
- defaults.put(Edge.TOP, new LineLabelRenderer());
- defaults.put(Edge.BOTTOM, new LineLabelRenderer());
- defaults.put(Edge.LEFT, new LineLabelRenderer());
- defaults.put(Edge.RIGHT, new LineLabelRenderer());
- return defaults;
- }
+ /**
+ * @return The Paint to be used to draw the cursor text label.
+ */
+ Paint getTextPaint();
- public LineLabelRenderer getLineLabelRenderer(Edge edge) {
- return lineLabelRenderers.get(edge);
- }
+ /**
+ * @return Null if no background should be drawn, the Paint used to draw the background
+ * otherwise.
+ */
+ Paint getBackgroundPaint();
- public void setLineLabelRenderer(Edge edge, LineLabelRenderer renderer) {
- lineLabelRenderers.put(edge, renderer);
+ String getLabelText(Number x, Number y);
}
- public LineLabelStyle getLineLabelStyle(Edge edge) {
- return lineLabelStyles.get(edge);
- }
+ public enum Edge {
+ NONE(0),
+ LEFT(1),
+ RIGHT(2),
+ TOP(4),
+ BOTTOM(8);
- public void setLineLabelStyle(Edge edge, LineLabelStyle style) {
- lineLabelStyles.put(edge, style);
- }
+ private final int value;
- public CursorLabelFormatter getCursorLabelFormatter() {
- return cursorLabelFormatter;
- }
+ Edge(int value) {
+ this.value = value;
+ }
- public void setCursorLabelFormatter(
- CursorLabelFormatter cursorLabelFormatter) {
- this.cursorLabelFormatter = cursorLabelFormatter;
+ public int getValue() {
+ return value;
+ }
}
- public interface CursorLabelFormatter {
+ {
+ gridBackgroundPaint = new Paint();
+ gridBackgroundPaint.setColor(Color.rgb(140, 140, 140));
+ gridBackgroundPaint.setStyle(Paint.Style.FILL);
- /**
- *
- * @return The Paint to be used to draw the cursor text label.
- */
- Paint getTextPaint();
+ final Paint defaultLinePaint = new Paint();
+ defaultLinePaint.setColor(Color.rgb(180, 180, 180));
+ defaultLinePaint.setAntiAlias(true);
+ defaultLinePaint.setStyle(Paint.Style.STROKE);
- /**
- *
- * @return Null if no background should be drawn,
- * the Paint used to draw the background otherwise.
- */
- Paint getBackgroundPaint();
- String getLabelText(Number x, Number y);
+ rangeGridLinePaint = new Paint(defaultLinePaint);
+ domainGridLinePaint = new Paint(defaultLinePaint);
+ domainSubGridLinePaint = new Paint(defaultLinePaint);
+ rangeSubGridLinePaint = new Paint(defaultLinePaint);
+ domainOriginLinePaint = new Paint(defaultLinePaint);
+ rangeOriginLinePaint = new Paint(defaultLinePaint);
+
+ domainCursorPaint = new Paint();
+ domainCursorPaint.setColor(Color.YELLOW);
+
+ rangeCursorPaint = new Paint();
+ rangeCursorPaint.setColor(Color.YELLOW);
+
+ setMarginTop(7);
+ setMarginRight(4);
+ setMarginBottom(4);
+ setClippingEnabled(true);
+ }
+
+ public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) {
+ super(layoutManager, size);
+ this.plot = plot;
+ renderStack = new RenderStack(plot);
}
/**
* 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);
@@ -297,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()));
@@ -323,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,
@@ -358,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,
@@ -366,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,
@@ -374,263 +391,164 @@ 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
+ );
- // graphWidget
+ // rotation
+ AttrUtils.configureWidgetRotation(attrs, this, R.styleable.xy_XYPlot_graphRotation);
+
+ // padding & margin
AttrUtils.configureBoxModelable(attrs, this,
R.styleable.xy_XYPlot_graphMarginTop, R.styleable.xy_XYPlot_graphMarginBottom,
R.styleable.xy_XYPlot_graphMarginLeft, R.styleable.xy_XYPlot_graphMarginRight,
R.styleable.xy_XYPlot_graphPaddingTop, R.styleable.xy_XYPlot_graphPaddingBottom,
- R.styleable.xy_XYPlot_graphPaddingLeft, R.styleable.xy_XYPlot_graphPaddingRight);
+ 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);
-
- getBackgroundPaint().setColor(attrs.getColor(
- R.styleable.xy_XYPlot_graphBackgroundColor,
- getBackgroundPaint().getColor()));
-
- getGridBackgroundPaint().setColor(attrs.getColor(
- R.styleable.xy_XYPlot_gridBackgroundColor,
- getGridBackgroundPaint().getColor()));
- }
+ R.styleable.xy_XYPlot_rangeLineThickness
+ );
- /**
- * Grid insets
- */
- public Insets getGridInsets() {
- return gridInsets;
- }
+ AttrUtils.setColor(attrs, getBackgroundPaint(),
+ R.styleable.xy_XYPlot_graphBackgroundColor
+ );
- public void setGridInsets(Insets gridInsets) {
- this.gridInsets = gridInsets;
+ AttrUtils.setColor(attrs, getGridBackgroundPaint(),
+ R.styleable.xy_XYPlot_gridBackgroundColor
+ );
}
/**
- * Domain / Range label insets
+ * 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 Insets getLineLabelInsets() {
- return lineLabelInsets;
- }
-
- public void setLineLabelInsets(Insets lineLabelInsets) {
- this.lineLabelInsets = lineLabelInsets;
- }
-
- public RectF getGridRect() {
- return gridRect;
- }
-
- public void setGridRect(RectF gridRect) {
- this.gridRect = gridRect;
- }
-
- public RectF getLabelRect() {
- return labelRect;
- }
-
- public void setLabelRect(RectF labelRect) {
- this.labelRect = labelRect;
- }
-
- public boolean isGridClippingEnabled() {
- return isGridClippingEnabled;
- }
-
- public void setGridClippingEnabled(boolean gridClippingEnabled) {
- isGridClippingEnabled = gridClippingEnabled;
- }
-
- public boolean isLineLabelEnabled(Edge position) {
- return lineLabelEdges.contains(position);
- }
-
- public void setLineLabelEdges(Edge... positions) {
- Set positionSet = new HashSet<>();
- if(positions != null) {
- for(Edge position : positions) {
- positionSet.add(position);
- }
- }
- setLineLabelEdges(positionSet);
- }
-
- public void setLineLabelEdges(Set positions) {
- this.lineLabelEdges = positions;
- }
-
- protected void setLineLabelEdges(int bitfield) {
- for(Edge tp : Edge.values()) {
- if((tp.value & bitfield) == tp.value) {
- lineLabelEdges.add(tp);
- }
- }
- }
-
- public enum Edge {
- LEFT(1),
- RIGHT(2),
- TOP(4),
- BOTTOM(8);
-
- private final int value;
-
- Edge(int value) {
- this.value = value;
- }
-
- public int getValue() {
- return value;
+ protected XYCoords screenToSeries(PointF point) {
+ if (!plot.getBounds().isFullyDefined()) {
+ return null;
}
- }
-
- public Paint getDomainCursorPaint() {
- return domainCursorPaint;
+ return new RectRegion(gridRect)
+ .transform(point.x, point.y, plot.getBounds(), false, true);
}
/**
+ * 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 domainCursorPaint The {@link Paint} used to draw the domain cursor line.
- * Set to null (default) to disable.
+ * @param point
+ * @return
*/
- public void setDomainCursorPaint(Paint domainCursorPaint) {
- this.domainCursorPaint = domainCursorPaint;
- }
-
- public Paint getRangeCursorPaint() {
- return rangeCursorPaint;
+ protected Number screenToSeriesX(PointF point) {
+ return screenToSeriesX(point.x);
}
/**
- *
- * @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;
- }
-
- {
- gridBackgroundPaint = new Paint();
- gridBackgroundPaint.setColor(Color.rgb(140, 140, 140));
- gridBackgroundPaint.setStyle(Paint.Style.FILL);
-
- final Paint defaultLinePaint = new Paint();
- defaultLinePaint.setColor(Color.rgb(180, 180, 180));
- defaultLinePaint.setAntiAlias(true);
- defaultLinePaint.setStyle(Paint.Style.STROKE);
-
- rangeGridLinePaint = new Paint(defaultLinePaint);
- domainGridLinePaint = new Paint(defaultLinePaint);
- domainSubGridLinePaint = new Paint(defaultLinePaint);
- rangeSubGridLinePaint = new Paint(defaultLinePaint);
- domainOriginLinePaint = new Paint(defaultLinePaint);
- rangeOriginLinePaint = new Paint(defaultLinePaint);
-
- domainCursorPaint = new Paint();
- domainCursorPaint.setColor(Color.YELLOW);
-
- rangeCursorPaint = new Paint();
- rangeCursorPaint.setColor(Color.YELLOW);
-
- setMarginTop(7);
- setMarginRight(4);
- setMarginBottom(4);
- setClippingEnabled(true);
- }
-
- public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) {
- super(layoutManager, size);
- this.plot = plot;
- renderStack = new RenderStack(plot);
- }
-
- /**
- * Convenience method. Wraps getYVal(float)
+ * Convenience method. Wraps screenToSeriesY(float)
+ * This is a relatively slow operation and should not be used for operations that are a part of
+ * the main render loop of a dynamic plot.
*
* @param point
* @return
*/
- public Double getYVal(PointF point) {
- return getYVal(point.y);
+ protected Number screenToSeriesY(PointF point) {
+ return screenToSeriesY(point.y);
}
/**
- * Converts a y pixel to a y value.
+ * 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 yPix
+ * @param xPix
* @return
*/
- public Double getYVal(float yPix) {
- if (plot.getCalculatedMinY() == null
- || plot.getCalculatedMaxY() == null) {
+ protected Number screenToSeriesX(float xPix) {
+ if (!plot.getBounds().xRegion.isDefined()) {
return null;
}
- return ValPixConverter.pixToVal(yPix - gridRect.top, plot
- .getCalculatedMinY().doubleValue(), plot.getCalculatedMaxY()
- .doubleValue(), gridRect.height(), true);
+ return new Region(gridRect.left, gridRect.right)
+ .transform(xPix, plot.getBounds().getxRegion());
}
/**
- * Convenience method. Wraps getXVal(float)
+ * 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 point
+ * @param yPix
* @return
*/
- public Double getXVal(PointF point) {
- return getXVal(point.x);
+ protected Number screenToSeriesY(float yPix) {
+ if (!plot.getBounds().getyRegion().isDefined()) {
+ return null;
+ }
+ return new Region(gridRect.top, gridRect.bottom)
+ .transform(yPix, plot.getBounds().getyRegion(), true);
}
- /**
- * Converts an x pixel into an x value.
- *
- * @param xPix
- * @return
- */
- public Double getXVal(float xPix) {
- if (plot.getCalculatedMinX() == null
- || plot.getCalculatedMaxX() == null) {
+ protected PointF seriesToScreen(XYCoords xy) {
+ if (!plot.getBounds().isFullyDefined()) {
return null;
}
- return ValPixConverter.pixToVal(xPix - gridRect.left, plot
- .getCalculatedMinX().doubleValue(), plot.getCalculatedMaxX()
- .doubleValue(), gridRect.width(), false);
+ return plot.getBounds().transform(xy, gridRect, false, true);
+ }
+
+ protected float seriesToScreenX(Number x) {
+ return (float) plot.getBounds().getxRegion().
+ transform(x.doubleValue(), gridRect.left, gridRect.right, false);
+ }
+
+ protected float seriesToScreenY(Number y) {
+ return (float) plot.getBounds().getyRegion().
+ transform(y.doubleValue(), gridRect.bottom, gridRect.top, true);
}
@Override
- protected void doOnDraw(Canvas canvas, RectF widgetRect)
- throws PlotRenderException {
+ protected void onResize(@Nullable RectF oldRect, @NonNull RectF newRect) {
+ recalculateSizes(newRect);
+ }
- if(gridRect == null) {
- gridRect = RectFUtils.applyInsets(widgetRect, gridInsets);
+ protected void recalculateSizes(@Nullable RectF rect) {
+ if(rect == null) {
+ rect = getWidgetDimensions().paddedRect;
}
+ gridRect = RectFUtils.applyInsets(rect, gridInsets);
+ labelRect = RectFUtils.applyInsets(rect, lineLabelInsets);
+ }
- if(labelRect == null) {
- labelRect = RectFUtils.applyInsets(widgetRect, lineLabelInsets);
- }
+ @Override
+ protected void doOnDraw(Canvas canvas, RectF widgetRect) {
// don't draw if we have no space to draw into
if (gridRect.height() > ZERO && gridRect.width() > ZERO) {
- if (plot.getCalculatedMinX() != null
- && plot.getCalculatedMaxX() != null
- && plot.getCalculatedMinY() != null
- && plot.getCalculatedMaxY() != null) {
- if(drawGridOnTop) {
+ final RectRegion bounds = plot.getBounds();
+ if (bounds.getMinX() != null
+ && bounds.getMaxX() != null
+ && bounds.getMinY() != null
+ && bounds.getMaxY() != null) {
+ if (drawGridOnTop) {
drawData(canvas);
drawGrid(canvas);
} else {
@@ -645,219 +563,152 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect)
}
}
- private void drawDomainLine(Canvas canvas, float xPix, Number xVal,
- Paint linePaint, boolean isOrigin) {
+ protected void drawDomainLine(Canvas canvas, float xPix, Number xVal,
+ 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);
+ }
+ }
}
- public void drawRangeLine(Canvas canvas, float yPix, Number yVal,
- Paint linePaint, boolean isOrigin) {
+ protected void drawRangeLine(Canvas canvas, float yPix, Number yVal,
+ Paint linePaint, boolean isOrigin, boolean shouldDrawLabel) {
// lines
if (linePaint != null) {
canvas.drawLine(gridRect.left - lineExtensionLeft, yPix,
- gridRect.right + lineExtensionRight, yPix, linePaint);
+ 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);
}
- double domainOrigin;
- if (plot.getDomainOrigin() != null) {
- double domainOriginVal = plot.getDomainOrigin().doubleValue();
- domainOrigin = ValPixConverter.valToPix(domainOriginVal, plot
- .getCalculatedMinX().doubleValue(), plot
- .getCalculatedMaxX().doubleValue(), gridRect.width(),
- false);
- domainOrigin += gridRect.left;
- // if no origin is set, use the leftmost value visible on the grid:
- } else {
- domainOrigin = gridRect.left;
- }
-
- Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN,
- gridRect, plot.getCalculatedMinX().doubleValue(), plot
- .getCalculatedMaxX().doubleValue());
- // draw domain origin:
- if (domainOrigin >= gridRect.left
- && domainOrigin <= gridRect.right) {
- drawDomainLine(canvas, (float) domainOrigin, plot.getDomainOrigin()
- .doubleValue(), domainOriginLinePaint, true);
- }
-
- // draw lines LEFT of origin:
- double xPix = domainOrigin - domainStep.getStepPix();
- for (int i = ONE; xPix >= gridRect.left - FUDGE; xPix = domainOrigin
- - (i * domainStep.getStepPix())) {
- double xVal = plot.getDomainOrigin().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++;
+ Number domainOrigin = plot.getDomainOrigin();
+ 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();
}
- // draw lines RIGHT of origin:
- xPix = domainOrigin + domainStep.getStepPix();
- for (int i = ONE; xPix <= gridRect.right + FUDGE; xPix = domainOrigin
- + (i * domainStep.getStepPix())) {
- double xVal = plot.getDomainOrigin().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);
+ Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect);
+
+ // 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);
}
- double rangeOrigin;
- if (plot.getRangeOrigin() != null) {
- double rangeOriginD = plot.getRangeOrigin().doubleValue();
- rangeOrigin = ValPixConverter.valToPix(rangeOriginD, plot
- .getCalculatedMinY().doubleValue(), plot
- .getCalculatedMaxY().doubleValue(),
- gridRect.height(), true);
- rangeOrigin += gridRect.top;
- // if no origin is set, use the leftmost value visible on the grid
+ Number rangeOrigin = plot.getRangeOrigin();
+ final double rangeOriginPix;
+ if (rangeOrigin != null) {
+ rangeOriginPix = plot.getBounds().getyRegion().transform(
+ rangeOrigin.doubleValue(), gridRect.top, gridRect.bottom, true);
} else {
- rangeOrigin = gridRect.bottom;
+ // if no range origin is set, use the bottom-most value visible on the grid:
+ rangeOriginPix = gridRect.bottom;
+ rangeOrigin = plot.getBounds().getMinY();
}
- Step rangeStep = XYStepCalculator.getStep(plot, Axis.RANGE,
- gridRect, plot.getCalculatedMinY().doubleValue(), plot
- .getCalculatedMaxY().doubleValue());
+ Step rangeStep = XYStepCalculator.getStep(plot, Axis.RANGE, gridRect);
- // draw range origin:
- if (rangeOrigin >= gridRect.top && rangeOrigin <= gridRect.bottom) {
- drawRangeLine(canvas, (float) rangeOrigin, plot.getRangeOrigin()
- .doubleValue(), rangeOriginLinePaint, true);
- }
+ // Draw Range Lines:
final double rangeStepPix = rangeStep.getStepPix();
-
- // draw lines ABOVE origin:
- double yPix = rangeOrigin - rangeStep.getStepPix();
- for (int i = ONE; yPix >= gridRect.top - FUDGE; yPix = rangeOrigin - (i * rangeStepPix)) {
- double yVal = plot.getRangeOrigin().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 = rangeOrigin + rangeStep.getStepPix();
- for (int i = ONE; yPix <= gridRect.bottom + FUDGE; yPix = rangeOrigin + (i * rangeStepPix)) {
- double yVal = plot.getRangeOrigin().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) {
- for (YValueMarker marker : plot.getYValueMarkers()) {
- if (marker.getValue() != null) {
- double yVal = marker.getValue().doubleValue();
- float yPix = (float) ValPixConverter.valToPix(yVal, plot
- .getCalculatedMinY().doubleValue(), plot
- .getCalculatedMaxY().doubleValue(), gridRect.height(), true);
- yPix += gridRect.top;
- 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);
- }
+ if (plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) {
+ for (YValueMarker marker : plot.getYValueMarkers()) {
+ marker.draw(canvas, plot, gridRect);
}
}
- for (XValueMarker marker : plot.getXValueMarkers()) {
- if (marker.getValue() != null) {
- double xVal = marker.getValue().doubleValue();
- float xPix = (float) ValPixConverter.valToPix(xVal, plot
- .getCalculatedMinX().doubleValue(), plot
- .getCalculatedMaxX().doubleValue(), gridRect.width(), false);
- xPix += gridRect.left;
- 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);
- }
+ if (plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) {
+ for (XValueMarker marker : plot.getXValueMarkers()) {
+ marker.draw(canvas, plot, gridRect);
}
}
}
@@ -866,58 +717,69 @@ 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) {
- final String label = getCursorLabelFormatter().
- getLabelText(getDomainCursorVal(), getRangeCursorVal());
-
- // convert the label dimensions rect into floating-point:
- RectF cursorRect = new RectF(FontUtils.getPackedStringDimensions(
- label, getCursorLabelFormatter().getTextPaint()));
- cursorRect.offsetTo(domainCursorPosition, rangeCursorPosition
- - cursorRect.height());
-
- // if we are too close to the right edge of the plot, we will move
- // the label to the left side of our cursor:
- if (cursorRect.right >= gridRect.right) {
- cursorRect.offsetTo(domainCursorPosition - cursorRect.width(),
- cursorRect.top);
- }
+ if (getCursorLabelFormatter() != null && hasRangeCursor && hasDomainCursor) {
+ drawCursorLabel(canvas);
+ }
+ }
- // same thing for the top edge of the plot:
- // dunno why but these rects can have negative values for top and bottom.
- if (cursorRect.top <= gridRect.top) {
- cursorRect.offsetTo(cursorRect.left, rangeCursorPosition);
- }
+ protected void drawCursorLabel(Canvas canvas) {
+ final String label = getCursorLabelFormatter().
+ getLabelText(getDomainCursorVal(), getRangeCursorVal());
- if (getCursorLabelFormatter().getBackgroundPaint() != null) {
- canvas.drawRect(cursorRect, getCursorLabelFormatter().getBackgroundPaint());
- }
+ // convert the label dimensions rect into floating-point:
+ RectF cursorRect = new RectF(FontUtils.getPackedStringDimensions(
+ label, getCursorLabelFormatter().getTextPaint()));
+ cursorRect.offsetTo(domainCursorPosition, rangeCursorPosition
+ - cursorRect.height());
+
+ // if we are too close to the right edge of the plot, we will move
+ // the label to the left side of our cursor:
+ if (cursorRect.right >= gridRect.right) {
+ cursorRect.offsetTo(
+ domainCursorPosition - cursorRect.width(),
+ cursorRect.top
+ );
+ }
- canvas.drawText(label, cursorRect.left, cursorRect.bottom,
- getCursorLabelFormatter().getTextPaint());
+ // same thing for the top edge of the plot:
+ // dunno why but these rects can have negative values for top and bottom.
+ if (cursorRect.top <= gridRect.top) {
+ cursorRect.offsetTo(cursorRect.left, rangeCursorPosition);
}
+
+ if (getCursorLabelFormatter().getBackgroundPaint() != null) {
+ canvas.drawRect(cursorRect, getCursorLabelFormatter().getBackgroundPaint());
+ }
+
+ canvas.drawText(label, cursorRect.left, cursorRect.bottom,
+ getCursorLabelFormatter().getTextPaint()
+ );
}
protected void drawGridBackground(Canvas canvas) {
- if(gridBackgroundPaint != null) {
+ if (gridBackgroundPaint != null) {
canvas.drawRect(gridRect, gridBackgroundPaint);
}
}
@@ -926,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(
@@ -950,7 +811,7 @@ protected void drawData(Canvas canvas) throws PlotRenderException {
}
} finally {
- if(isGridClippingEnabled) {
+ if (isGridClippingEnabled) {
canvas.restore();
}
}
@@ -977,6 +838,7 @@ public Paint getDomainGridLinePaint() {
/**
* Set the paint used to draw the domain grid line.
+ *
* @param gridLinePaint
*/
public void setDomainGridLinePaint(Paint gridLinePaint) {
@@ -999,6 +861,7 @@ public Paint getDomainSubGridLinePaint() {
/**
* Set the paint used to draw the domain grid line.
+ *
* @param gridLinePaint
*/
public void setDomainSubGridLinePaint(Paint gridLinePaint) {
@@ -1007,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) {
@@ -1022,6 +886,7 @@ public Paint getRangeSubGridLinePaint() {
/**
* Set the Paint used to draw the range grid line.
+ *
* @param gridLinePaint
*/
public void setRangeSubGridLinePaint(Paint gridLinePaint) {
@@ -1060,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 Double getDomainCursorVal() {
- return getXVal(getDomainCursorPosition());
+ public Number getDomainCursorVal() {
+ 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 Double getRangeCursorVal() {
- return getYVal(getRangeCursorPosition());
+ public Number getRangeCursorVal() {
+ 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;
}
@@ -1109,6 +995,177 @@ public void setDrawMarkersEnabled(boolean drawMarkersEnabled) {
this.drawMarkersEnabled = drawMarkersEnabled;
}
+ public Paint getDomainCursorPaint() {
+ return domainCursorPaint;
+ }
+
+ /**
+ * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line. Set to null
+ * (default) to disable.
+ */
+ public void setDomainCursorPaint(Paint domainCursorPaint) {
+ this.domainCursorPaint = domainCursorPaint;
+ }
+
+ public Paint getRangeCursorPaint() {
+ return rangeCursorPaint;
+ }
+
+ /**
+ * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line. Set to null
+ * (default) to disable.
+ */
+ public void setRangeCursorPaint(Paint rangeCursorPaint) {
+ this.rangeCursorPaint = rangeCursorPaint;
+ }
+
+ public float getLineExtensionTop() {
+ return lineExtensionTop;
+ }
+
+ public void setLineExtensionTop(float lineExtensionTop) {
+ this.lineExtensionTop = lineExtensionTop;
+ }
+
+ public float getLineExtensionBottom() {
+ return lineExtensionBottom;
+ }
+
+ public void setLineExtensionBottom(float lineExtensionBottom) {
+ this.lineExtensionBottom = lineExtensionBottom;
+ }
+
+ public float getLineExtensionLeft() {
+ return lineExtensionLeft;
+ }
+
+ public void setLineExtensionLeft(float lineExtensionLeft) {
+ this.lineExtensionLeft = lineExtensionLeft;
+ }
+
+ public float getLineExtensionRight() {
+ return lineExtensionRight;
+ }
+
+ public void setLineExtensionRight(float lineExtensionRight) {
+ this.lineExtensionRight = lineExtensionRight;
+ }
+
+ protected Map getDefaultLineLabelStyles() {
+ EnumMap defaults = new EnumMap<>(Edge.class);
+ defaults.put(Edge.TOP, new LineLabelStyle());
+ defaults.put(Edge.BOTTOM, new LineLabelStyle());
+ defaults.put(Edge.LEFT, new LineLabelStyle());
+ defaults.put(Edge.RIGHT, new LineLabelStyle());
+ return defaults;
+ }
+
+ protected Map getDefaultLineLabelRenderers() {
+ EnumMap defaults = new EnumMap<>(Edge.class);
+ defaults.put(Edge.TOP, new LineLabelRenderer());
+ defaults.put(Edge.BOTTOM, new LineLabelRenderer());
+ defaults.put(Edge.LEFT, new LineLabelRenderer());
+ defaults.put(Edge.RIGHT, new LineLabelRenderer());
+ return defaults;
+ }
+
+ public LineLabelRenderer getLineLabelRenderer(Edge edge) {
+ return lineLabelRenderers.get(edge);
+ }
+
+ public void setLineLabelRenderer(Edge edge, LineLabelRenderer renderer) {
+ lineLabelRenderers.put(edge, renderer);
+ }
+
+ public LineLabelStyle getLineLabelStyle(Edge edge) {
+ return lineLabelStyles.get(edge);
+ }
+
+ public void setLineLabelStyle(Edge edge, LineLabelStyle style) {
+ lineLabelStyles.put(edge, style);
+ }
+
+ public CursorLabelFormatter getCursorLabelFormatter() {
+ return cursorLabelFormatter;
+ }
+
+ public void setCursorLabelFormatter(
+ CursorLabelFormatter cursorLabelFormatter) {
+ this.cursorLabelFormatter = cursorLabelFormatter;
+ }
+
+ /**
+ * Grid insets
+ */
+ public Insets getGridInsets() {
+ return gridInsets;
+ }
+
+ public void setGridInsets(Insets gridInsets) {
+ this.gridInsets = gridInsets;
+ recalculateSizes(null);
+ }
+
+ /**
+ * Domain / Range label insets
+ */
+ public Insets getLineLabelInsets() {
+ return lineLabelInsets;
+ }
+
+ public void setLineLabelInsets(Insets lineLabelInsets) {
+ this.lineLabelInsets = lineLabelInsets;
+ recalculateSizes(null);
+ }
+
+ public RectF getGridRect() {
+ return gridRect;
+ }
+
+ public void setGridRect(RectF gridRect) {
+ this.gridRect = gridRect;
+ }
+
+ public RectF getLabelRect() {
+ return labelRect;
+ }
+
+ public void setLabelRect(RectF labelRect) {
+ this.labelRect = labelRect;
+ }
+
+ public boolean isGridClippingEnabled() {
+ return isGridClippingEnabled;
+ }
+
+ public void setGridClippingEnabled(boolean gridClippingEnabled) {
+ isGridClippingEnabled = gridClippingEnabled;
+ }
+
+ public boolean isLineLabelEnabled(Edge position) {
+ return lineLabelEdges.contains(position);
+ }
+
+ public void setLineLabelEdges(Edge... positions) {
+ EnumSet positionSet = EnumSet.noneOf(Edge.class);
+ if (positions != null) {
+ Collections.addAll(positionSet, positions);
+ }
+ this.lineLabelEdges = positionSet;
+ }
+
+ 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) {
+ lineLabelEdges.add(tp);
+ }
+ }
+ }
+
/**
* Checks whether the point exists within the visible grid space.
*
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 84edd91c..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,220 +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