From 633ac9854a00435f25498818c577de265851ec6e Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 19 May 2017 19:36:52 -0500 Subject: [PATCH 01/61] Corrected documentation for `OrderedXYSeries` (#42) --- .../src/main/java/com/androidplot/xy/OrderedXYSeries.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java index 659e38ea..820e4330 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java @@ -9,13 +9,13 @@ public interface OrderedXYSeries extends XYSeries { enum XOrder { /** * XVals are in strict ascending order such that: - * x(i) > x(i+1) == true + * x(i) < x(i+1) == true */ ASCENDING, /** * XVals are in strict descending order such that: - * x(i) < x(i+1) == true + * x(i) > x(i+1) == true */ DESCENDING, From ef94704bf105841c316c81f7507e4e5e5a2ee789 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 22 May 2017 22:23:23 -0500 Subject: [PATCH 02/61] Implement descriptive toString() methods for RectRegion and related classes (#44) adds more descriptive toString implementations to various classes --- .../src/main/java/com/androidplot/Region.java | 18 ++++++- .../java/com/androidplot/util/FastNumber.java | 5 ++ .../com/androidplot/util/SeriesUtils.java | 24 +++++---- .../java/com/androidplot/xy/RectRegion.java | 10 ++++ .../com/androidplot/xy/XYConstraints.java | 52 +++++++++++++------ 5 files changed, 81 insertions(+), 28 deletions(-) diff --git a/androidplot-core/src/main/java/com/androidplot/Region.java b/androidplot-core/src/main/java/com/androidplot/Region.java index 2ace04a7..c8820f59 100644 --- a/androidplot-core/src/main/java/com/androidplot/Region.java +++ b/androidplot-core/src/main/java/com/androidplot/Region.java @@ -17,7 +17,7 @@ package com.androidplot; -import com.androidplot.util.*; +import com.androidplot.util.FastNumber; /** * A one dimensional region represented by a starting and ending value. @@ -249,4 +249,20 @@ public void setMax(Number max) { public boolean isDefined() { return min != null && max != null; } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Region{"); + sb.append("min=").append(min); + sb.append(", max=").append(max); + sb.append(", cachedLength=").append(cachedLength); + sb.append(", defaults="); + if (defaults != this) { + sb.append(defaults); + } else { + sb.append("this"); + } + sb.append('}'); + return sb.toString(); + } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java index 049431b3..d54daca4 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java +++ b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java @@ -63,4 +63,9 @@ public double doubleValue() { } return doublePrimitive; } + + @Override + public String toString() { + return String.valueOf(doubleValue()); + } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java index a9f09192..e74a8f3b 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java @@ -16,10 +16,14 @@ package com.androidplot.util; -import com.androidplot.*; -import com.androidplot.xy.*; +import com.androidplot.Region; +import com.androidplot.xy.FastXYSeries; +import com.androidplot.xy.OrderedXYSeries; +import com.androidplot.xy.RectRegion; +import com.androidplot.xy.XYConstraints; +import com.androidplot.xy.XYSeries; -import java.util.*; +import java.util.List; /** * Utilities for dealing with Series data. @@ -83,18 +87,18 @@ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArr // if this is an advanced xy series then minMax have already been calculated for us: if(series instanceof FastXYSeries) { - final RectRegion b = ((FastXYSeries) series).minMax(); - if(b == null) { + final RectRegion seriesBounds = ((FastXYSeries) series).minMax(); + if (seriesBounds == null) { continue; } if(constraints == null) { - bounds.union(b); + bounds.union(seriesBounds); } else { - if(constraints.contains(b.getMinX(), b.getMinY())) { - bounds.union(b.getMinX(), b.getMinY()); + if (constraints.contains(seriesBounds.getMinX(), seriesBounds.getMinY())) { + bounds.union(seriesBounds.getMinX(), seriesBounds.getMinY()); } - if(constraints.contains(b.getMaxX(), b.getMaxY())) { - bounds.union(b.getMaxX(), b.getMaxY()); + if (constraints.contains(seriesBounds.getMaxX(), seriesBounds.getMaxY())) { + bounds.union(seriesBounds.getMaxX(), seriesBounds.getMaxY()); } } 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 7eeb0d73..5a1433f7 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java @@ -20,6 +20,7 @@ import android.graphics.RectF; import com.androidplot.Region; + import java.util.ArrayList; import java.util.List; @@ -344,4 +345,13 @@ public boolean isFullyDefined() { 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/XYConstraints.java b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java index ae22a4ea..2ee06adf 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java @@ -18,6 +18,7 @@ /** * Calculates the min/max constraints for an xy plane. + * * @since 0.9.7 */ public class XYConstraints { @@ -52,26 +53,26 @@ public XYConstraints(Number minX, Number maxX, Number minY, Number maxY) { } 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; - } - } - 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; } public Number getMinX() { @@ -153,4 +154,21 @@ public void setMinY(Number minY) { public void setMaxY(Number maxY) { this.maxY = maxY; } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("XYConstraints{"); + sb.append("domainFramingModel=").append(domainFramingModel); + sb.append(", rangeFramingModel=").append(rangeFramingModel); + sb.append(", domainUpperBoundaryMode=").append(domainUpperBoundaryMode); + sb.append(", domainLowerBoundaryMode=").append(domainLowerBoundaryMode); + sb.append(", rangeUpperBoundaryMode=").append(rangeUpperBoundaryMode); + sb.append(", rangeLowerBoundaryMode=").append(rangeLowerBoundaryMode); + sb.append(", minX=").append(minX); + sb.append(", maxX=").append(maxX); + sb.append(", minY=").append(minY); + sb.append(", maxY=").append(maxY); + sb.append('}'); + return sb.toString(); + } } From 7dc7055e5c7eee2d99c0343a79ba072e9d1d34af Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 26 May 2017 17:41:20 -0500 Subject: [PATCH 03/61] Many Performance Improvements (#47) * Corrected documentation for `OrderedXYSeries` * Rename local variable in SeriesUtils.java to better describe its contents. Android studio also replaced wildcards in imports probably to match AOSP style guides https://source.android.com/source/code-style#fully-qualify-imports * Implements `toString` methods for `RectRegion` and related classes for easier debugging. * Refactoring only - reduced nesting in XYConstraints.java * Rename local variable in SeriesUtils.java to better describe its contents. Android studio also replaced wildcards in imports probably to match AOSP style guides https://source.android.com/source/code-style#fully-qualify-imports * Implements `toString` methods for `RectRegion` and related classes for easier debugging. * Refactoring only - reduced nesting in XYConstraints.java * Added shortcut to XYConstraints.java#contains for situations where there are no defined constraints. This avoids getting the double value of two numbers. * Removed redundant array creation in Redrawer.java * Replaced manual array copy operations with possibly faster method (will vary by android device) * Fixed `FastLineAndPointRenderer` allocating many extra instances of `PointF` which hurts android performance. * Prevent creation of a new instance of `FastNumber` when setting the min or max of a `Region` * Replaced empty string concatenation with more efficient `String.valueOf` * Replaced HashMaps with EnumMaps where possible in XYGraphWidget.java * Replaced stringbuffer with string in XYConstraints.java#toString() * Replaced HashSet with EnumSet * Replace float new instance with Float.valueOf * Replaced StringBuffer with StringBuilder * set initial array sizes * Removed the primative cache from FastNumber#equals and FastNumber#hashcode() and made the number field final * Made methods static where possible * Add Unit Tests for FastNumber.java#equals and FastNumber.java#hashCode * FastNumber.java does not allow null * Annotated all method/parameters in overrides which aren't annotated as the method/parameter they override. * Inferred nullity annotations for `FastNumber` and `FixedSizeEditableXYSeries` * fix typo * Simplified unit tests for FastNumberTest.java --- .../src/main/java/com/androidplot/Region.java | 6 +- .../java/com/androidplot/SeriesRegistry.java | 4 +- .../com/androidplot/ui/DynamicTableModel.java | 4 +- .../com/androidplot/ui/LayoutManager.java | 10 +- .../com/androidplot/ui/PositionMetrics.java | 4 +- .../java/com/androidplot/util/FastNumber.java | 39 ++++- .../java/com/androidplot/util/Redrawer.java | 9 +- .../com/androidplot/xy/BubbleFormatter.java | 11 +- .../xy/FastLineAndPointRenderer.java | 16 ++- .../xy/FixedSizeEditableXYSeries.java | 15 +- .../java/com/androidplot/xy/RectRegion.java | 2 +- .../com/androidplot/xy/SimpleXYSeries.java | 13 +- .../com/androidplot/xy/XYConstraints.java | 29 ++-- .../com/androidplot/xy/XYGraphWidget.java | 54 ++++--- .../main/java/com/androidplot/xy/XYPlot.java | 14 +- .../com/androidplot/xy/XYSeriesFormatter.java | 4 +- .../com/androidplot/util/FastNumberTest.java | 135 ++++++++++++++++++ .../demos/AnimatedXYPlotActivity.java | 7 +- .../demos/CandlestickChartActivity.java | 29 ++-- .../androidplot/demos/DualScaleActivity.java | 20 +-- .../androidplot/demos/ListViewActivity.java | 11 +- .../demos/SimpleXYPlotActivity.java | 17 ++- .../demos/StepChartExampleActivity.java | 18 ++- .../androidplot/demos/TimeSeriesActivity.java | 26 +++- 24 files changed, 380 insertions(+), 117 deletions(-) create mode 100644 androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java diff --git a/androidplot-core/src/main/java/com/androidplot/Region.java b/androidplot-core/src/main/java/com/androidplot/Region.java index c8820f59..3f67bb09 100644 --- a/androidplot-core/src/main/java/com/androidplot/Region.java +++ b/androidplot-core/src/main/java/com/androidplot/Region.java @@ -215,7 +215,7 @@ public void setMin(Number min) { } else { this.min = null; } - } else { + } else if (this.min == null || !this.min.equals(min)) { this.min = new FastNumber(min); } } @@ -237,7 +237,7 @@ public void setMax(Number max) { } else { this.max = null; } - } else { + } else if (this.max == null || !this.max.equals(max)) { this.max = new FastNumber(max); } } @@ -252,7 +252,7 @@ public boolean isDefined() { @Override public String toString() { - final StringBuffer sb = new StringBuffer("Region{"); + final StringBuilder sb = new StringBuilder("Region{"); sb.append("min=").append(min); sb.append(", max=").append(max); sb.append(", cachedLength=").append(cachedLength); diff --git a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java index 78f3d805..7a7b3023 100644 --- a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java +++ b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java @@ -19,7 +19,7 @@ import com.androidplot.ui.Formatter; import com.androidplot.ui.SeriesBundle; -import java.io.*; +import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -38,7 +38,7 @@ public List getSeriesAndFormatterList() { return registry; } public List getSeriesList() { - List result = new ArrayList<>(); + List result = new ArrayList<>(registry.size()); for(SeriesBundle sfPair : registry) { result.add(sfPair.getSeries()); } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java b/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java index c7c7f066..ff973b9d 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/DynamicTableModel.java @@ -149,10 +149,10 @@ public TableModelIterator(DynamicTableModel dynamicTableModel, RectF tableRect, calculatedRows = dynamicTableModel.getNumRows(); // round up: - calculatedColumns = new Float((totalElements / (float) calculatedRows) + 0.5).intValue(); + calculatedColumns = Float.valueOf((totalElements / (float) calculatedRows) + 0.5f).intValue(); } else if(dynamicTableModel.getNumRows() == 0 && dynamicTableModel.getNumColumns() >= 1) { calculatedColumns = dynamicTableModel.getNumColumns(); - calculatedRows = new Float((totalElements / (float) calculatedColumns) + 0.5).intValue(); + calculatedRows = Float.valueOf((totalElements / (float) calculatedColumns) + 0.5f).intValue(); // unlimited rows and columns (impossible) so default a single row with n columns: }else if(dynamicTableModel.getNumColumns() == 0 && dynamicTableModel.getNumRows() == 0) { calculatedRows = 1; diff --git a/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java b/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java index d9107442..9e66e80c 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java @@ -16,9 +16,15 @@ package com.androidplot.ui; -import android.graphics.*; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.Region; import android.view.MotionEvent; import android.view.View; + import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.widget.Widget; import com.androidplot.util.DisplayDimensions; @@ -132,7 +138,7 @@ public void draw(Canvas canvas) throws PlotRenderException { } } - private void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) { + private static void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) { try { canvas.save(Canvas.ALL_SAVE_FLAG); canvas.clipRect(inner, Region.Op.DIFFERENCE); diff --git a/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java b/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java index 3381f20f..88efddcb 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/PositionMetrics.java @@ -16,6 +16,8 @@ package com.androidplot.ui; +import android.support.annotation.NonNull; + public class PositionMetrics implements Comparable { private HorizontalPosition horizontalPosition; @@ -47,7 +49,7 @@ public void setAnchor(Anchor anchor) { } @Override - public int compareTo(PositionMetrics o) { + public int compareTo(@NonNull PositionMetrics o) { if(this.layerDepth < o.layerDepth) { return -1; } else if(this.layerDepth == o.layerDepth) { diff --git a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java index d54daca4..4b113b5d 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java +++ b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java @@ -1,11 +1,15 @@ package com.androidplot.util; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + /** * An extension of {@link Number} optimized for speed at the cost of memory. */ public class FastNumber extends Number { - private Number number; + @NonNull + private final Number number; private boolean hasDoublePrimitive; private boolean hasFloatPrimitive; private boolean hasIntPrimitive; @@ -14,7 +18,11 @@ public class FastNumber extends Number { private float floatPrimitive; private int intPrimitive; - public FastNumber(Number number) { + public FastNumber(@NonNull Number number) { + //noinspection ConstantConditions //in case someone ignores the @NonNull annotation + if (number == null) { + throw new IllegalArgumentException("number parameter cannot be null"); + } // avoid nested instances of FastNumber : if(number instanceof FastNumber) { @@ -64,6 +72,33 @@ public double doubleValue() { return doublePrimitive; } + /** + * To be equal, two instances must both be instances of {@link FastNumber}. The inner {@link + * #number} field must also be a common type. Numbers which are mathematically equal are not + * necessarily equal. This keeps with the java implementation of common Number classes where for + * instance {@code new Integer(0).equals(new Double(0))} returns {@code false} + */ + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FastNumber that = (FastNumber) o; + + return number.equals(that.number); + + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @NonNull @Override public String toString() { return String.valueOf(doubleValue()); diff --git a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java index 00174027..6cf3b4eb 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java +++ b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java @@ -17,11 +17,12 @@ package com.androidplot.util; import android.util.Log; + import com.androidplot.Plot; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -52,7 +53,7 @@ public class Redrawer implements Runnable { * @param startImmediately If true, invokes run() immediately after construction. */ public Redrawer(List plots, float maxRefreshRate, boolean startImmediately) { - this.plots = new ArrayList<>(); + this.plots = new ArrayList<>(plots.size()); for(Plot plot : plots) { this.plots.add(new WeakReference<>(plot)); } @@ -65,7 +66,7 @@ public Redrawer(List plots, float maxRefreshRate, boolean startImmediately } public Redrawer(Plot plot, float maxRefreshRate, boolean startImmediately) { - this(Arrays.asList(new Plot[]{plot}), maxRefreshRate, startImmediately); + this(Collections.singletonList(plot), maxRefreshRate, startImmediately); } /** @@ -121,7 +122,7 @@ public void run() { } } } - } catch(InterruptedException e) { + } catch (InterruptedException ignored) { } finally { Log.d(TAG, "Redrawer thread exited."); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java index b351358b..3a794b5c 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java @@ -16,11 +16,12 @@ package com.androidplot.xy; -import android.content.*; -import android.graphics.*; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Paint; -import com.androidplot.ui.*; -import com.androidplot.util.*; +import com.androidplot.ui.SeriesRenderer; +import com.androidplot.util.PixelUtils; /** * Format for drawing a value using {@link BubbleRenderer}. @@ -51,7 +52,7 @@ public class BubbleFormatter extends XYSeriesFormatter { setPointLabeler(new PointLabeler() { @Override public String getLabel(BubbleSeries series, int index) { - return series.getZ(index) + ""; + return String.valueOf(series.getZ(index)); } }); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java index 7aaa58a8..4e117252 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java @@ -16,12 +16,16 @@ package com.androidplot.xy; -import android.graphics.*; +import android.graphics.Canvas; +import android.graphics.PointF; +import android.graphics.RectF; + import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesRenderer; -import java.util.*; +import java.util.ArrayList; +import java.util.List; /** * A faster implementation of of {@link LineAndPointRenderer}. For performance reasons, has these constraints: @@ -53,11 +57,11 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte int segmentLen = 0; boolean isLastPointNull = true; + PointF resultPoint = new PointF(); for (int i = 0, j = 0; i < series.size(); i++, j+=2) { Number y = series.getY(i); Number x = series.getX(i); - PointF thisPoint; if (y != null && x != null) { if(isLastPointNull) { segmentOffsets.add(j); @@ -65,9 +69,9 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte isLastPointNull = false; } - thisPoint = getPlot().getBounds().transformScreen(x, y, plotArea); - points[j] = thisPoint.x; - points[j+1] = thisPoint.y; + getPlot().getBounds().transformScreen(resultPoint, x, y, plotArea); + points[j] = resultPoint.x; + points[j + 1] = resultPoint.y; segmentLen+=2; // if this is the last point, account for it in segment lengths: diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java index 240f58dc..03e328c9 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java @@ -1,8 +1,11 @@ package com.androidplot.xy; -import com.androidplot.util.*; +import android.support.annotation.NonNull; -import java.util.*; +import com.androidplot.util.FastNumber; + +import java.util.ArrayList; +import java.util.List; /** * An efficient implementation of {@link EditableXYSeries} intended for use cases where @@ -14,7 +17,9 @@ */ public class FixedSizeEditableXYSeries implements EditableXYSeries { + @NonNull private List xVals = new ArrayList<>(); + @NonNull private List yVals = new ArrayList<>(); private String title; @@ -24,12 +29,12 @@ public FixedSizeEditableXYSeries(String title, int size) { } @Override - public void setX(Number x, int index) { + public void setX(@NonNull Number x, int index) { xVals.set(index, new FastNumber(x)); } @Override - public void setY(Number y, int index) { + public void setY(@NonNull Number y, int index) { yVals.set(index, new FastNumber(y)); } @@ -44,7 +49,7 @@ public void resize(int size) { resize(yVals, size); } - protected void resize(List list, int size) { + protected void resize(@NonNull List list, int size) { if (size > list.size()) { while (list.size() < size) { list.add(null); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java index 5a1433f7..0bb8988d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java @@ -245,7 +245,7 @@ public Number getHeight() { * @param y * @return */ - private Number distanceBetween(Number x, Number y) { + private static Number distanceBetween(Number x, Number y) { return Math.abs(x.doubleValue() - y.doubleValue()); } 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 f346694a..4d3ebaac 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java @@ -17,10 +17,15 @@ package com.androidplot.xy; import android.graphics.Canvas; + import com.androidplot.Plot; import com.androidplot.PlotListener; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -82,10 +87,8 @@ public void onAfterDraw(Plot source, Canvas canvas) { } protected static List asNumberList(Number... model) { - List numbers = new ArrayList<>(); - for(Number n : model) { - numbers.add(n); - } + List numbers = new ArrayList<>(model.length); + Collections.addAll(numbers, model); return numbers; } 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 2ee06adf..705ae635 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java @@ -58,6 +58,11 @@ public boolean contains(Number x, Number y) { 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; @@ -157,18 +162,16 @@ public void setMaxY(Number maxY) { @Override public String toString() { - final StringBuffer sb = new StringBuffer("XYConstraints{"); - sb.append("domainFramingModel=").append(domainFramingModel); - sb.append(", rangeFramingModel=").append(rangeFramingModel); - sb.append(", domainUpperBoundaryMode=").append(domainUpperBoundaryMode); - sb.append(", domainLowerBoundaryMode=").append(domainLowerBoundaryMode); - sb.append(", rangeUpperBoundaryMode=").append(rangeUpperBoundaryMode); - sb.append(", rangeLowerBoundaryMode=").append(rangeLowerBoundaryMode); - sb.append(", minX=").append(minX); - sb.append(", maxX=").append(maxX); - sb.append(", minY=").append(minY); - sb.append(", maxY=").append(maxY); - sb.append('}'); - return sb.toString(); + return "XYConstraints{" + "domainFramingModel=" + domainFramingModel + + ", rangeFramingModel=" + rangeFramingModel + + ", domainUpperBoundaryMode=" + domainUpperBoundaryMode + + ", domainLowerBoundaryMode=" + domainLowerBoundaryMode + + ", rangeUpperBoundaryMode=" + rangeUpperBoundaryMode + + ", rangeLowerBoundaryMode=" + rangeLowerBoundaryMode + + ", minX=" + minX + + ", maxX=" + maxX + + ", minY=" + minY + + ", maxY=" + maxY + + '}'; } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java index 21c512f6..a6460799 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java @@ -16,19 +16,33 @@ package com.androidplot.xy; -import android.content.res.*; -import android.graphics.*; - -import com.androidplot.*; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; + +import com.androidplot.R; import com.androidplot.Region; import com.androidplot.exception.PlotRenderException; -import com.androidplot.ui.*; +import com.androidplot.ui.Insets; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.RenderStack; +import com.androidplot.ui.Size; import com.androidplot.ui.widget.Widget; -import com.androidplot.util.*; +import com.androidplot.util.AttrUtils; +import com.androidplot.util.FontUtils; +import com.androidplot.util.PixelUtils; +import com.androidplot.util.RectFUtils; import java.text.DecimalFormat; import java.text.Format; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; /** * Displays graphical data (lines, points, etc.) annotated with domain and range tick markers. @@ -121,14 +135,14 @@ public class XYGraphWidget extends Widget { /** * Set of edges for which line labels should be displayed */ - private Set lineLabelEdges = new HashSet<>(); + private EnumSet lineLabelEdges = EnumSet.noneOf(Edge.class); private RenderStack renderStack; private CursorLabelFormatter cursorLabelFormatter; - private HashMap lineLabelStyles = getDefaultLineLabelStyles(); - private HashMap lineLabelRenderers = getDefaultLineLabelRenderers(); + private Map lineLabelStyles = getDefaultLineLabelStyles(); + private Map lineLabelRenderers = getDefaultLineLabelRenderers(); public static class LineLabelRenderer { @@ -1051,8 +1065,8 @@ public void setLineExtensionRight(float lineExtensionRight) { this.lineExtensionRight = lineExtensionRight; } - protected HashMap getDefaultLineLabelStyles() { - HashMap defaults = new HashMap<>(); + protected Map getDefaultLineLabelStyles() { + EnumMap defaults = new EnumMap<>(Edge.class); defaults.put(Edge.TOP, new LineLabelStyle()); defaults.put(Edge.BOTTOM, new LineLabelStyle()); defaults.put(Edge.LEFT, new LineLabelStyle()); @@ -1060,8 +1074,8 @@ protected HashMap getDefaultLineLabelStyles() { return defaults; } - protected HashMap getDefaultLineLabelRenderers() { - HashMap defaults = new HashMap<>(); + protected Map getDefaultLineLabelRenderers() { + EnumMap defaults = new EnumMap<>(Edge.class); defaults.put(Edge.TOP, new LineLabelRenderer()); defaults.put(Edge.BOTTOM, new LineLabelRenderer()); defaults.put(Edge.LEFT, new LineLabelRenderer()); @@ -1145,17 +1159,15 @@ public boolean isLineLabelEnabled(Edge position) { } public void setLineLabelEdges(Edge... positions) { - Set positionSet = new HashSet<>(); + EnumSet positionSet = EnumSet.noneOf(Edge.class); if(positions != null) { - for(Edge position : positions) { - positionSet.add(position); - } + Collections.addAll(positionSet, positions); } - setLineLabelEdges(positionSet); + this.lineLabelEdges = positionSet; } - public void setLineLabelEdges(Set positions) { - this.lineLabelEdges = positions; + public void setLineLabelEdges(Collection positions) { + this.lineLabelEdges = EnumSet.copyOf(positions); } protected void setLineLabelEdges(int bitfield) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java index 33769886..9c131fa6 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -25,9 +25,15 @@ import android.support.annotation.NonNull; import android.util.AttributeSet; -import com.androidplot.*; -import com.androidplot.ui.*; +import com.androidplot.Plot; +import com.androidplot.R; +import com.androidplot.ui.Anchor; +import com.androidplot.ui.DynamicTableModel; +import com.androidplot.ui.HorizontalPositioning; +import com.androidplot.ui.Size; +import com.androidplot.ui.SizeMode; import com.androidplot.ui.TextOrientation; +import com.androidplot.ui.VerticalPositioning; import com.androidplot.ui.widget.TextLabelWidget; import com.androidplot.util.AttrUtils; import com.androidplot.util.PixelUtils; @@ -581,7 +587,7 @@ protected Number getCalculatedLowerBoundary(BoundaryMode mode, Number previousMi * @param min * @param max */ - private Number applyUserMinMax(Number value, Number min, Number max) { + private static Number applyUserMinMax(Number value, Number min, Number max) { value = (((min == null) || (value == null) || (value.doubleValue() > min.doubleValue())) ? value : min); @@ -678,7 +684,7 @@ protected Number[] getOriginMinMax(BoundaryMode mode, Number origin, Number exte * @param y * @return */ - private double distance(double x, double y) { + private static double distance(double x, double y) { if (x > y) { return x - y; } else { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java index 0c18d2e9..27924ab1 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java @@ -16,7 +16,7 @@ package com.androidplot.xy; -import android.content.*; +import android.content.Context; import com.androidplot.ui.Formatter; import com.androidplot.util.LayerHash; @@ -28,7 +28,7 @@ public abstract class XYSeriesFormatter Date: Thu, 1 Jun 2017 07:25:51 -0500 Subject: [PATCH 04/61] Bounds not calculated properly for FastXYSeries (#49) * uprev Gradle build tools 2.3.1 -> 2.3.2 * FastNumber's constructor is now private. Added safe static initializer; FastNumber.orNull which returns null if a null Number is passed into it. * #45 SeriesUtils now falls back to default min/max calculation on FastXYSeries whose bounds fall completely within the owning XYPlot's constraints. --- .../src/main/java/com/androidplot/Region.java | 6 +- .../java/com/androidplot/util/FastNumber.java | 19 +++++- .../com/androidplot/util/SeriesUtils.java | 23 +++---- .../java/com/androidplot/xy/FastXYSeries.java | 4 ++ .../xy/FixedSizeEditableXYSeries.java | 10 +-- .../com/androidplot/xy/XYConstraints.java | 64 ++++++++++------- .../main/java/com/androidplot/xy/XYPlot.java | 44 ++++++------ .../com/androidplot/util/FastNumberTest.java | 17 ++--- .../com/androidplot/util/SeriesUtilsTest.java | 68 +++++++++++++++++-- build.gradle | 2 +- demoapp-wearable/build.gradle | 2 +- 11 files changed, 172 insertions(+), 87 deletions(-) diff --git a/androidplot-core/src/main/java/com/androidplot/Region.java b/androidplot-core/src/main/java/com/androidplot/Region.java index 3f67bb09..f22f9115 100644 --- a/androidplot-core/src/main/java/com/androidplot/Region.java +++ b/androidplot-core/src/main/java/com/androidplot/Region.java @@ -71,7 +71,7 @@ public Number length() { Number l = getMax() == null || getMin() == null ? null : getMax().doubleValue() - getMin().doubleValue(); if(l != null) { - cachedLength = new FastNumber(l); + cachedLength = FastNumber.orNull(l); } } return cachedLength; @@ -216,7 +216,7 @@ public void setMin(Number min) { this.min = null; } } else if (this.min == null || !this.min.equals(min)) { - this.min = new FastNumber(min); + this.min = FastNumber.orNull(min); } } @@ -238,7 +238,7 @@ public void setMax(Number max) { this.max = null; } } else if (this.max == null || !this.max.equals(max)) { - this.max = new FastNumber(max); + this.max = FastNumber.orNull(max); } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java index 4b113b5d..72e8a25d 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java +++ b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java @@ -8,8 +8,7 @@ */ public class FastNumber extends Number { - @NonNull - private final Number number; + @NonNull private final Number number; private boolean hasDoublePrimitive; private boolean hasFloatPrimitive; private boolean hasIntPrimitive; @@ -18,7 +17,21 @@ public class FastNumber extends Number { private float floatPrimitive; private int intPrimitive; - public FastNumber(@NonNull Number number) { + /** + * Safe-instantiator of FastNumber; returns a null result if the input Number is also null. + * @param number + * @return + */ + public static FastNumber orNull(@NonNull Number number) { + if(number == null) { + return null; + } else { + return new FastNumber(number); + } + } + + private FastNumber(@NonNull Number number) { + //noinspection ConstantConditions //in case someone ignores the @NonNull annotation if (number == null) { throw new IllegalArgumentException("number parameter cannot be null"); diff --git a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java index e74a8f3b..bd265582 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java @@ -65,7 +65,6 @@ public static Region minMaxY(XYSeries... seriesList) { * @since 0.9.7 */ public static RectRegion minMax(XYConstraints constraints, List seriesList) { - // TODO: this is inefficient...clean it up! return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()])); } @@ -86,23 +85,17 @@ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArr for (XYSeries series : seriesArray) { // if this is an advanced xy series then minMax have already been calculated for us: - if(series instanceof FastXYSeries) { - final RectRegion seriesBounds = ((FastXYSeries) series).minMax(); - if (seriesBounds == null) { + boolean isPreCalculated = false; + if (series instanceof FastXYSeries) { + final RectRegion b = ((FastXYSeries) series).minMax(); + if(b == null) { continue; } - if(constraints == null) { - bounds.union(seriesBounds); - } else { - if (constraints.contains(seriesBounds.getMinX(), seriesBounds.getMinY())) { - bounds.union(seriesBounds.getMinX(), seriesBounds.getMinY()); - } - if (constraints.contains(seriesBounds.getMaxX(), seriesBounds.getMaxY())) { - bounds.union(seriesBounds.getMaxX(), seriesBounds.getMaxY()); - } + if(constraints == null || constraints.contains(b)) { + bounds.union(b); } - - } else if (series.size() > 0) { + } + if (!isPreCalculated && series.size() > 0) { for (int i = 0; i < series.size(); i++) { final Number xi = series.getX(i); final Number yi = series.getY(i); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java index 4faa4d54..de5d534b 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java @@ -3,6 +3,10 @@ /** * An implementation of {@link XYSeries} that defines additional methods to speed up rendering by * giving a hint to the renderer about the min/max values contained in the series. + * + * Note that these hints can only be leveraged if the containing XYPlot's constraints completely + * contain the FastXYSeries min/max values. If this condition is not met then XYPlot falls back + * to manually determining the min/max values of the series that exist within the defined constraints. */ public interface FastXYSeries extends XYSeries { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java index 03e328c9..0e6ea1a1 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java @@ -1,6 +1,7 @@ package com.androidplot.xy; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.androidplot.util.FastNumber; @@ -19,6 +20,7 @@ public class FixedSizeEditableXYSeries implements EditableXYSeries { @NonNull private List xVals = new ArrayList<>(); + @NonNull private List yVals = new ArrayList<>(); private String title; @@ -29,13 +31,13 @@ public FixedSizeEditableXYSeries(String title, int size) { } @Override - public void setX(@NonNull Number x, int index) { - xVals.set(index, new FastNumber(x)); + public void setX(@Nullable Number x, int index) { + xVals.set(index, FastNumber.orNull(x)); } @Override - public void setY(@NonNull Number y, int index) { - yVals.set(index, new FastNumber(y)); + public void setY(@Nullable Number y, int index) { + yVals.set(index, FastNumber.orNull(y)); } /** 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 705ae635..58338264 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java @@ -16,6 +16,9 @@ package com.androidplot.xy; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + /** * Calculates the min/max constraints for an xy plane. * @@ -45,13 +48,18 @@ 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) { // this is essentially an invisible point: @@ -80,86 +88,96 @@ public boolean contains(Number x, Number y) { 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 + diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java index 9c131fa6..15e8bbc2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -694,15 +694,15 @@ private static double distance(double x, double y) { public void updateDomainMinMaxForOriginModel() { double origin = userDomainOrigin.doubleValue(); - double maxXDelta = distance(bounds.getMaxX().doubleValue(), origin); - double minXDelta = distance(bounds.getMinX().doubleValue(), origin); - double delta = maxXDelta > minXDelta ? maxXDelta : minXDelta; - double dlb = origin - delta; - double dub = origin + delta; + double maxDelta = distance(bounds.getMaxX().doubleValue(), origin); + double minDelta = distance(bounds.getMinX().doubleValue(), origin); + double delta = maxDelta > minDelta ? maxDelta : minDelta; + double lowerBoundary = origin - delta; + double upperBoundary = origin + delta; switch (domainOriginBoundaryMode) { case AUTO: - bounds.setMinX(dlb); - bounds.setMaxX(dub); + bounds.setMinX(lowerBoundary); + bounds.setMaxX(upperBoundary); break; // if fixed, then the value already exists within "user" vals. @@ -710,28 +710,28 @@ public void updateDomainMinMaxForOriginModel() { break; case GROW: { - if (prevMinX == null || dlb < prevMinX.doubleValue()) { - bounds.setMinX(dlb); + if (prevMinX == null || lowerBoundary < prevMinX.doubleValue()) { + bounds.setMinX(lowerBoundary); } else { bounds.setMinX(prevMinX); } - if (prevMaxX == null || dub > prevMaxX.doubleValue()) { - bounds.setMaxX(dub); + if (prevMaxX == null || upperBoundary > prevMaxX.doubleValue()) { + bounds.setMaxX(upperBoundary); } else { bounds.setMaxX(prevMaxX); } } break; case SHRINK: - if (prevMinX == null || dlb > prevMinX.doubleValue()) { - bounds.setMinX(dlb); + if (prevMinX == null || lowerBoundary > prevMinX.doubleValue()) { + bounds.setMinX(lowerBoundary); } else { bounds.setMinX(prevMinX); } - if (prevMaxX == null || dub < prevMaxX.doubleValue()) { - bounds.setMaxX(dub); + if (prevMaxX == null || upperBoundary < prevMaxX.doubleValue()) { + bounds.setMaxX(upperBoundary); } else { bounds.setMaxX(prevMaxX); } @@ -745,14 +745,14 @@ public void updateRangeMinMaxForOriginModel() { switch (rangeOriginBoundaryMode) { case AUTO: double origin = userRangeOrigin.doubleValue(); - double maxYDelta = distance(bounds.getMaxY().doubleValue(), origin); - double minYDelta = distance(bounds.getMinY().doubleValue(), origin); - if (maxYDelta > minYDelta) { - bounds.setMinY(origin - maxYDelta); - bounds.setMaxY(origin + maxYDelta); + double maxDelta = distance(bounds.getMaxY().doubleValue(), origin); + double minDelta = distance(bounds.getMinY().doubleValue(), origin); + if (maxDelta > minDelta) { + bounds.setMinY(origin - maxDelta); + bounds.setMaxY(origin + maxDelta); } else { - bounds.setMinY(origin - minYDelta); - bounds.setMaxY(origin + minYDelta); + bounds.setMinY(origin - minDelta); + bounds.setMaxY(origin + minDelta); } break; case FIXED: diff --git a/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java b/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java index a029600a..8bca2a38 100644 --- a/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java +++ b/androidplot-core/src/test/java/com/androidplot/util/FastNumberTest.java @@ -9,6 +9,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @SuppressWarnings({"UnnecessaryBoxing", "ObjectEqualsNull", "EqualsBetweenInconvertibleTypes", "EqualsWithItself", "NumberEquality"}) @@ -84,16 +85,16 @@ public void tearDown() throws Exception { } - @Test(expected = IllegalArgumentException.class) - public void constructor_throwsException_ifNumberIsNull() { + @Test + public void orNull_returnsNull_ifNullNumber() { //noinspection ConstantConditions - new FastNumber(null); + assertNull(FastNumber.orNull(null)); } @Test public void equals_returnsTrue_ifFromSameNumberInstance() { for (Number number : NUMBERS) { - assertTrue("Equality test failed on " + number, new FastNumber(number).equals(new FastNumber(number))); + assertTrue("Equality test failed on " + number, FastNumber.orNull(number).equals(FastNumber.orNull(number))); } } @@ -102,7 +103,7 @@ public void equals_returnsTrue_ifFromSameNumber() { assertEquals("misconfigured test values", NUMBERS.length, NUMBERS_CLONE.length); for (int i = 0; i < NUMBERS.length; i++) { assertEquals("misconfigured test values", NUMBERS[i], NUMBERS_CLONE[i]); - assertTrue(new FastNumber(NUMBERS[i]).equals(new FastNumber(NUMBERS_CLONE[i]))); + assertTrue(FastNumber.orNull(NUMBERS[i]).equals(FastNumber.orNull(NUMBERS_CLONE[i]))); } } @@ -114,7 +115,7 @@ public void equals_returnsFalse_ifNumberIsDifferent() { continue; } assertNotEquals("duplicate test values at index " + i + " and " + j, NUMBERS[i], NUMBERS[j]); - assertFalse(new FastNumber(NUMBERS[i]).equals(new FastNumber(NUMBERS[j]))); + assertFalse(FastNumber.orNull(NUMBERS[i]).equals(FastNumber.orNull(NUMBERS[j]))); } } } @@ -123,8 +124,8 @@ public void equals_returnsFalse_ifNumberIsDifferent() { public void hashCode_isEqual_ifInstanceIsEqual() { for (Number number : NUMBERS) { for (Number number2 : NUMBERS) { - FastNumber fastNumber1 = new FastNumber(number); - FastNumber fastNumber2 = new FastNumber(number2); + FastNumber fastNumber1 = FastNumber.orNull(number); + FastNumber fastNumber2 = FastNumber.orNull(number2); if (fastNumber1.equals(fastNumber2)) { assertEquals(fastNumber1.hashCode(), fastNumber2.hashCode()); } diff --git a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java index 4ff129bc..df9014cd 100644 --- a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java +++ b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java @@ -51,7 +51,7 @@ public void tearDown() throws Exception { } @Test - public void testSeriesMinMax() { + public void minMax_onSimpleXYSeries_calculatesExpectedRegion() { SimpleXYSeries series = new SimpleXYSeries(LINEAR, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); RectRegion minMax = SeriesUtils.minMax(series); assertEquals(0, minMax.getMinX().doubleValue(), 0); @@ -103,14 +103,68 @@ public void testSeriesMinMax() { } @Test - public void minMax_usesSeriesMinMax_onFastXYSeries() { + public void minMax_onFastXYSeries_usesSeriesMinMax() { FastXYSeries series = mock(FastXYSeries.class); SeriesUtils.minMax(series); verify(series).minMax(); } @Test - public void testListMinMax() { + public void minMax_calculatesExpectedRegion_onFastXYSeriesWithLayoutConstraints() { + FastXYSeries series = new FastXYSeries() { + + // create a couple arrays of y-values to plot: + final ArrayList times = new ArrayList<>(); + final ArrayList values = new ArrayList<>(); + + { + for (int i = 0; i < 10; i++) { + times.add(i); + values.add(i); + } + } + + @Override + public int size() { + return times.size(); + } + + @Override + public Number getX(int index) { + return times.get(index); + } + + @Override + public Number getY(int index) { + return values.get(index); + } + + @Override + public String getTitle() { + return "Isaac's crazy thing"; + } + + @Override + public RectRegion minMax() { + return new RectRegion(0, 10, 0, 10); + } + }; + + final XYConstraints constraints = new XYConstraints(); + constraints.setDomainLowerBoundaryMode(BoundaryMode.FIXED); + constraints.setDomainUpperBoundaryMode(BoundaryMode.FIXED); + constraints.setMinX(5); + constraints.setMaxX(9); + + final RectRegion result = SeriesUtils.minMax(constraints, series); + assertEquals(5d, result.getMinX().doubleValue()); + assertEquals(9d, result.getMaxX().doubleValue()); + assertEquals(5d, result.getMinY().doubleValue()); + assertEquals(9d, result.getMaxY().doubleValue()); + } + + @Test + public void minMax_onSeriesList_producesAggregateResult() { Region minMax = SeriesUtils.minMax(LINEAR); assertEquals(1, minMax.getMin().doubleValue(), 0); assertEquals(8, minMax.getMax().doubleValue(), 0); @@ -141,7 +195,7 @@ public void testListMinMax() { } @Test - public void testGetNullRegion() { + public void getNullRegion_producesExpectedResult() { XYSeries s1 = new SimpleXYSeries( SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "s1", 0, 0, // 0 @@ -176,7 +230,7 @@ public void testGetNullRegion() { } @Test - public void testIboundsMin() { + public void iBoundsMin_findsMin() { XYSeries s1 = new SimpleXYSeries( Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), @@ -203,7 +257,7 @@ public void testIboundsMin() { } @Test - public void testIboundsMax() { + public void iBoundsMax_findsMax() { XYSeries s1 = new SimpleXYSeries( Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), @@ -230,7 +284,7 @@ public void testIboundsMax() { } @Test - public void testIbounds() { + public void iBounds_findsMinMax() { FastXYSeries series = mock(FastXYSeries.class); when(series.size()).thenReturn(3); when(series.getX(0)).thenReturn(0); diff --git a/build.gradle b/build.gradle index dd84178e..f6deec86 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.3.1' + classpath 'com.android.tools.build:gradle:2.3.2' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.5.0' diff --git a/demoapp-wearable/build.gradle b/demoapp-wearable/build.gradle index dd1d8926..37406aaa 100644 --- a/demoapp-wearable/build.gradle +++ b/demoapp-wearable/build.gradle @@ -19,7 +19,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.1' + classpath 'com.android.tools.build:gradle:2.3.2' } } apply plugin: 'com.android.application' From 28e5b312c1fc9d7b608472f8815cdee054e46008 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Wed, 12 Jul 2017 08:39:03 -0500 Subject: [PATCH 05/61] Pie legend widget (#54) Adds legend support to PieChart and refactors legend functionality into abstract class LegendWidget. Also updates docs / unit tests. --- .../java/com/androidplot/pie/PieChart.java | 45 +++- .../com/androidplot/pie/PieLegendItem.java | 26 ++ .../com/androidplot/pie/PieLegendWidget.java | 41 +++ .../java/com/androidplot/ui/RenderStack.java | 2 +- .../com/androidplot/ui/widget/LegendItem.java | 13 + .../ui/widget/LegendItemOrganizer.java | 9 + .../androidplot/ui/widget/LegendWidget.java | 197 +++++++++++++++ .../java/com/androidplot/xy/XYLegendItem.java | 28 ++ .../com/androidplot/xy/XYLegendWidget.java | 239 ++++-------------- .../main/java/com/androidplot/xy/XYPlot.java | 6 +- .../com/androidplot/ui/RenderStackTest.java | 52 ++++ .../androidplot/xy/XYLegendWidgetTest.java | 120 ++++++--- .../demos/SimplePieChartActivity.java | 29 +-- docs/legend.md | 59 +++++ docs/release_notes.md | 20 +- docs/xyplot.md | 41 +-- gradle/wrapper/gradle-wrapper.properties | 2 +- 17 files changed, 618 insertions(+), 311 deletions(-) create mode 100644 androidplot-core/src/main/java/com/androidplot/pie/PieLegendItem.java create mode 100644 androidplot-core/src/main/java/com/androidplot/pie/PieLegendWidget.java create mode 100644 androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java create mode 100644 androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java create mode 100644 androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java create mode 100644 androidplot-core/src/test/java/com/androidplot/ui/RenderStackTest.java create mode 100644 docs/legend.md diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java index 615efaaa..35e5e416 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java @@ -37,15 +37,18 @@ public class PieChart extends Plot { + + private PieChart pieChart; + + public PieLegendWidget(LayoutManager layoutManager, PieChart pieChart, + Size widgetSize, + TableModel tableModel, + Size iconSize) { + super(tableModel, layoutManager, widgetSize, iconSize); + this.pieChart = pieChart; + } + + @Override + protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull PieLegendItem item) { + canvas.drawRect(iconRect, item.formatter.getFillPaint()); + } + + @Override + protected List getLegendItems() { + final List legendItems = new ArrayList<>(); + for(SeriesBundle item : pieChart.getRegistry().getLegendEnabledItems()) { + legendItems.add(new PieLegendItem(item.getSeries(), item.getFormatter())); + } + return legendItems; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java index d9b9d812..99227563 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java @@ -23,7 +23,7 @@ import java.util.List; /** - * A stack of series to be rendered. The stack order is immutable but individual elements may be + * A stack of series to be rendered. The stack order is immutable but individual elements may be * manipulated via the public methods of {@link RenderStack.StackElement}. */ public class RenderStack { diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java new file mode 100644 index 00000000..5bb428ac --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java @@ -0,0 +1,13 @@ +package com.androidplot.ui.widget; + +/** + * An item to be displayed by {@link LegendWidget}. + */ +public interface LegendItem { + + /** + * + * @return The user facing label for this item. + */ + String getTitle(); +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java new file mode 100644 index 00000000..536ee9d8 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java @@ -0,0 +1,9 @@ +package com.androidplot.ui.widget; + +import java.util.List; + + +public interface LegendItemOrganizer { + + void organize(List items); +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java new file mode 100644 index 00000000..69e41407 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java @@ -0,0 +1,197 @@ +package com.androidplot.ui.widget; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.androidplot.exception.PlotRenderException; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.Size; +import com.androidplot.ui.TableModel; +import com.androidplot.util.FontUtils; +import com.androidplot.util.PixelUtils; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +/** + * Provides core functionality for displaying a legend widget within a {@link com.androidplot.Plot}. + * @param + */ +public abstract class LegendWidget extends Widget { + + private static final float DEFAULT_TEXT_SIZE_DP = 20; + + private TableModel tableModel; + private Size iconSize; + + private Paint textPaint; + private Paint iconBackgroundPaint; + private Paint iconBorderPaint; + + private boolean drawIconBackgroundEnabled = true; + private boolean drawIconBorderEnabled = true; + + private Comparator legendItemComparator; + + { + textPaint = new Paint(); + textPaint.setColor(Color.LTGRAY); + textPaint.setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_DP)); + textPaint.setAntiAlias(true); + + iconBackgroundPaint = new Paint(); + iconBackgroundPaint.setColor(Color.BLACK); + + iconBorderPaint = new Paint(); + iconBorderPaint.setColor(Color.TRANSPARENT); + iconBorderPaint.setStyle(Paint.Style.STROKE); + } + + + public LegendWidget(@NonNull TableModel tableModel, @NonNull LayoutManager layoutManager, + @NonNull Size size, @NonNull Size iconSize) { + super(layoutManager, size); + setTableModel(tableModel); + this.iconSize = iconSize; + } + + @Override + protected void doOnDraw(Canvas canvas, RectF widgetRect) throws PlotRenderException { + final List items = getLegendItems(); + if(legendItemComparator != null) { + Collections.sort(items, legendItemComparator); + } + final Iterator cellRectIterator = tableModel.getIterator(widgetRect, items.size()); + for(ItemT item : items) { + final RectF cellRect = cellRectIterator.next(); + final RectF iconRect = getIconRect(cellRect); + beginDrawingCell(canvas, iconRect); + drawItem(canvas, iconRect, item); + finishDrawingCell(canvas, cellRect, iconRect, item); + } + } + + protected void drawItem(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item) { + drawIcon(canvas, iconRect, item); + } + + /** + * Draw the icon representing the legend item + * @param canvas + * @param iconRect The space to be occupied by the icon. + * @param item + */ + protected abstract void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item); + + /** + * + * @return The list of legend items to be drawn. This is used to calculate table dimensions etc. + */ + protected abstract List getLegendItems(); + + private RectF getIconRect(RectF cellRect) { + float cellRectCenterY = cellRect.top + (cellRect.height()/2); + RectF iconRect = iconSize.getRectF(cellRect); + + // center the icon rect vertically + float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2); + iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY); + return iconRect; + } + + /** + * Done at the start of rendering a new cell. Whatever is drawn here will be beneath the rest + * of the cell content; typically used to draw backgrounds. + * @param canvas + * @param iconRect + */ + protected void beginDrawingCell(Canvas canvas, RectF iconRect) { + + if(drawIconBackgroundEnabled && iconBackgroundPaint != null) { + canvas.drawRect(iconRect, iconBackgroundPaint); + } + } + + /** + * Done at the end of rendering a new cell. Whatever is drawn here will be on top of + * the rest of the cell content; typically used to draw borders and text. + * @param canvas + * @param cellRect + * @param iconRect + * @param legendItem + */ + protected void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, LegendItem legendItem) { + + if(drawIconBorderEnabled && iconBorderPaint != null) { + canvas.drawRect(iconRect, iconBorderPaint); + } + + float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2); + + if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) { + canvas.drawText(legendItem.getTitle(), iconRect.left - 2, centeredTextOriginY, textPaint); + } else { + canvas.drawText(legendItem.getTitle(), iconRect.right + 2, centeredTextOriginY, textPaint); + } + } + + protected static float getRectCenterY(RectF cellRect) { + return cellRect.top + (cellRect.height()/2); + } + + public synchronized void setTableModel(TableModel tableModel) { + this.tableModel = tableModel; + } + + public Paint getTextPaint() { + return textPaint; + } + + public void setTextPaint(Paint textPaint) { + this.textPaint = textPaint; + } + + public boolean isDrawIconBackgroundEnabled() { + return drawIconBackgroundEnabled; + } + + public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) { + this.drawIconBackgroundEnabled = drawIconBackgroundEnabled; + } + + public boolean isDrawIconBorderEnabled() { + return drawIconBorderEnabled; + } + + public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) { + this.drawIconBorderEnabled = drawIconBorderEnabled; + } + + public Size getIconSize() { + return iconSize; + } + + public void setIconSize(Size iconSize) { + this.iconSize = iconSize; + } + + public Comparator getLegendItemComparator() { + return legendItemComparator; + } + + /** + * Set a scheme for sorting the display order or legend items. By default no sorting is applied + * and {@link com.androidplot.Series} items typically appear in the order which the series was + * added to the {@link com.androidplot.Plot}. + * @param legendItemComparator + */ + public void setLegendItemComparator(Comparator legendItemComparator) { + this.legendItemComparator = legendItemComparator; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java new file mode 100644 index 00000000..1fd10ebd --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java @@ -0,0 +1,28 @@ +package com.androidplot.xy; + +import android.support.annotation.NonNull; + +import com.androidplot.ui.widget.LegendItem; + +public class XYLegendItem implements LegendItem { + + public enum Type { + SERIES, + REGION + } + + public final Type type; + public final Object item; + private final String text; + + public XYLegendItem(@NonNull Type cellType, @NonNull Object item, @NonNull String text) { + this.type = cellType; + this.item = item; + this.text = text; + } + + @Override + public String getTitle() { + return this.text; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java index e5b981c5..f08783cb 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java @@ -17,233 +17,80 @@ package com.androidplot.xy; import android.graphics.*; +import android.support.annotation.NonNull; + import com.androidplot.ui.LayoutManager; import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.Size; import com.androidplot.ui.TableModel; -import com.androidplot.ui.widget.Widget; -import com.androidplot.util.FontUtils; +import com.androidplot.ui.widget.LegendWidget; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Hashtable; +import java.util.List; +import java.util.Map.Entry; /** * Displays a legend for each series added to the owning {@link XYPlot}. */ -public class XYLegendWidget extends Widget { - - /** - * This class is of no use outside of XYLegendWidget. It's just used to alphabetically sort - * Region legend entries. - */ - private static class RegionEntryComparator implements Comparator> { - @Override - public int compare(Map.Entry o1, Map.Entry o2) { - return o1.getValue().compareTo(o2.getValue()); - } - } - - private enum CellType { - SERIES, - REGION - } +public class XYLegendWidget extends LegendWidget { private XYPlot plot; - //private float iconWidth = 12; - private Paint textPaint; - private Paint iconBorderPaint; - private TableModel tableModel; - private boolean drawIconBackgroundEnabled = true; - private boolean drawIconBorderEnabled = true; - - private Size iconSize; - private static final RegionEntryComparator regionEntryComparator = new RegionEntryComparator(); - //private RectF iconRect = new RectF(0, 0, ICON_WIDTH_DEFAULT, ICON_HEIGHT_DEFAULT); - - { - textPaint = new Paint(); - textPaint.setColor(Color.LTGRAY); - textPaint.setAntiAlias(true); - - iconBorderPaint = new Paint(); - iconBorderPaint.setStyle(Paint.Style.STROKE); - //regionEntryComparator = new RegionEntryComparator(); - } public XYLegendWidget(LayoutManager layoutManager, XYPlot plot, Size widgetSize, TableModel tableModel, Size iconSize) { - super(layoutManager, widgetSize); + super(tableModel, layoutManager, widgetSize, iconSize); this.plot = plot; - setTableModel(tableModel); - this.iconSize = iconSize; - } - - public synchronized void setTableModel(TableModel tableModel) { - this.tableModel = tableModel; - } - private RectF getIconRect(RectF cellRect) { - float cellRectCenterY = cellRect.top + (cellRect.height()/2); - RectF iconRect = iconSize.getRectF(cellRect); - - // center the icon rect vertically - float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2); - iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY); - return iconRect; - } - - private static float getRectCenterY(RectF cellRect) { - return cellRect.top + (cellRect.height()/2); - } - - private void beginDrawingCell(Canvas canvas, RectF iconRect) { - - Paint bgPaint = plot.getGraph().getGridBackgroundPaint(); - if(drawIconBackgroundEnabled && bgPaint != null) { - canvas.drawRect(iconRect, bgPaint); - } - } - - private void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, String text) { - - Paint bgPaint = plot.getGraph().getGridBackgroundPaint(); - if(drawIconBorderEnabled && bgPaint != null) { - iconBorderPaint.setColor(bgPaint.getColor()); - canvas.drawRect(iconRect, iconBorderPaint); - } - - float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2); - - if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) { - canvas.drawText(text, iconRect.left - 2, centeredTextOriginY, textPaint); - } else { - canvas.drawText(text, iconRect.right + 2, centeredTextOriginY, textPaint); - } + // Set a default comparator that sorts by type and then alphabetically + setLegendItemComparator(new Comparator() { + @Override + public int compare(XYLegendItem o1, XYLegendItem o2) { + if(o1.type == o2.type) { + return o1.getTitle().compareTo(o2.getTitle()); + } else { + return(o1.type.compareTo(o2.type)); + } + } + }); } protected void drawRegionLegendIcon(Canvas canvas, RectF rect, XYRegionFormatter formatter) { - canvas.drawRect(rect, formatter.getPaint()); - } - - private void drawRegionLegendCell(Canvas canvas, XYRegionFormatter formatter, RectF cellRect, String text) { - RectF iconRect = getIconRect(cellRect); - beginDrawingCell(canvas, iconRect); - - drawRegionLegendIcon( - canvas, - iconRect, - formatter - ); - finishDrawingCell(canvas, cellRect, iconRect, text); + canvas.drawRect(rect, formatter.getPaint()); } - private void drawSeriesLegendCell(Canvas canvas, XYSeriesRenderer renderer, XYSeriesFormatter formatter, RectF cellRect, String seriesTitle) { - RectF iconRect = getIconRect(cellRect); - beginDrawingCell(canvas, iconRect); - - renderer.drawSeriesLegendIcon( - canvas, - iconRect, - formatter); - finishDrawingCell(canvas, cellRect, iconRect, seriesTitle); + @Override + protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull XYLegendItem XYLegendItem) { + switch (XYLegendItem.type) { + case REGION: + drawRegionLegendIcon(canvas, iconRect, (XYRegionFormatter) XYLegendItem.item); + break; + case SERIES: + final XYSeriesFormatter formatter = (XYSeriesFormatter) XYLegendItem.item; + plot.getRenderer(formatter.getRendererClass()).drawSeriesLegendIcon(canvas, iconRect, formatter); + break; + default: + throw new UnsupportedOperationException("Unexpected item type: " + XYLegendItem.type); + } } -// protected List> getLegendEnabledSeriesAndFormatterList() { -// List> sfList = new ArrayList<>(); -// ListIterator> it = plot.getSeriesRegistry().listIterator(); -// while(it.hasNext()) { -// SeriesAndFormatter thisSf = it.next(); -// if(thisSf.getFormatter().isLegendIconEnabled()) { -// sfList.add(thisSf); -// } -// } -// return sfList; -// } - @Override - protected synchronized void doOnDraw(Canvas canvas, RectF widgetRect) { - if(plot.isEmpty()) { - return; + protected List getLegendItems() { + final ArrayList items = new ArrayList<>(); + for (SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) { + items.add(new XYLegendItem(XYLegendItem.Type.SERIES, sfPair.getFormatter(), sfPair.getSeries().getTitle())); } - // Keep an alphabetically sorted list of regions: - TreeSet> sortedRegions = new TreeSet>(new RegionEntryComparator()); - - // Calculate the number of cells needed to draw the Legend: - int seriesCount = plot.getRegistry().size(); - - for(XYSeriesRenderer renderer : plot.getRendererList()) { + for (XYSeriesRenderer renderer : plot.getRendererList()) { Hashtable urf = renderer.getUniqueRegionFormatters(); - sortedRegions.addAll(urf.entrySet()); - } - - seriesCount += sortedRegions.size(); - - // Create an iterator specially created to draw the number of cells we calculated: - Iterator it = tableModel.getIterator(widgetRect, seriesCount); - - RectF cellRect; - - // draw each series legend item: - for(SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) { - //for(SeriesAndFormatter sfPair : plot.getSeriesRegistry()) { - cellRect = it.next(); - XYSeriesFormatter format = sfPair.getFormatter(); - drawSeriesLegendCell(canvas, plot.getRenderer(sfPair.getFormatter().getRendererClass()), - format, cellRect, sfPair.getSeries().getTitle()); - } - - // draw each region legend item: - for(Map.Entry entry : sortedRegions) { - if(!it.hasNext()) { - break; + for (Entry entry : urf.entrySet()) { + items.add(new XYLegendItem(XYLegendItem.Type.REGION, entry.getKey(), entry.getValue())); } - cellRect = it.next(); - XYRegionFormatter formatter = entry.getKey(); - drawRegionLegendCell(canvas, formatter, cellRect, entry.getValue()); } - } - - - public Paint getTextPaint() { - return textPaint; - } - - public void setTextPaint(Paint textPaint) { - this.textPaint = textPaint; - } - - public boolean isDrawIconBackgroundEnabled() { - return drawIconBackgroundEnabled; - } - - public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) { - this.drawIconBackgroundEnabled = drawIconBackgroundEnabled; - } - - public boolean isDrawIconBorderEnabled() { - return drawIconBorderEnabled; - } - - public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) { - this.drawIconBorderEnabled = drawIconBorderEnabled; - } - - public TableModel getTableModel() { - return tableModel; - } - - public Size getIconSize() { - return iconSize; - } - /** - * Set the size of each legend's icon. Note that when using relative sizing, - * the size is calculated against the countaining cell's size, not the plot's size. - * @param iconSize - */ - public void setIconSize(Size iconSize) { - this.iconSize = iconSize; + return items; } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java index 15e8bbc2..67eaafa8 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -48,9 +48,6 @@ */ public class XYPlot extends Plot { - private static final int DEFAULT_LEGEND_WIDGET_H_DP = 10; - private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 7; - private static final int DEFAULT_GRAPH_WIDGET_H_DP = 18; private static final int DEFAULT_GRAPH_WIDGET_W_DP = 10; @@ -60,8 +57,11 @@ public class XYPlot extends Plot renderStack = new RenderStack<>(plot); + + renderStack.sync(); + assertEquals(2, renderStack.getElements().size()); + for(RenderStack.StackElement element : renderStack.getElements()) { + assertTrue(element.isEnabled()); + } + + renderStack.disable(LineAndPointRenderer.class); + assertEquals(2, renderStack.getElements().size()); + for(RenderStack.StackElement element : renderStack.getElements()) { + assertFalse(element.isEnabled()); + } + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java index bbb2bec5..b50f7237 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java @@ -17,61 +17,101 @@ package com.androidplot.xy; import android.graphics.*; -import com.androidplot.Plot; import com.androidplot.test.AndroidplotTest; -import org.junit.After; +import com.androidplot.ui.DynamicTableModel; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.Size; +import com.androidplot.ui.SizeMode; +import com.google.common.collect.Lists; + +import org.junit.Before; import org.junit.Test; -import org.robolectric.RuntimeEnvironment; -import java.util.Arrays; -import static junit.framework.Assert.assertEquals; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class XYLegendWidgetTest extends AndroidplotTest { - static class MockXYPlot extends XYPlot { + @Mock LayoutManager layoutManager; + @Mock XYPlot xyPlot; + @Mock Canvas canvas; + @Mock XYRegionFormatter xyRegionFormatter; + LineAndPointRenderer lineAndPointRenderer; - public MockXYPlot() { - super(RuntimeEnvironment.application, "Test", - Plot.RenderMode.USE_MAIN_THREAD); - } + Size widgetSize = new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE); + Size iconSize = new Size(10, SizeMode.ABSOLUTE, 10, SizeMode.ABSOLUTE); + XYSeriesRegistry seriesRegistry; - public void exposedOnSizeChanged(int w, int h, int oldw, int oldh) { - this.onSizeChanged(w, h, oldw, oldh); - } + XYLegendWidget legendWidget; - public void exposedOnDraw(Canvas canvas) { - this.onDraw(canvas); - } - } + @Before + public void before() { + seriesRegistry = new XYSeriesRegistry(); + legendWidget = spy(new XYLegendWidget(layoutManager, xyPlot, widgetSize, + new DynamicTableModel(4, 4), iconSize)); - @After - public void tearDown() throws Exception {} + lineAndPointRenderer = new LineAndPointRenderer(xyPlot); - @Test - public void testDoOnDraw() throws Exception { - MockXYPlot plot = new MockXYPlot(); + when(xyPlot.getRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getRendererList()).thenReturn(Lists.newArrayList(lineAndPointRenderer)); + when(xyPlot.getRenderer(any(Class.class))).thenReturn(lineAndPointRenderer); + } - SimpleXYSeries s1 = new SimpleXYSeries((Arrays.asList(1, 2, 3)), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1"); + @Test + public void draw_drawsLegendIcons_forEnabledItemsOnly() throws Exception { + final XYSeries s1 = mock(XYSeries.class); + final XYSeriesFormatter f1 = new LineAndPointFormatter(); + f1.setLegendIconEnabled(true); + + final XYSeries s2 = mock(XYSeries.class); + final XYSeriesFormatter f2 = new LineAndPointFormatter(); + f2.setLegendIconEnabled(false); + + final RectRegion r1 = new RectRegion(0, 0, 10, 10, "r1"); + final RectRegion r2 = new RectRegion(0, 0, 20, 20, "r2"); + f1.addRegion(r1, new XYRegionFormatter(0)); + f2.addRegion(r2, new XYRegionFormatter(0)); + + seriesRegistry.add(s1, f1); + seriesRegistry.add(s2, f2); + legendWidget.draw(canvas); + + verify(legendWidget, times(2)) + .drawRegionLegendIcon(any(Canvas.class), any(RectF.class), any(XYRegionFormatter.class)); + verify(legendWidget, times(3)) + .drawIcon(any(Canvas.class), any(RectF.class), any(XYLegendItem.class)); + } - plot.addSeries(s1, new LineAndPointFormatter( - Color.RED, Color.GREEN, Color.BLUE, null)); + @Test + public void draw_sortsItemsAlphabeticallyByTitle() throws Exception{ + final XYLegendItem i1 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "zoo"); + final XYLegendItem i2 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "apple"); + final XYLegendItem i3 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "boo"); - assertEquals(1, plot.getRegistry().size()); + final List legendItems = Lists.newArrayList(i1, i2, i3); + doReturn(legendItems).when(legendWidget).getLegendItems(); - plot.exposedOnSizeChanged(100, 100, 100, 100); - plot.redraw(); - // have to manually invoke this because the invalidate() - // invoked by redraw() is a stub and will not result in onDraw being called. - plot.exposedOnDraw(new Canvas()); + legendWidget.draw(canvas); - plot.removeSeries(s1); - assertEquals(0, plot.getRegistry().size()); - plot.addSeries(s1, new BarFormatter(Color.RED, Color.GREEN)); - plot.redraw(); + InOrder inOrder = Mockito.inOrder(legendWidget); - // throws NullPointerException before fix - // for ANDROIDPLOT-166 was applied. - plot.exposedOnDraw(new Canvas()); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i2)); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i3)); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i1)); } - } diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java index 03b5b12b..9484176f 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java @@ -62,6 +62,9 @@ public void onCreate(Bundle savedInstanceState) // initialize our XYPlot reference: pie = (PieChart) findViewById(R.id.mySimplePieChart); + // enable the legend: + pie.getLegend().setVisible(true); + final float padding = PixelUtils.dpToPix(30); pie.getPie().setPadding(padding, padding, padding, padding); @@ -183,36 +186,10 @@ protected void setupIntroAnimation() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float scale = valueAnimator.getAnimatedFraction(); -// scalingSeries1.setScale(scale); -// scalingSeries2.setScale(scale); renderer.setExtentDegs(360 * scale); pie.redraw(); } }); -// animator.addListener(new Animator.AnimatorListener() { -// @Override -// public void onAnimationStart(Animator animator) { -// -// } -// -// @Override -// public void onAnimationEnd(Animator animator) { -// // the animation is over, so show point labels: -// series1Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); -// series2Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); -// plot.redraw(); -// } -// -// @Override -// public void onAnimationCancel(Animator animator) { -// -// } -// -// @Override -// public void onAnimationRepeat(Animator animator) { -// -// } -// }); // the animation will run for 1.5 seconds: animator.setDuration(1500); diff --git a/docs/legend.md b/docs/legend.md new file mode 100644 index 00000000..37c2d136 --- /dev/null +++ b/docs/legend.md @@ -0,0 +1,59 @@ +# The Legend +For `Plot` types that support it, the legend displays a list of elements in the plot along with +a color coded icon. The color coded icon is automatically generated using the colors and line styles +used to render the associated item. In the case of a `Series`, this is the `Formatter` you associated +with the `Series` when you added it to your `Plot`. + +# Showing / Hiding the Legend +Depending on the `Plot` type(s) you are using, the legend may or may not be visible by default. To +can enable / disable the legend: + +```java +plot.getLegend().setVisible(true|false); +``` + +# Hiding Series Items +You can tell Androidplot not to generate a legend item for a Series by configuring it's associated +`Formatter`: + +```java +formatter.setLegendIconEnabled(false); +``` + +## The TableModel +The `TableModel` controls how and where each item in the legend is drawn. Androidplot provides two +default implementations; `DynamicTableModel` and `FixedTableModel` (detailed below). All `TableModel` implementations +organize elements into a grid. This grid is populated with items based on the order which it's corresponding +series was added to the plot. This ordering can be further controlled by setting the `TableModel`'s +`TableOrder` param to either [ROW_MAJOR](https://en.wikipedia.org/wiki/Row-major_order) (items are added left-to-right, top-down) +or `COLUMN_MAJOR` (items are added top-down, left-to-right). + +### DynamicTableModel +The `DynamicTableModel` takes a desired of numbered rows and columns and evenly subdivides the `LegendWidget`'s +visible space into cells. For example, A 2x2 legend using `ROW_MAJOR` ordering: + +```java +plot.getLegend().setTableModel(new DynamicTableModel(2, 2, TableOrder.ROW_MAJOR)); +``` + +### FixedTableModel +The `FixedTableModel` takes a desired size of each cell in pixels and adds cells using the specified `TableOrder`. +It automatically wraps to the next row or column (based on `TableOrder`) when the cell being added +exceeds the legend's available space on a given axis. For example, A `FixedTableModel` using 300w*100h cells and +a TableOrder of `COLUMN_MAJOR`: + +```java +plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300), + PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR)); +``` + +# Sorting Legend Entries +You can control the order of Legend entries by setting a custom `Comparator` on the legend: + +```java +Comparator<...> myComparator = ... +plot.getLegend().setLegendItemComparator(myComparator); +``` + +Using a custom `Comparator` in conjunction with `ROW_MAJOR` and `COLUMN_MAJOR` properties on the `TableModel` +(show above) gives you full control over the display ordering of your legend entries. \ No newline at end of file diff --git a/docs/release_notes.md b/docs/release_notes.md index d9a0ef4a..7ea22016 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -2,17 +2,32 @@ For details on what to expect in general when updating to a new version of Androiplot, check out the [versioning doc](versioning.md). +# 1.5.0 + +_Updates to legend functionality in this version may result in changes to the display order +of legend items in some cases. A custom `Comparator` can be used to resolve this if necessary; +see the [legend doc](legend.md) for implementation details._ + +* Added [legend doc](legend.md) +* Added legend support to `PieChart` +* Added configurable legend item sorting +* (#45) Auto range boundaries calculation fix for when using a fixed domain range and a `FastXYSeries` +* Minor Performance Optimizations + # 1.4.3 + * (#39) `FastLineAndPointRenderer` now renders vertices for legend items. * Added [XML Attrs reference doc](attrs.md). # 1.4.2 + * (#32) New step mode: `INCREMENT_BY_FIT`. -* (#33) PanZoom support for 'INCREMENT_BY_FIT'. +* (#33) `PanZoom` support for `INCREMENT_BY_FIT`. * (#34) Removed examples and documentation for serializing `SeriesRegistry` to preserve state. # 1.4.1 -* (#26) Fixed an NPE issue when drawing null values with a PointLabeler. + +* (#26) Fixed an NPE issue when drawing null values with a `PointLabeler`. * Fixed a broken link in Quickstart doc. # 1.4.0 @@ -92,6 +107,7 @@ See the [pie chart documentation](piechart.md) for usage details. * Removed InteractiveXYPlot as PanZoom makes it obsolete. # 1.0.0 + This is a factor of several core elements of the Androidplot lib. The general theme was to make class and method names more intuitive and to make xml styling more powerful. diff --git a/docs/xyplot.md b/docs/xyplot.md index 1e363d12..2dab08db 100644 --- a/docs/xyplot.md +++ b/docs/xyplot.md @@ -292,44 +292,9 @@ See the [candlestick documentation](candlestick.md) Smooth lines can be created by applying the [Catmull-Rom interpolator](http://androidplot.com/smooth-curves-and-androidplot/) to your series' Format. -# The Legend -By default, Androidplot will automatically produce a legend for your Plot. You however choose to hide the legend -or you can customize it to suit your needs. - -# Hiding Legend Items -As mentioned above, Androidplot automatically produces a legend for your Plot. This "auto legend" includes -items for each series added to the plot. If you wish to omit a series from the legend: - -```java -formatter.setLegendIconEnabled(false); -``` - -## The TableModel -The `TableModel` controls how and where each item in the legend is drawn. Androidplot provides two -default implementations; `DynamicTableModel` and `FixedTableModel` (detailed below). All `TableModel` implementations -organize elements into a grid. This grid is populated with items based on the order which it's corresponding -series was added to the plot. This ordering can be further controlled by setting the `TableModel`'s -`TableOrder` param to either [ROW_MAJOR](https://en.wikipedia.org/wiki/Row-major_order) (items are added left-to-right, top-down) -or `COLUMN_MAJOR` (items are added top-down, left-to-right). - -### DynamicTableModel -The `DynamicTableModel` takes a desired of numbered rows and columns and evenly subdivides the `LegendWidget`'s -visible space into cells. For example, A 2x2 legend using `ROW_MAJOR` ordering: - -```java -plot.getLegend().setTableModel(new DynamicTableModel(2, 2, TableOrder.ROW_MAJOR)); -``` - -### FixedTableModel -The `FixedTableModel` takes a desired size of each cell in pixels and adds cells using the specified `TableOrder`. -It automatically wraps to the next row or column (based on `TableOrder`) when the cell being added -exceeds the legend's available space on a given axis. For example, A `FixedTableModel` using 300w*100h cells and -a TableOrder of `COLUMN_MAJOR`: - -```java -plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300), - PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR)); -``` +# The Legend +By default, Androidplot will automatically produce a legend for your `XYPlot`. See [the legend](legend.md) doc +for usage details. # Graph Rotation Androidplot provides the `Widget.setRotation(Widget.Rotation)` method for controlling the orientation diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0429c501..12d6c54e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip From e58d4dadd428ce81b580c3df4938014036529eb7 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sun, 28 May 2017 08:57:26 -0500 Subject: [PATCH 06/61] Uprev to 1.5.0 --- build.gradle | 2 +- docs/quickstart.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f6deec86..f7124dd4 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 25 theTargetSdkVersion = 25 theMinSdkVersion = 5 - theVersionName = '1.4.4' + theVersionName = '1.5.0' theVersionCode = 0 } diff --git a/docs/quickstart.md b/docs/quickstart.md index 4c629126..76060cd9 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -14,7 +14,7 @@ To use the library in your gradle project add the following to your build.gradle ```groovy dependencies { - compile "com.androidplot:androidplot-core:1.4.3" + compile "com.androidplot:androidplot-core:1.5.0" } ``` From a124e850eb31bdfd5447223bbe008c7cebe8b056 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Wed, 12 Jul 2017 08:14:15 -0500 Subject: [PATCH 07/61] Documentation updates --- docs/index.md | 1 + docs/legend.md | 8 ++++++-- docs/plot_composition.md | 26 +++++++++++++------------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/index.md b/docs/index.md index c42b6b06..abb8a729 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,7 @@ specific plot types explaining styling and other advanced topics. * [Quickstart](quickstart.md) :star: * [Quickstart (YouTube Video)](https://www.youtube.com/watch?v=wEFkzQY_wWI) :movie_camera: * [Plot Composition](plot_composition.md) +* [The Legend](legend.md) * [XY Plots](xyplot.md) * [Bar Charts](barchart.md) * [Candlestick Charts](candlestick.md) diff --git a/docs/legend.md b/docs/legend.md index 37c2d136..c3ea90e4 100644 --- a/docs/legend.md +++ b/docs/legend.md @@ -13,7 +13,7 @@ plot.getLegend().setVisible(true|false); ``` # Hiding Series Items -You can tell Androidplot not to generate a legend item for a Series by configuring it's associated +You can tell Androidplot not to generate a legend item for a `Series` by configuring it's associated `Formatter`: ```java @@ -56,4 +56,8 @@ plot.getLegend().setLegendItemComparator(myComparator); ``` Using a custom `Comparator` in conjunction with `ROW_MAJOR` and `COLUMN_MAJOR` properties on the `TableModel` -(show above) gives you full control over the display ordering of your legend entries. \ No newline at end of file +(show above) gives you full control over the display ordering of your legend entries. + +# Positioning and Resizing +The legend is just an implementation of a Widget and is positioned and resized in the same ways +that all Widget instances are positioned. See the [Plot Composition](plot_composition.md) doc for details. \ No newline at end of file diff --git a/docs/plot_composition.md b/docs/plot_composition.md index 20b80952..3252b9f5 100644 --- a/docs/plot_composition.md +++ b/docs/plot_composition.md @@ -1,26 +1,26 @@ # Plot Composition -All plots in Androidplot inherit from the abstract base class Plot which provides common behaviors -for all Plot implementations. +All plots in Androidplot inherit from the abstract base class `Plot` which provides common behaviors +for all `Plot` implementations. # Widgets -Plots are composed of one or more Widgets. A Widget is an abstraction of a visual +Plots are composed of one or more Widgets. A `Widget` is an abstraction of a visual component that may be positioned and scaled within the visible area of a Plot. For example, -an XY Plot is typically composed of these 5 Widgets: +an `XYPlot` is typically composed of these 5 `Widgets`: * Title * Graph * Domain Label * Range Label -* Legend +* [Legend](legend.md) -All Plot implementations will contain at least one default Widget providing the core -behavior encapsulated by that Plot. In addition to moving and scaling these Widgets, developers may -also extend them and replace the Plot's default instance with the derived implenentation in order to +All implementations of `Plot` will contain at least one default `Widget` providing the core +behavior encapsulated by that `Plot`. In addition to moving and scaling a `Widget`, developers may +also extend them and replace the `Plot` instance's default instance with the derived implementation in order to get custom behavior. # The LayoutManager -The LayoutManager provides the logic for visually positioning and scaling Widgets; all Plot implementations -contain an instance of LayoutManager that can be retrieved via `Plot.getLayoutManager()`. +The `LayoutManager` provides the logic for visually positioning and scaling Widgets; all `Plot` implementations +contain an instance of `LayoutManager` that can be retrieved via `Plot.getLayoutManager()`. ## Z-Indexing Z-indexing is a 2D drawing concept which associates each drawable entity with a value that determines @@ -28,12 +28,12 @@ which elements get drawn onto the screen first, producing the visual effect that on top of others. While Androidplot uses the term "z-index" it's implemented internally as a linked list to prevent the possibility -of duplicate index values and therefore ensuring that the drawing order of Widgets is always explicit. +of duplicate index values and therefore ensuring that `Widget` drawing order is always explicit. The [Layerable](../androidplot-core/src/main/java/com/androidplot/util/Layerable.java) interface -defines methods used for manipulating the z-index of a Widget. +defines methods used for manipulating the z-index of a `Widget`. ## Adding & Removing Widgets -New Widgets can be added either to the front or back of the z-index using these methods: +New `Widget` instances can be added either to the front or back of the z-index using these methods: * `LayoutManager.addToTop(Widget)` * `LayoutManager.addToBottom(Widget)` From e6ac20ac8952e907ffee4cc503b198131547c55c Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Wed, 12 Jul 2017 09:27:32 -0500 Subject: [PATCH 08/61] uprev to 1.5.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f7124dd4..d81cbf77 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 25 theTargetSdkVersion = 25 theMinSdkVersion = 5 - theVersionName = '1.5.0' + theVersionName = '1.5.1' theVersionCode = 0 } From d448f5c047e3e9ccdba6b768a3ee12341d54d7ef Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Fri, 4 Aug 2017 07:51:18 -0500 Subject: [PATCH 09/61] #55 Fixes PieRenderer.getContainingSegment for segments larger than 50% of the pie. (#57) --- .../java/com/androidplot/pie/PieRenderer.java | 38 ++++++++++------- .../com/androidplot/pie/PieRendererTest.java | 41 ++++++++++++++++++- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java index 8b663a82..dc83e620 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java @@ -30,11 +30,14 @@ */ public class PieRenderer extends SeriesRenderer { + private static final float FULL_PIE_DEGS = 360f; + private static final float HALF_PIE_DEGS = 180f; + // starting angle to use when drawing the first radial line of the first segment. private float startDegs = 0; // number of degrees to extend from startDegs; can be used to "shape" the pie chart. - private float extentDegs = 360; + private float extentDegs = FULL_PIE_DEGS; // TODO: express donut in units other than px. private float donutSize = 0.5f; @@ -240,7 +243,7 @@ protected PointF calculateLineEnd(float x, float y, float rad, float deg) { protected PointF calculateLineEnd(PointF origin, float rad, float deg) { - double radians = deg * Math.PI / 180F; + double radians = deg * Math.PI / HALF_PIE_DEGS; double x = rad * Math.cos(radians); double y = rad * Math.sin(radians); @@ -292,11 +295,10 @@ public Segment getContainingSegment(PointF point) { float dx = point.x - origin.x; float dy = point.y - origin.y; double theta = Math.atan2(dy, dx); - double angle = (theta * (180f / Math.PI)); + double angle = (theta * (HALF_PIE_DEGS / Math.PI)); if (angle < 0) { - // convert angle to 0-360 range with 0 being in the - // traditional "east" orientation: - angle += 360f; + // bring into 0-360 range + angle += FULL_PIE_DEGS; } // find the segment whose starting and ending angle (degs) contains @@ -310,10 +312,16 @@ public Segment getContainingSegment(PointF point) { float lastOffset = offset; float sweep = (float) (scale * (values[i]) * extentDegs); offset += sweep; - offset = offset % 360; + offset = offset % FULL_PIE_DEGS; final double dist = signedDistance(offset, angle); - if(dist > 0 && dist <= signedDistance(offset, lastOffset)) { + double endDist = signedDistance(offset, lastOffset); + if(endDist < 0) { + // segment accounts for more than 50% of the pie and wrapped around + // need to correct: + endDist = FULL_PIE_DEGS + endDist; + } + if(dist > 0 && dist <= endDist) { return sfPair.getSeries(); } i++; @@ -328,10 +336,10 @@ public Segment getContainingSegment(PointF point) { * @return */ protected static float degsToScreenDegs(float degs) { - degs = degs % 360; + degs = degs % FULL_PIE_DEGS; if (degs > 0) { - return 360 - degs; + return FULL_PIE_DEGS - degs; } else { return degs; } @@ -344,12 +352,12 @@ protected static float degsToScreenDegs(float degs) { * @return */ protected static double signedDistance(double angle1, double angle2) { - double d = Math.abs(angle1 - angle2) % 360; - double r = d > 180 ? 360 - d : d; + double d = Math.abs(angle1 - angle2) % FULL_PIE_DEGS; + double r = d > HALF_PIE_DEGS ? FULL_PIE_DEGS - d : d; //calculate sign - int sign = (angle1 - angle2 >= 0 && angle1 - angle2 <= 180) - || (angle1 - angle2 <= -180 && angle1 - angle2 >= -360) ? 1 : -1; + int sign = (angle1 - angle2 >= 0 && angle1 - angle2 <= HALF_PIE_DEGS) + || (angle1 - angle2 <= -HALF_PIE_DEGS && angle1 - angle2 >= -FULL_PIE_DEGS) ? 1 : -1; r *= sign; return r; } @@ -359,7 +367,7 @@ protected static double signedDistance(double angle1, double angle2) { * @param degs */ protected static void validateInputDegs(float degs) { - if(degs < 0 || degs > 360) { + if(degs < 0 || degs > FULL_PIE_DEGS) { throw new IllegalArgumentException("Degrees values must be between 0.0 and 360."); } } diff --git a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java index dba4ce43..e504582a 100644 --- a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java @@ -110,7 +110,7 @@ public void testOnRender() throws Exception { } @Test - public void testGetContainingSegment() throws Exception { + public void getContainingSegment_returnsCorrectSegment() throws Exception { Segment segment1 = spy(new Segment("s1", 25)); Segment segment2 = spy(new Segment("s2", 25)); Segment segment3 = spy(new Segment("s3", 25)); @@ -150,6 +150,45 @@ public void testGetContainingSegment() throws Exception { assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0))); } + @Test + public void getContainingSegment_handlesSegmentsLargerThanHalfPie() throws Exception { + Segment segment1 = spy(new Segment("s1", 25)); + Segment segment2 = spy(new Segment("s2", 24)); + Segment segment3 = spy(new Segment("s3", 51)); + SegmentFormatter formatter = spy( + new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN)); + PieRenderer renderer = formatter.getRendererInstance(pieChart); + + pieChart.addSegment(segment1, formatter); + pieChart.addSegment(segment2, formatter); + pieChart.addSegment(segment3, formatter); + + // southeast + assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 100))); + + // southwest + assertEquals(segment2, renderer.getContainingSegment(new PointF(0, 100))); + + // northwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0))); + + // northeast + assertEquals(segment3, renderer.getContainingSegment(new PointF(100, 0))); + + renderer.setStartDegs(90); + // southeast + assertEquals(segment2, renderer.getContainingSegment(new PointF(100, 100))); + + // southwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 100))); + + // northwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0))); + + // northeast + assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0))); + } + @Test public void testDegsToScreenDegs() throws Exception { assertEquals(0f, PieRenderer.degsToScreenDegs(0)); From 44c9b621cd6801b6fc368fea14932a2b50891162 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sat, 5 Aug 2017 09:29:03 -0500 Subject: [PATCH 10/61] Updates buildscript to CircleCI 2.0 --- .circleci/config.yml | 99 +++++++++++++++++++++++++++++++++++ .gitignore | 3 +- androidplot-core/build.gradle | 4 +- build.gradle | 2 +- circle.yml | 59 --------------------- demoapp-wearable/build.gradle | 2 +- 6 files changed, 104 insertions(+), 65 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 circle.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..43c50cfc --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,99 @@ +# Java Gradle CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-java/ for more details +# +version: 2 + +general: + branches: + only: + #- circleci +jobs: + build: + docker: + # specify the version you desire here + #- image: circleci/openjdk:8-jdk + + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + - image: circleci/android:api-25-alpha + + working_directory: ~/repo + + environment: + # Customize the JVM maximum heap limit + JVM_OPTS: -Xmx3200m + TERM: dumb +# KEYSTORE: ${CIRCLE_WORKING_DIRECTORY}/sigining.keystore +# PUBLISHER_ACCT_JSON_FILE: ${CIRCLE_WORKING_DIRECTORY}/publisher_profile.json + + steps: + - checkout + + - run: echo 'export KEYSTORE=${HOME}/repo/sigining.keystore' >> $BASH_ENV + - run: echo 'export PUBLISHER_ACCT_JSON_FILE=${HOME}/repo/publisher_profile.json' >> $BASH_ENV + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "build.gradle" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + # Get private keys etc + - run: ./misc/download_keystore.sh + - run: ./misc/inject_circle_build_number.sh + + - run: ./gradlew dependencies + + - save_cache: + paths: + - ~/.m2 + key: v1-dependencies-{{ checksum "build.gradle" }} + + # run tests & code coc! + - run: ./gradlew testDebug jacocoTestReportDebug + + # build release + - run: ./gradlew assembleRelease + + # javadoc + - run: ./gradlew javadoc + + # trigger codecod.io + - run: bash <(curl -s https://codecov.io/bash) + + - store_artifacts: + path: androidplot-core/build/outputs/aar/ + destination: aar + + - store_artifacts: + path: demoapp/build/outputs/apk/ + destination: apk + + - store_artifacts: + path: androidplot-core/build/reports/jacoco/debug/ + destination: coverage_report + + - store_artifacts: + path: androidplot-core/build/reports/tests/ + destination: test_results + + - store_test_results: + path: androidplot-core/build/test-results/ + + - deploy: + name: "Deploy to Bintray" + command: | + if [ "${CIRCLE_BRANCH}" == "master" ]; + then ./gradlew bintrayUpload; + fi + + - deploy: + name: "Deploy to Google Play" + command: | + if [ "${CIRCLE_BRANCH}" == "master" ]; + then + ./misc/download_google_publisher_json.sh; + ./gradlew publishApkRelease + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index 58204be4..694f2995 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,5 @@ DemoApp/.settings DemoApp/bin DemoApp/gen DemoApp/target -.idea/libraries -.idea/*.xml +.idea **/R.java diff --git a/androidplot-core/build.gradle b/androidplot-core/build.gradle index aff191ce..3e305c7d 100644 --- a/androidplot-core/build.gradle +++ b/androidplot-core/build.gradle @@ -74,7 +74,7 @@ android { lintOptions { abortOnError false } - buildToolsVersion '25.0.0' + buildToolsVersion theBuildToolsVersion } group = 'com.androidplot' @@ -86,7 +86,7 @@ def gitUrl = 'https://github.com/halfhp/androidplot.git' dependencies { compile 'com.halfhp.fig:figlib:1.0.3' - compile 'com.android.support:support-annotations:24.2.0' + compile 'com.android.support:support-annotations:25.3.1' testCompile "org.mockito:mockito-core:1.10.19" testCompile group: 'junit', name: 'junit', version: '4.12' testCompile "org.robolectric:robolectric:3.1" diff --git a/build.gradle b/build.gradle index d81cbf77..7b379558 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.3.2' + classpath 'com.android.tools.build:gradle:2.3.3' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.5.0' diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 494b4bc2..00000000 --- a/circle.yml +++ /dev/null @@ -1,59 +0,0 @@ - -machine: - environment: - KEYSTORE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/sigining.keystore - PUBLISHER_ACCT_JSON_FILE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/publisher_profile.json - -dependencies: - - pre: - - if [ ! -e /usr/local/android-sdk-linux/platforms/android-25 ]; then echo y | android update sdk --all --no-ui --filter "android-25"; fi; - - if [ ! -e /usr/local/android-sdk-linux/build-tools/25.0.2 ]; then echo y | android update sdk --all --no-ui --filter "build-tools-25.0.2"; fi; - - bash ./misc/download_keystore.sh - - bash ./misc/inject_circle_build_number.sh - -test: - - override: - - (./gradlew test assembleRelease javadoc): - timeout: 360 - - post: - - # core lib: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/outputs/aar/ $CIRCLE_ARTIFACTS - - # demo app .apk: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/demoapp/build/outputs/apk/ $CIRCLE_ARTIFACTS - - # javadoc: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/docs/javadoc/ $CIRCLE_ARTIFACTS - - - # junit xml report: - - mkdir -p $CIRCLE_TEST_REPORTS/junit-xml/ - - find . -type f -regex ".*/build/test-results/testReleaseUnitTest/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit-xml/ \; - - # junit html report: - # TODO: recursively copy subdirs etc - - mkdir -p $CIRCLE_TEST_REPORTS/junit-html/ - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/tests/testReleaseUnitTest/* $CIRCLE_TEST_REPORTS/junit-html/ - - # lint report: - - mkdir -p $CIRCLE_TEST_REPORTS/lint/ - - find . -type f -regex ".*/build/outputs/.*html" -exec cp {} $CIRCLE_TEST_REPORTS/lint/ \; - - # code coverage: - - ./gradlew jacocoTestReportDebug - - mkdir -p $CIRCLE_TEST_REPORTS/jacoco/ - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/jacoco/debug/. $CIRCLE_TEST_REPORTS/jacoco - - bash <(curl -s https://codecov.io/bash) - -deployment: - master: - branch: master - commands: - - (./gradlew bintrayUpload): - timeout: 360 - - bash ./misc/download_google_publisher_json.sh - - ./gradlew publishApkRelease diff --git a/demoapp-wearable/build.gradle b/demoapp-wearable/build.gradle index 37406aaa..8154801c 100644 --- a/demoapp-wearable/build.gradle +++ b/demoapp-wearable/build.gradle @@ -19,7 +19,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.2' + classpath 'com.android.tools.build:gradle:2.3.3' } } apply plugin: 'com.android.application' From a16a1df75adf417e4c18083e0f756eb746d6da36 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sat, 5 Aug 2017 09:58:01 -0500 Subject: [PATCH 11/61] #52 - Added NPE check to Plot.renderOnCanvas (#59) --- androidplot-core/src/main/java/com/androidplot/Plot.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index f2eab84a..3ee10174 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -21,6 +21,7 @@ import android.graphics.*; import android.os.Build; import android.os.Looper; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -804,7 +805,10 @@ protected void onDraw(Canvas canvas) { * "heavy lifting". * @param canvas */ - protected synchronized void renderOnCanvas(Canvas canvas) { + protected synchronized void renderOnCanvas(@Nullable Canvas canvas) { + if(canvas == null) { + return; + } try { // any series interested in synchronizing with plot should // implement PlotListener.onBeforeDraw(...) and do a read lock from within its From 9088a7b1ae536fceb9b570ec07c2eb39fa0a13a0 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sat, 5 Aug 2017 09:59:50 -0500 Subject: [PATCH 12/61] updates quickstart lib version to 1.5.1 --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 76060cd9..cf238b57 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -14,7 +14,7 @@ To use the library in your gradle project add the following to your build.gradle ```groovy dependencies { - compile "com.androidplot:androidplot-core:1.5.0" + compile "com.androidplot:androidplot-core:1.5.1" } ``` From 0cb45aef633bc64fcaf4eca17c5b1ffd454f350f Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sat, 5 Aug 2017 21:01:15 -0500 Subject: [PATCH 13/61] uprev to 1.5.2 for development --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7b379558..dad2f2f7 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 25 theTargetSdkVersion = 25 theMinSdkVersion = 5 - theVersionName = '1.5.1' + theVersionName = '1.5.2' theVersionCode = 0 } From 4662832d5c73b7bceba53c9dd948f8e9ff1f9026 Mon Sep 17 00:00:00 2001 From: guycnicholas Date: Wed, 8 Nov 2017 09:40:24 -0800 Subject: [PATCH 14/61] For issue #61 updated screenToSeriesY to use the vertical bounds rather than horizontal (#62) --- .../com/androidplot/xy/XYGraphWidget.java | 2 +- .../com/androidplot/xy/XYGraphWidgetTest.java | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) 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 a6460799..d1ce96ec 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java @@ -489,7 +489,7 @@ protected float seriesToScreenX(Number x) { protected float seriesToScreenY(Number y) { return (float) plot.getBounds().getyRegion(). - transform(y.doubleValue(), gridRect.left, gridRect.right, true); + transform(y.doubleValue(), gridRect.bottom, gridRect.top, true); } @Override diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java index 4b9f156a..0bfe4d9d 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java @@ -79,7 +79,7 @@ public void setUp() throws Exception { xyPlot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1); graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); - graphWidget.setGridRect(new RectF(0, 0, 100, 100)); + graphWidget.setGridRect(new RectF(0, 0, 10, 100)); graphWidget.setLabelRect(new RectF(0, 0, 100, 100)); } @@ -253,11 +253,11 @@ public void testScreenToSeries() throws Exception { assertEquals(-100, coords.x.intValue()); assertEquals(100, coords.y.intValue()); - coords = graphWidget.screenToSeries(new PointF(100, 100)); + coords = graphWidget.screenToSeries(new PointF(10, 100)); assertEquals(100, coords.x.intValue()); assertEquals(-100, coords.y.intValue()); - coords = graphWidget.screenToSeries(new PointF(50, 50)); + coords = graphWidget.screenToSeries(new PointF(5, 50)); assertEquals(0, coords.x.intValue()); assertEquals(0, coords.y.intValue()); } @@ -271,11 +271,11 @@ public void testSeriesToScreen() throws Exception { assertEquals(0f, point.y); point = graphWidget.seriesToScreen(new XYCoords(100, -100)); - assertEquals(100f, point.x); + assertEquals(10f, point.x); assertEquals(100f, point.y); point = graphWidget.seriesToScreen(new XYCoords(0, 0)); - assertEquals(50f, point.x); + assertEquals(5f, point.x); assertEquals(50f, point.y); } @@ -284,8 +284,8 @@ public void testScreenToSeriesX() throws Exception { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); assertEquals(-100, graphWidget.screenToSeriesX(new PointF(0, 0)).intValue()); - assertEquals(100, graphWidget.screenToSeriesX(new PointF(100, 100)).intValue()); - assertEquals(0, graphWidget.screenToSeriesX(new PointF(50, 50)).intValue()); + assertEquals(100, graphWidget.screenToSeriesX(new PointF(10, 100)).intValue()); + assertEquals(0, graphWidget.screenToSeriesX(new PointF(5, 50)).intValue()); } @Test @@ -302,16 +302,16 @@ public void testSeriesToScreenX() throws Exception { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); assertEquals(0f, graphWidget.seriesToScreenX(-100)); - assertEquals(100f, graphWidget.seriesToScreenX(100)); - assertEquals(50f, graphWidget.seriesToScreenX(0)); + assertEquals(10f, graphWidget.seriesToScreenX(100)); + assertEquals(5f, graphWidget.seriesToScreenX(0)); } @Test public void testSeriesToScreenY() throws Exception { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); - assertEquals(0f, graphWidget.seriesToScreenY(100)); - assertEquals(100f, graphWidget.seriesToScreenY(-100)); + assertEquals(100f, graphWidget.seriesToScreenY(100)); + assertEquals(0f, graphWidget.seriesToScreenY(-100)); assertEquals(50f, graphWidget.seriesToScreenY(0)); } } From 55fce04bff334e0988aa253b25eeb667456d5be8 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Fri, 8 Dec 2017 08:43:17 -0600 Subject: [PATCH 15/61] Androidplot 1.5.2 (#65) * target Android SDK 26 * update fig dependency for gradle 3.x.x support * adds custom renderer documentation * remove obsolete class / unnecessary casts * adds sizing section to plot_composition.md * adds more sizing and positioning documentation --- .circleci/config.yml | 2 +- androidplot-core/build.gradle | 6 +- .../src/main/java/com/androidplot/Plot.java | 8 +- .../java/com/androidplot/ui/Formatter.java | 6 +- .../java/com/androidplot/ui/SizeMetric.java | 1 - .../ui/widget/LegendItemOrganizer.java | 9 -- .../java/com/androidplot/xy/BarRenderer.java | 2 +- .../com/androidplot/xy/XYRegionFormatter.java | 6 +- .../src/main/res/values/attrs.xml | 51 ++++++- build.gradle | 13 +- demoapp-wearable/build.gradle | 2 +- demoapp/build.gradle | 11 +- .../demos/SimpleXYPlotActivity.java | 6 +- .../demos/TouchZoomExampleActivity.java | 8 +- .../demos/XYRegionExampleActivity.java | 12 +- .../src/main/res/layout/bar_plot_example.xml | 54 ++++---- .../src/main/res/layout/demo_app_widget.xml | 17 ++- .../res/layout/dynamic_xyplot_example.xml | 22 +-- demoapp/src/main/res/layout/main.xml | 54 +++++--- demoapp/src/main/res/layout/pie_chart.xml | 3 +- .../main/res/layout/step_chart_example.xml | 25 ++-- .../main/res/layout/time_series_example.xml | 42 +++--- .../main/res/layout/touch_zoom_example.xml | 49 +++---- demoapp/src/main/res/values-hdpi/dimens.xml | 18 --- demoapp/src/main/res/values-ldpi/dimens.xml | 20 --- demoapp/src/main/res/values/dimens.xml | 18 --- demoapp/src/main/res/values/style.xml | 28 ---- docs/attrs.md | 50 ++++++- docs/custom_renderer.md | 101 ++++++++++++++ docs/grouprenderer.md | 8 +- docs/images/rounded_bar_renderer.png | Bin 0 -> 70585 bytes docs/images/sizing/abs100x-abs100y.png | Bin 0 -> 5852 bytes docs/images/sizing/abs100x-abs150y.png | Bin 0 -> 6725 bytes docs/images/sizing/abs100x-rel1y.png | Bin 0 -> 6049 bytes docs/images/sizing/fil50x-fil50y.png | Bin 0 -> 7882 bytes docs/images/sizing/rel075x-abs100y.png | Bin 0 -> 5907 bytes docs/index.md | 1 + docs/plot_composition.md | 131 +++++++++++++++++- docs/quickstart.md | 2 +- docs/release_notes.md | 12 ++ gradle/wrapper/gradle-wrapper.properties | 4 +- 41 files changed, 527 insertions(+), 275 deletions(-) delete mode 100644 androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java delete mode 100644 demoapp/src/main/res/values-ldpi/dimens.xml delete mode 100644 demoapp/src/main/res/values/style.xml create mode 100644 docs/custom_renderer.md create mode 100644 docs/images/rounded_bar_renderer.png create mode 100644 docs/images/sizing/abs100x-abs100y.png create mode 100644 docs/images/sizing/abs100x-abs150y.png create mode 100644 docs/images/sizing/abs100x-rel1y.png create mode 100644 docs/images/sizing/fil50x-fil50y.png create mode 100644 docs/images/sizing/rel075x-abs100y.png diff --git a/.circleci/config.yml b/.circleci/config.yml index 43c50cfc..6a24c3f8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,7 +16,7 @@ jobs: # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ - - image: circleci/android:api-25-alpha + - image: circleci/android:api-26-alpha working_directory: ~/repo diff --git a/androidplot-core/build.gradle b/androidplot-core/build.gradle index 3e305c7d..a814a4c1 100644 --- a/androidplot-core/build.gradle +++ b/androidplot-core/build.gradle @@ -58,7 +58,6 @@ task generateAttrsMarkdown(type: AttrMarkdown) { android { compileSdkVersion theCompileSdkVersion - buildToolsVersion theBuildToolsVersion defaultConfig { versionCode theVersionCode @@ -74,7 +73,6 @@ android { lintOptions { abortOnError false } - buildToolsVersion theBuildToolsVersion } group = 'com.androidplot' @@ -85,8 +83,8 @@ def gitUrl = 'https://github.com/halfhp/androidplot.git' dependencies { - compile 'com.halfhp.fig:figlib:1.0.3' - compile 'com.android.support:support-annotations:25.3.1' + compile 'com.halfhp.fig:figlib:1.0.7' + compile 'com.android.support:support-annotations:27.0.2' testCompile "org.mockito:mockito-core:1.10.19" testCompile group: 'junit', name: 'junit', version: '4.12' testCompile "org.robolectric:robolectric:3.1" diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index 3ee10174..8b436199 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -520,7 +520,7 @@ private void loadAttrs(AttributeSet attrs, int defStyle) { // apply "configurator" attrs: (overrides any previously applied styleable attrs) // filter out androidplot prefixed attrs: - HashMap attrHash = new HashMap(); + HashMap attrHash = new HashMap<>(); for (int i = 0; i < attrs.getAttributeCount(); i++) { String attrName = attrs.getAttributeName(i); @@ -529,7 +529,11 @@ private void loadAttrs(AttributeSet attrs, int defStyle) { attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i)); } } - Fig.configure(getContext(), this, attrHash); + try { + Fig.configure(getContext(), this, attrHash); + } catch (FigException e) { + throw new RuntimeException(e); + } } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java index 8f2a354b..d920ab36 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java @@ -48,7 +48,11 @@ public Formatter(Context ctx, int xmlCfgId) { } public void configure(Context ctx, int xmlCfgId) { - Fig.configure(ctx, this, xmlCfgId); + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } } /** diff --git a/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java b/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java index e3819acd..d7f3e58b 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java @@ -43,7 +43,6 @@ protected void validatePair(float value, SizeMode layoutType) { @Override public float getPixelValue(float size) { - //switch(layoutType) switch(getLayoutType()) { case ABSOLUTE: return getValue(); diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java deleted file mode 100644 index 536ee9d8..00000000 --- a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.androidplot.ui.widget; - -import java.util.List; - - -public interface LegendItemOrganizer { - - void organize(List items); -} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java index c696bbf2..bb8c4c7f 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java @@ -238,7 +238,7 @@ protected RectF createBarRect(float w1, float h1, float w2, float h2, BarFormatt return result; } - protected void drawBar(Canvas canvas, Bar bar, RectF rect) { + protected void drawBar(Canvas canvas, Bar bar, RectF rect) { // null yVals are skipped: if(bar.getY() == null) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java index b0a3186f..0492c6c2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java @@ -43,7 +43,11 @@ public class XYRegionFormatter { public XYRegionFormatter(Context ctx, int xmlCfgId) { // prevent configuration of classes derived from this one: if (getClass().equals(XYRegionFormatter.class)) { - Fig.configure(ctx, this, xmlCfgId); + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } } } diff --git a/androidplot-core/src/main/res/values/attrs.xml b/androidplot-core/src/main/res/values/attrs.xml index e640706e..c9dc65a0 100644 --- a/androidplot-core/src/main/res/values/attrs.xml +++ b/androidplot-core/src/main/res/values/attrs.xml @@ -16,12 +16,17 @@ --> + @@ -442,6 +447,10 @@ __dimension|float|integer__ * relative_from_left * relative_from_right * relative_from_center + +`HorizontalPositioning` component of the `HorizontalPosition` of the `TextLabelWidget` +that displays the domain title. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. --> + xmlns:ap="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + ap:title="Growth" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:progress="10" /> + android:progress="1" /> + android:checked="true" + android:text="Series 1" /> + android:checked="true" + android:text="Series 2" /> \ No newline at end of file diff --git a/demoapp/src/main/res/layout/demo_app_widget.xml b/demoapp/src/main/res/layout/demo_app_widget.xml index 85b5fe6b..81977453 100644 --- a/demoapp/src/main/res/layout/demo_app_widget.xml +++ b/demoapp/src/main/res/layout/demo_app_widget.xml @@ -17,14 +17,13 @@ --> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - - + \ No newline at end of file diff --git a/demoapp/src/main/res/layout/dynamic_xyplot_example.xml b/demoapp/src/main/res/layout/dynamic_xyplot_example.xml index ddfd0cba..5b0c5644 100644 --- a/demoapp/src/main/res/layout/dynamic_xyplot_example.xml +++ b/demoapp/src/main/res/layout/dynamic_xyplot_example.xml @@ -1,5 +1,4 @@ - - + xmlns:ap="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + ap:legendTextSize="15sp" + ap:rangeTitle="Range" + ap:title="A Dynamic XY Plot" /> diff --git a/demoapp/src/main/res/layout/main.xml b/demoapp/src/main/res/layout/main.xml index 65a43ef9..590087de 100644 --- a/demoapp/src/main/res/layout/main.xml +++ b/demoapp/src/main/res/layout/main.xml @@ -32,99 +32,117 @@