From f55660cf39855e864cd3e22e54c5f9ce3ec4e460 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sun, 27 Nov 2016 17:24:13 -0600 Subject: [PATCH 01/86] 1.3.0 Changes (#14) * * Cleaned up PanZoom logic and improved zoom functionality. Resolves Issue #11. * Sampling support, including LTTB estimating algorithm implementation, LTTBSampler. * documentation tweak --- .../src/main/java/com/androidplot/Plot.java | 113 +++-- .../src/main/java/com/androidplot/Region.java | 85 +++- .../java/com/androidplot/SeriesRegistry.java | 93 ++++- .../java/com/androidplot/pie/PieChart.java | 18 +- .../java/com/androidplot/pie/PieRenderer.java | 12 +- .../com/androidplot/pie/SegmentBundle.java | 14 + .../com/androidplot/pie/SegmentRegistry.java | 14 + .../java/com/androidplot/ui/Formatter.java | 11 +- .../java/com/androidplot/ui/RenderStack.java | 21 +- ...iesAndFormatter.java => SeriesBundle.java} | 7 +- .../com/androidplot/ui/SeriesRenderer.java | 20 +- .../ui/widget/TextLabelWidget.java | 2 +- .../java/com/androidplot/util/APTrace.java | 26 ++ .../java/com/androidplot/util/FastNumber.java | 66 +++ .../java/com/androidplot/util/PixelUtils.java | 2 +- .../com/androidplot/util/SeriesUtils.java | 161 ++++++- .../java/com/androidplot/xy/BarRenderer.java | 8 +- .../com/androidplot/xy/BubbleRenderer.java | 2 +- .../androidplot/xy/CandlestickRenderer.java | 6 +- .../com/androidplot/xy/EditableXYSeries.java | 33 ++ .../java/com/androidplot/xy/Estimator.java | 10 + .../xy/FastLineAndPointRenderer.java | 10 +- .../java/com/androidplot/xy/FastXYSeries.java | 15 + .../xy/FixedSizeEditableXYSeries.java | 82 ++++ .../com/androidplot/xy/GroupRenderer.java | 8 +- .../java/com/androidplot/xy/LTTBSampler.java | 106 +++++ .../androidplot/xy/LineAndPointFormatter.java | 6 +- .../androidplot/xy/LineAndPointRenderer.java | 89 +++- .../com/androidplot/xy/OrderedXYSeries.java | 33 ++ .../main/java/com/androidplot/xy/PanZoom.java | 392 +++++++----------- .../java/com/androidplot/xy/RectRegion.java | 72 +++- .../com/androidplot/xy/SampledXYSeries.java | 216 ++++++++++ .../main/java/com/androidplot/xy/Sampler.java | 16 + .../com/androidplot/xy/SimpleXYSeries.java | 30 +- .../com/androidplot/xy/XYLegendWidget.java | 28 +- .../main/java/com/androidplot/xy/XYPlot.java | 175 ++------ .../java/com/androidplot/xy/XYSeries.java | 1 - .../com/androidplot/xy/XYSeriesBundle.java | 13 + .../com/androidplot/xy/XYSeriesRegistry.java | 36 ++ .../com/androidplot/xy/XYSeriesRenderer.java | 4 +- .../com/androidplot/xy/ZoomEstimator.java | 26 ++ .../test/java/com/androidplot/PlotTest.java | 31 +- .../test/java/com/androidplot/RegionTest.java | 40 +- .../java/com/androidplot/test/TestUtils.java | 55 +++ .../androidplot/util/InstrumentedXYPlot.java | 24 ++ .../com/androidplot/util/SeriesUtilsTest.java | 183 ++++++-- .../xy/FastLineAndPointRendererTest.java | 2 +- .../com/androidplot/xy/LTTBSamplerTest.java | 68 +++ .../xy/LineAndPointRendererTest.java | 4 +- .../java/com/androidplot/xy/PanZoomTest.java | 113 ++++- .../com/androidplot/xy/RectRegionTest.java | 92 +++- .../androidplot/xy/SampledXYSeriesTest.java | 92 ++++ .../androidplot/xy/SimpleXYSeriesTest.java | 19 + .../com/androidplot/xy/XYGraphWidgetTest.java | 7 +- .../androidplot/xy/XYLegendWidgetTest.java | 4 +- .../java/com/androidplot/xy/XYPlotTest.java | 228 +++++----- .../com/androidplot/xy/ZoomEstimatorTest.java | 65 +++ build.gradle | 2 +- .../demos/BarPlotExampleActivity.java | 8 +- .../androidplot/demos/ListViewActivity.java | 13 +- .../OrientationSensorExampleActivity.java | 8 +- .../demos/SimplePieChartActivity.java | 2 +- .../demos/TouchZoomExampleActivity.java | 107 +++-- .../main/java/com/androidplot/demos/Util.java | 17 + .../demos/XYRegionExampleActivity.java | 2 +- .../main/res/layout/touch_zoom_example.xml | 1 + docs/advanced_xy_plot.md | 89 ++++ docs/dynamicdata.md | 6 +- docs/index.md | 1 + docs/quickstart.md | 2 +- docs/release_notes.md | 7 +- docs/xyplot.md | 10 +- 72 files changed, 2541 insertions(+), 843 deletions(-) create mode 100644 androidplot-core/src/main/java/com/androidplot/pie/SegmentBundle.java create mode 100644 androidplot-core/src/main/java/com/androidplot/pie/SegmentRegistry.java rename androidplot-core/src/main/java/com/androidplot/ui/{SeriesAndFormatter.java => SeriesBundle.java} (79%) create mode 100644 androidplot-core/src/main/java/com/androidplot/util/APTrace.java create mode 100644 androidplot-core/src/main/java/com/androidplot/util/FastNumber.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/EditableXYSeries.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/Estimator.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/Sampler.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRegistry.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java create mode 100644 androidplot-core/src/test/java/com/androidplot/test/TestUtils.java create mode 100644 androidplot-core/src/test/java/com/androidplot/util/InstrumentedXYPlot.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/ZoomEstimatorTest.java create mode 100644 demoapp/src/main/java/com/androidplot/demos/Util.java create mode 100644 docs/advanced_xy_plot.md diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index 512e1a28..0fe39d2b 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -43,7 +43,9 @@ /** * Base class for all Plot implementations. */ -public abstract class Plot +public abstract class Plot, + RegistryType extends SeriesRegistry> extends View implements Resizable { private static final String TAG = Plot.class.getName(); private static final String XML_ATTR_PREFIX = "androidplot"; @@ -66,10 +68,23 @@ public HashMap, RendererType> getRenderers() { /** * Associates lists series and getFormatter pairs with the class of the Renderer used to render them. */ - public SeriesRegistry getSeriesRegistry() { - return seriesRegistry; + public RegistryType getRegistry() { + return registry; } + public void setRegistry(RegistryType registry) { + this.registry = registry; + for(BundleType bundle : registry.getSeriesAndFormatterList()) { + attachSeries(bundle.getSeries(), bundle.getFormatter()); + } + } + + /** + * + * @return A new instance of RegistryType + */ + protected abstract RegistryType getRegistryInstance(); + public TextLabelWidget getTitle() { return title; } @@ -152,7 +167,8 @@ public enum RenderMode { private final Object renderSynch = new Object(); private HashMap, RendererType> renderers; - private SeriesRegistry seriesRegistry; + + private RegistryType registry; private final ArrayList listeners; private Thread renderThread; @@ -161,7 +177,7 @@ public enum RenderMode { { listeners = new ArrayList<>(); - seriesRegistry = new SeriesRegistry<>(); + registry = getRegistryInstance(); renderers = new HashMap<>(); borderPaint = new Paint(); @@ -459,8 +475,8 @@ private void loadAttrs(AttributeSet attrs, int defStyle) { styleableName = styleableName.replace('.', '_'); try { /** - * Use reflection to safely check for the existence of styleable defs for Plot - * and it's derivatives. This safety check is necessary to avoid runtime exceptions + * Use reflection to safely run for the existence of styleable defs for Plot + * and it's derivatives. This safety run is necessary to avoid runtime exceptions * in apps that don't include Androidplot as a .aar and won't have access to * the resources defined in the core library. */ @@ -507,7 +523,7 @@ private void loadAttrs(AttributeSet attrs, int defStyle) { for (int i = 0; i < attrs.getAttributeCount(); i++) { String attrName = attrs.getAttributeName(i); - // case insensitive check to see if this attr begins with our prefix: + // case insensitive run to see if this attr begins with our prefix: if (attrName != null && attrName.toUpperCase().startsWith(XML_ATTR_PREFIX.toUpperCase())) { attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i)); } @@ -569,11 +585,14 @@ public synchronized boolean addSeries(FormatterType formatter, SeriesType... ser * @return True if the series was added or false if the series / formatter pair already exists in the registry. */ public synchronized boolean addSeries(SeriesType series, FormatterType formatter) { - Class rendererClass = formatter.getRendererClass(); + final boolean result = getRegistry().add(series, formatter); + attachSeries(series, formatter); + return result; + } -// if(getSeries(series, rendererClass) != null) { -// return false; -// } + protected void attachSeries(SeriesType series, FormatterType formatter) { + + Class rendererClass = formatter.getRendererClass(); // initialize the Renderer if necessary: if(!getRenderers().containsKey(rendererClass)) { @@ -584,20 +603,17 @@ public synchronized boolean addSeries(SeriesType series, FormatterType formatter if(series instanceof PlotListener) { addListener((PlotListener)series); } - - getSeriesRegistry().add(new SeriesAndFormatter<>(series, formatter)); - return true; } /** * * @param series * @param rendererClass - * @return The {@link SeriesAndFormatter} that matches the series and rendererClass params, or null if one is not found. + * @return The {@link SeriesBundle} that matches the series and rendererClass params, or null if one is not found. */ - protected SeriesAndFormatter getSeries(SeriesType series, Class rendererClass) { - for(SeriesAndFormatter thisPair : seriesRegistry) { - if(thisPair.getSeries() == series && thisPair.getFormatter().getRendererClass() == rendererClass) { + protected SeriesBundle getSeries(SeriesType series, Class rendererClass) { + for(SeriesBundle thisPair : getSeries(series)) { + if(thisPair.getFormatter().getRendererClass() == rendererClass) { return thisPair; } } @@ -607,17 +623,10 @@ protected SeriesAndFormatter getSeries(SeriesType ser /** * * @param series - * @return A List of {@link SeriesAndFormatter} instances that reference series. - */ - protected List> getSeries(SeriesType series) { - List> results = - new ArrayList<>(); - for(SeriesAndFormatter thisPair : seriesRegistry) { - if(thisPair.getSeries() == series) { - results.add(thisPair); - } - } - return results; + * @return A List of {@link SeriesBundle} instances that reference series. + */ + protected List> getSeries(SeriesType series) { + return getRegistry().get(series); } /** @@ -626,26 +635,18 @@ protected List> getSeries(SeriesTy * from the plot completely. * @param series * @param rendererClass - * @return The SeriesAndFormatterPair that was removed or null if nothing was removed. + * @return True if anything was removed, false otherwise */ - public synchronized SeriesAndFormatter removeSeries(SeriesType series, Class rendererClass) { + public synchronized boolean removeSeries(SeriesType series, Class rendererClass) { - List> results = getSeries(series); - SeriesAndFormatter result = null; - for(SeriesAndFormatter thisPair : results) { - if(thisPair.getFormatter().getRendererClass() == rendererClass) { - result = thisPair; - getSeriesRegistry().remove(result); - break; - } - } + List removedItems = getRegistry().remove(series, rendererClass); // if series implements PlotListener and is not assigned to any other renderers remove it as a listener: - if(series instanceof PlotListener && results.size() == 1) { + if (removedItems.size() == 1 && series instanceof PlotListener) { removeListener((PlotListener) series); + return true; } - - return result; + return false; } /** @@ -653,31 +654,28 @@ public synchronized SeriesAndFormatter removeSeries(S * @param series */ public synchronized void removeSeries(SeriesType series) { - - for(Iterator> it = getSeriesRegistry().iterator(); it.hasNext();) { - if(it.next().getSeries() == series) { - it.remove(); - } - } - // if series implements PlotListener, remove it from listeners: if (series instanceof PlotListener) { removeListener((PlotListener) series); } + + getRegistry().remove(series); } /** * Remove all series from the plot. */ public void clear() { - for(Iterator> it = getSeriesRegistry().iterator(); it.hasNext();) { - it.next(); - it.remove(); + for(SeriesType series : getRegistry().getSeriesList()) { + if(series instanceof PlotListener) { + removeListener((PlotListener) series); + } } + getRegistry().clear(); } public boolean isEmpty() { - return getSeriesRegistry().isEmpty(); + return getRegistry().isEmpty(); } /** @@ -754,7 +752,7 @@ protected synchronized void onSizeChanged (int w, int h, int oldw, int oldh) { PixelUtils.init(getContext()); // disable hardware acceleration if it's not explicitly supported - // by the current Plot implementation. this check only applies to + // by the current Plot implementation. this run only applies to // honeycomb and later environments. if (Build.VERSION.SDK_INT >= 11) { if (!isHwAccelerationSupported() && isHardwareAccelerated()) { @@ -831,12 +829,13 @@ protected synchronized void renderOnCanvas(Canvas canvas) { } catch (Exception e) { Log.e(TAG, "Exception while rendering Plot.", e); } - } finally { + isIdle = true; // any series interested in synchronizing with plot should // implement PlotListener.onAfterDraw(...) and do a read unlock from within that // invocation. This is the entry point for that invocation. notifyListenersAfterDraw(canvas); + } finally { } } diff --git a/androidplot-core/src/main/java/com/androidplot/Region.java b/androidplot-core/src/main/java/com/androidplot/Region.java index e6fad364..2ace04a7 100644 --- a/androidplot-core/src/main/java/com/androidplot/Region.java +++ b/androidplot-core/src/main/java/com/androidplot/Region.java @@ -17,12 +17,16 @@ package com.androidplot; +import com.androidplot.util.*; + /** * A one dimensional region represented by a starting and ending value. */ public class Region { - private Number min; - private Number max; + private FastNumber min; + private FastNumber max; + private FastNumber cachedLength; + private Region defaults = this; public Region() {} @@ -46,6 +50,11 @@ public Region(Number v1, Number v2) { } } + public void setMinMax(Region region) { + setMin(region.getMin()); + setMax(region.getMax()); + } + /** * * @param v1 @@ -58,8 +67,14 @@ public static Number measure(Number v1, Number v2) { } public Number length() { - return getMax() == null || getMin() == null ? - null : getMax().doubleValue() - getMin().doubleValue(); + if(cachedLength == null) { + Number l = getMax() == null || getMin() == null ? + null : getMax().doubleValue() - getMin().doubleValue(); + if(l != null) { + cachedLength = new FastNumber(l); + } + } + return cachedLength; } /** @@ -95,7 +110,7 @@ public Number transform(double value, Region region2) { } public Number transform(double value, Region region2, boolean flip) { - return transform(value, region2.min.doubleValue(), region2.max.doubleValue(), flip); + return transform(value, region2.getMin().doubleValue(), region2.getMax().doubleValue(), flip); } public double transform(double value, double min, double max, boolean flip) { @@ -112,13 +127,35 @@ public double transform(double value, double min, double max, boolean flip) { } public Number ratio(Region r2) { - return ratio(r2.min.doubleValue(), r2.max.doubleValue()); + return ratio(r2.getMin().doubleValue(), r2.getMax().doubleValue()); } + /** + * + * @param min + * @param max + * @return length of this series divided by the length of the distance between min and max. + */ public double ratio(double min, double max) { return length().doubleValue() / (max - min); } + + public void union(Number value) { + if(value == null) { + return; + } + double val = value.doubleValue(); + if(getMin() == null || + val < getMin().doubleValue()) { + setMin(value); + } + if(getMax() == null || val > + getMax().doubleValue()) { + setMax(value); + } + } + /** * Compares the input bounds min/max against this instance's current min/max. * If the input.min is less than this.min then this.min will be set to input.min. @@ -128,14 +165,8 @@ public double ratio(double min, double max) { * @param input */ public void union(Region input) { - if(getMin() == null || input.min != null && - input.min.doubleValue() < getMin().doubleValue()) { - setMin(input.min); - } - if(getMax() == null || input.max != null && input.max.doubleValue() > - getMax().doubleValue()) { - setMax(input.max); - } + union(input.getMin()); + union(input.getMax()); } /** @@ -176,10 +207,17 @@ public Number getMin() { } public void setMin(Number min) { - if(min == null && defaults == null) { - throw new NullPointerException("Region values cannot be null unless defaults have been set."); + cachedLength = null; + if(min == null) { + if(defaults == null) { + throw new NullPointerException( + "Region values cannot be null unless defaults have been set."); + } else { + this.min = null; + } + } else { + this.min = new FastNumber(min); } - this.min = min; } public boolean isMaxSet() { @@ -191,10 +229,17 @@ public Number getMax() { } public void setMax(Number max) { - if(max == null && defaults == null) { - throw new NullPointerException("Region values can never be null unless defaults have been set."); + cachedLength = null; + if(max == null) { + if(defaults == null) { + throw new NullPointerException( + "Region values can never be null unless defaults have been set."); + } else { + this.max = null; + } + } else { + this.max = new FastNumber(max); } - this.max = max; } /** diff --git a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java index 49d3348a..948d1d11 100644 --- a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java +++ b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java @@ -17,8 +17,9 @@ package com.androidplot; import com.androidplot.ui.Formatter; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; +import java.io.*; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -27,14 +28,98 @@ * Manages a list of {@link Series} and their associated {@link Formatter} in the context of a {@link Plot}. * @since 0.9.7 */ -public class SeriesRegistry - extends ArrayList> { +public abstract class SeriesRegistry + , + SeriesType extends Series, FormatterType extends Formatter> implements Serializable { + private ArrayList registry = new ArrayList<>(); + + public List getSeriesAndFormatterList() { + return registry; + } public List getSeriesList() { List result = new ArrayList<>(); - for(SeriesAndFormatter sfPair : this) { + for(SeriesBundle sfPair : registry) { result.add(sfPair.getSeries()); } return result; } + + public int size() { + return registry.size(); + } + + public boolean isEmpty() { + return registry.isEmpty(); + } + + public boolean add(SeriesType series, FormatterType formatter) { + return registry.add(newSeriesBundle(series, formatter)); + } + + protected abstract BundleType newSeriesBundle(SeriesType series, FormatterType formatter); + + /** + * + * @param series + * @return A List of {@link SeriesBundle} instances that reference series. + */ + protected List> get(SeriesType series) { + List> results = + new ArrayList<>(); + for(SeriesBundle thisPair : registry) { + if(thisPair.getSeries() == series) { + results.add(thisPair); + } + } + return results; + } + + public synchronized List remove(SeriesType series, Class rendererClass) { + ArrayList removedItems = new ArrayList<>(); + for(Iterator it = registry.iterator(); it.hasNext();) { + BundleType b = it.next(); + if(b.getSeries() == series && b.getFormatter().getRendererClass() == rendererClass) { + it.remove(); + removedItems.add(b); + } + } + return removedItems; + } + + /** + * Remove all occurrences of series regardless of the associated Renderer. + * @param series + */ + public synchronized boolean remove(SeriesType series) { + boolean result = false; + for(Iterator it = registry.iterator(); it.hasNext();) { + if(it.next().getSeries() == series) { + it.remove(); + result = true; + } + } + return result; + } + + /** + * Remove all series from the plot. + */ + public void clear() { + for(Iterator it + = registry.iterator(); it.hasNext();) { + it.next(); + it.remove(); + } + } + + public List> getLegendEnabledItems() { + List> sfList = new ArrayList<>(); + for(SeriesBundle sf : registry) { + if(sf.getFormatter().isLegendIconEnabled()) { + sfList.add(sf); + } + } + return sfList; + } } 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 3469bf6d..615efaaa 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java @@ -19,20 +19,17 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; -import com.androidplot.Plot; -import com.androidplot.R; -import com.androidplot.ui.Anchor; -import com.androidplot.ui.SizeMode; -import com.androidplot.ui.Size; + +import com.androidplot.*; +import com.androidplot.ui.*; import com.androidplot.util.AttrUtils; import com.androidplot.util.PixelUtils; -import com.androidplot.ui.HorizontalPositioning; -import com.androidplot.ui.VerticalPositioning; /** * Basic representation of a Pie Chart that displays a title and pie widget. */ -public class PieChart extends Plot { +public class PieChart extends Plot { private static final int DEFAULT_PIE_WIDGET_H_DP = 18; private static final int DEFAULT_PIE_WIDGET_W_DP = 10; @@ -49,6 +46,11 @@ public void setPie(PieWidget pie) { @SuppressWarnings("FieldCanBeLocal") private PieWidget pie; + @Override + protected SegmentRegistry getRegistryInstance() { + return new SegmentRegistry(); + } + public PieChart(Context context, String title) { super(context, title); } 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 a53e38d4..5d52d94b 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java @@ -19,7 +19,7 @@ import android.graphics.*; import com.androidplot.exception.PlotRenderException; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.SeriesRenderer; import com.androidplot.ui.RenderStack; @@ -70,7 +70,7 @@ public void onRender(Canvas canvas, RectF plotArea, Segment series, SegmentForma RectF rec = new RectF(origin.x - radius, origin.y - radius, origin.x + radius, origin.y + radius); int i = 0; - for (SeriesAndFormatter sfPair : getSeriesAndFormatterList()) { + for (SeriesBundle sfPair : getSeriesAndFormatterList()) { float lastOffset = offset; float sweep = (float) (scale * (values[i]) * 360); offset += sweep; @@ -220,10 +220,10 @@ protected double calculateScale(double[] values) { } protected double[] getValues() { - List> seriesList = getSeriesAndFormatterList(); + List> seriesList = getSeriesAndFormatterList(); double[] result = new double[seriesList.size()]; int i = 0; - for (SeriesAndFormatter sfPair : seriesList) { + for (SeriesBundle sfPair : seriesList) { result[i] = sfPair.getSeries().getValue().doubleValue(); i++; } @@ -287,12 +287,12 @@ public Segment getContainingSegment(PointF point) { // find the segment whose starting and ending angle (degs) contains // the angle calculated above - List> seriesList = getSeriesAndFormatterList(); + List> seriesList = getSeriesAndFormatterList(); int i = 0; double[] values = getValues(); double scale = calculateScale(values); float offset = startDeg; - for (SeriesAndFormatter sfPair : seriesList) { + for (SeriesBundle sfPair : seriesList) { float lastOffset = offset; float sweep = (float) (scale * (values[i]) * 360); offset += sweep; diff --git a/androidplot-core/src/main/java/com/androidplot/pie/SegmentBundle.java b/androidplot-core/src/main/java/com/androidplot/pie/SegmentBundle.java new file mode 100644 index 00000000..465fd69b --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/pie/SegmentBundle.java @@ -0,0 +1,14 @@ +package com.androidplot.pie; + +import com.androidplot.ui.*; + +/** + * Manages the association between a given {@link Segment} and the {@link SegmentFormatter} that + * will be used to render it. + */ +public class SegmentBundle extends SeriesBundle { + + public SegmentBundle(Segment series, SegmentFormatter formatter) { + super(series, formatter); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/pie/SegmentRegistry.java b/androidplot-core/src/main/java/com/androidplot/pie/SegmentRegistry.java new file mode 100644 index 00000000..1cbf905f --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/pie/SegmentRegistry.java @@ -0,0 +1,14 @@ +package com.androidplot.pie; + +import com.androidplot.*; + +/** + * SeriesRegistry implementation to be used in a {@link PieChart}. + */ +public class SegmentRegistry extends SeriesRegistry { + + @Override + protected SegmentBundle newSeriesBundle(Segment series, SegmentFormatter formatter) { + return new SegmentBundle(series, formatter); + } +} 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 5ed92142..8f2a354b 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java @@ -51,19 +51,26 @@ public void configure(Context ctx, int xmlCfgId) { Fig.configure(ctx, this, xmlCfgId); } + /** + * + * @param plot + * @param + * @return @return An instance of SeriesRenderer constructed with the specified plot. + */ public T getRendererInstance(PlotType plot) { return (T) doGetRendererInstance(plot); } /** * - * @return The Class of SeriesRenderer that should be used. + * @return The Class of SeriesRenderer that should be used when rendering series associated + * with instances of this formatter. */ public abstract Class getRendererClass(); /** * - * @return An instance of SeriesRenderer that took plot as an argument to its constructor. + * @return An instance of SeriesRenderer constructed with the specified plot. */ protected abstract SeriesRenderer doGetRendererInstance(PlotType plot); 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 ff570475..a1a07c25 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java @@ -40,20 +40,20 @@ public ArrayList> getElements() { * An element on the render stack. */ public class StackElement { - private SeriesAndFormatter seriesAndFormatter; + private SeriesBundle seriesBundle; private boolean isEnabled = true; - public StackElement(SeriesAndFormatter seriesAndFormatter) { - set(seriesAndFormatter); + public StackElement(SeriesBundle seriesBundle) { + set(seriesBundle); } - public SeriesAndFormatter get() { - return seriesAndFormatter; + public SeriesBundle get() { + return seriesBundle; } - public void set(SeriesAndFormatter seriesAndFormatter) { - this.seriesAndFormatter = seriesAndFormatter; + public void set(SeriesBundle seriesBundle) { + this.seriesBundle = seriesBundle; } public boolean isEnabled() { @@ -72,7 +72,7 @@ public void setEnabled(boolean isEnabled) { public RenderStack(Plot plot) { this.plot = plot; - elements = new ArrayList<>(plot.getSeriesRegistry().size()); + elements = new ArrayList<>(plot.getRegistry().size()); } /** @@ -84,8 +84,9 @@ public void sync() { * TODO: rendering performance *might* be improved by reusing StackElement instances but I'm skeptical... */ getElements().clear(); - List> pairList = plot.getSeriesRegistry(); - for(SeriesAndFormatter thisPair: pairList) { + List> pairList + = plot.getRegistry().getSeriesAndFormatterList(); + for(SeriesBundle thisPair: pairList) { getElements().add(new StackElement<>(thisPair)); } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/SeriesAndFormatter.java b/androidplot-core/src/main/java/com/androidplot/ui/SeriesBundle.java similarity index 79% rename from androidplot-core/src/main/java/com/androidplot/ui/SeriesAndFormatter.java rename to androidplot-core/src/main/java/com/androidplot/ui/SeriesBundle.java index db9c9f26..6a5e8481 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SeriesAndFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SeriesBundle.java @@ -19,14 +19,15 @@ import com.androidplot.Series; /** - * Defines an association between a Series and a Formatter. + * Defines a relationship between a Series instance and other elements needed to unique render that instance + * such as a Formatter etc. */ -public class SeriesAndFormatter { +public class SeriesBundle { private final SeriesType series; private final FormatterType formatter; - public SeriesAndFormatter(SeriesType series, FormatterType formatter) { + public SeriesBundle(SeriesType series, FormatterType formatter) { this.series = series; this.formatter = formatter; } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java b/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java index ce2896da..f535ee32 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java @@ -54,8 +54,8 @@ public SeriesFormatterType getFormatter(SeriesType series) { * @param sfPair The series / formatter pair to be rendered * @throws PlotRenderException */ - public void render(Canvas canvas, RectF plotArea, SeriesAndFormatter sfPair, RenderStack stack) throws PlotRenderException { + public void render(Canvas canvas, RectF plotArea, SeriesBundle sfPair, RenderStack stack) throws PlotRenderException { onRender(canvas, plotArea, sfPair.getSeries(), sfPair.getFormatter(), stack); } @@ -91,14 +91,14 @@ public void drawSeriesLegendIcon(Canvas canvas, RectF rect, SeriesFormatterType /** * - * @return A List of all {@link SeriesAndFormatter} instances currently associated + * @return A List of all {@link SeriesBundle} instances currently associated * with this Renderer. */ - public List> getSeriesAndFormatterList() { - List> results = new ArrayList<>(); - ArrayList sfList = getPlot().getSeriesRegistry(); - - for(SeriesAndFormatter thisPair : sfList) { + public List> getSeriesAndFormatterList() { + List> results = new ArrayList<>(); + List sfList = getPlot().getRegistry().getSeriesAndFormatterList(); + getPlot().getRegistry().getSeriesAndFormatterList(); + for(SeriesBundle thisPair : sfList) { if(thisPair.rendersWith(this)) { results.add(thisPair); } @@ -113,9 +113,9 @@ public void drawSeriesLegendIcon(Canvas canvas, RectF rect, SeriesFormatterType */ public List getSeriesList() { List results = new ArrayList<>(); - ArrayList sfList = getPlot().getSeriesRegistry(); + List sfList = getPlot().getRegistry().getSeriesAndFormatterList(); - for(SeriesAndFormatter thisPair : sfList) { + for(SeriesBundle thisPair : sfList) { if(thisPair.rendersWith(this)) { results.add(thisPair.getSeries()); } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java index facf3048..9f47aea9 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java @@ -143,7 +143,7 @@ public Paint getLabelPaint() { public void setLabelPaint(Paint labelPaint) { this.labelPaint = labelPaint; - // when paint changes, packing params change too so check + // when paint changes, packing params change too so run // to see if we need to resize: if(autoPackEnabled) { pack(); diff --git a/androidplot-core/src/main/java/com/androidplot/util/APTrace.java b/androidplot-core/src/main/java/com/androidplot/util/APTrace.java new file mode 100644 index 00000000..88e6317a --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/util/APTrace.java @@ -0,0 +1,26 @@ +package com.androidplot.util; + +import android.os.*; + +/** + * Wraps {@link Trace} to provide API-safe methods as well as an easy target for runtime removal + * via obfuscation. + */ +public abstract class APTrace { + + public static void begin(final String sectionName) { + if(Build.VERSION.SDK_INT >= 18) { + Trace.beginSection(sectionName); + } else { + // TODO: alternate impl? + } + } + + public static void end() { + if(Build.VERSION.SDK_INT >= 18) { + Trace.endSection(); + } else { + // TODO: alternate impl? + } + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java new file mode 100644 index 00000000..049431b3 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java @@ -0,0 +1,66 @@ +package com.androidplot.util; + +/** + * An extension of {@link Number} optimized for speed at the cost of memory. + */ +public class FastNumber extends Number { + + private Number number; + private boolean hasDoublePrimitive; + private boolean hasFloatPrimitive; + private boolean hasIntPrimitive; + + private double doublePrimitive; + private float floatPrimitive; + private int intPrimitive; + + public FastNumber(Number number) { + + // avoid nested instances of FastNumber : + if(number instanceof FastNumber) { + FastNumber rhs = (FastNumber) number; + this.number = rhs.number; + this.hasDoublePrimitive = rhs.hasDoublePrimitive; + this.hasFloatPrimitive = rhs.hasFloatPrimitive; + this.hasIntPrimitive = rhs.hasIntPrimitive; + this.doublePrimitive = rhs.doublePrimitive; + this.floatPrimitive = rhs.floatPrimitive; + this.intPrimitive = rhs.intPrimitive; + } else { + this.number = number; + } + } + + @Override + public int intValue() { + if(!hasIntPrimitive) { + intPrimitive = number.intValue(); + hasIntPrimitive = true; + } + return intPrimitive; + } + + @Override + public long longValue() { + // TODO: optimize me! + return number.longValue(); + } + + @Override + public float floatValue() { + if(!hasFloatPrimitive) { + floatPrimitive = number.floatValue(); + hasFloatPrimitive = true; + } + return floatPrimitive; + } + + @Override + public double doubleValue() { + if(!hasDoublePrimitive) { + doublePrimitive = number.doubleValue(); + hasDoublePrimitive = true; + } + return doublePrimitive; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java b/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java index 7070e6dc..8e3260b7 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java @@ -134,7 +134,7 @@ public InternalDimension(float value, int unit) { } /** - * Safety check to hopefully help clarify what could otherwise be a confusing NPE. + * Safety run to hopefully help clarify what could otherwise be a confusing NPE. */ private static void checkMetrics() { if(metrics == 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 95537723..922e51b9 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java @@ -60,15 +60,29 @@ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArr // iterate over each series for (XYSeries series : seriesArray) { - if (series.size() > 0) { + + // 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(constraints == null) { + bounds.union(b); + } else { + if(constraints.contains(b.getMinX(), b.getMinY())) { + bounds.union(b.getMinX(), b.getMinY()); + } + if(constraints.contains(b.getMaxX(), b.getMaxY())) { + bounds.union(b.getMaxX(), b.getMaxY()); + } + } + + } else if (series.size() > 0) { for (int i = 0; i < series.size(); i++) { final Number xi = series.getX(i); final Number yi = series.getY(i); // if constraints have been set, make sure this xy coordinate exists within them: if (constraints == null || constraints.contains(xi, yi)) { - minMax(bounds.getxRegion(), xi); - minMax(bounds.getyRegion(), yi); + bounds.union(xi, yi); } } } @@ -87,30 +101,134 @@ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArr public static Region minMax(Region bounds, List... lists) { for (final List list : lists) { for (final Number i : list) { - minMax(bounds, i); + //minMax(bounds, i); + bounds.union(i); } } return bounds; } /** - * Compares a number against the current min/max values in a region, updating the region - * with the new value if appropriate. - * @param bounds - * @param number + * Compute the range of visible i-vals in the specified series. Assumes that x-vals are + * in strict ascending order; behavior is undefined otherwise. + * @param series + * @param visibleBounds The visible constraints of the plot * @return */ - public static Region minMax(Region bounds, Number number) { - if (number != null) { - final double di = number.doubleValue(); - if (bounds.getMin() == null || di < bounds.getMin().doubleValue()) { - bounds.setMin(number); + public static Region iBounds(XYSeries series, RectRegion visibleBounds) { + final float step = series.size() >= 200 ? 50 : 1; + final int iBoundsMin = iBoundsMin(series, visibleBounds.getMinX().doubleValue(), step); + final int iBoundsMax = iBoundsMax(series, visibleBounds.getMaxX().doubleValue(), step); + return new Region(iBoundsMin, iBoundsMax); + } + + /** + * TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing + * TODO a binary search algorithm that also supports nulls is not trivial and would not likely + * TODO result in any noticeable performance increase here. It's a task for another day! + * @param series + * @param visibleMax + * @param step + * @return The index of the smallest non-null value that is greater than visibleMax, or the index + * of the last element if no such value exists. + */ + protected static int iBoundsMax(XYSeries series, double visibleMax, float step) { + int max = series.size() - 1; + final int seriesSize = series.size(); + final int steps = (int) Math.ceil(seriesSize / step); + for (int stepIndex = steps; stepIndex >= 0; stepIndex--) { + final int i = stepIndex * (int) step; + for (int ii = 0; ii < step; ii++) { + final int iii = i + ii; + if(iii < seriesSize) { + final Number thisX = series.getX(iii); + if (thisX != null) { + final double thisDouble = thisX.doubleValue(); + if (thisDouble > visibleMax) { + // this is the smallest non-null value in this block, so skip + // to the next block: + max = iii; + break; + } else if (thisDouble == visibleMax) { + return iii; + } else { + return max; + } + } + } + } + } + return max; + } + + /** + * TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing + * TODO a binary search algorithm that also supports nulls is not trivial and would not likely + * TODO result in any noticeable performance increase here. It's a task for another day! + * @param series + * @param visibleMin + * @param step + * @return The index of the largest non-null value that is less than visible, or 0 + * (the first element index) if no such value exists. + */ + protected static int iBoundsMin(XYSeries series, double visibleMin, float step) { + int min = 0; + final int steps = (int) Math.ceil(series.size() / step); + for (int stepIndex = 1; stepIndex <= steps; stepIndex++) { + final int i = stepIndex * (int) step; + for (int ii = 1; ii <= step; ii++) { + final int iii = i - ii; + if(iii < 0) { + break; + } + if(iii < series.size()) { + final Number thisX = series.getX(iii); + if (thisX != null) { + if (thisX.doubleValue() < visibleMin) { + // this is the largest non-null value in this block, so skip + // to the next block: + min = iii; + break; + } else if (thisX.doubleValue() == visibleMin) { + return iii; + } else { + return min; + } + } + } + } + } + return min; + } + + /** + * Determine the minMax iVals of the xVals surrounding a range of one or more null values. + * @param series + * @param index index of the null value in question + * @return The iVals of the non-null values surrounding the null range. If the null range is unbounded on + * either side then either or both min and max values will also be null. + */ + protected static Region getNullRegion(XYSeries series, int index) { + Region region = new Region(); + if(series.getX(index) != null) { + throw new IllegalArgumentException("Attempt to find null region for non null index: " + index); + } + for(int i = index - 1; i >= 0; i--) { + Number val = series.getX(i); + if(val != null) { + region.setMin(i); + break; } - if (bounds.getMax() == null || di > bounds.getMax().doubleValue()) { - bounds.setMax(number); + } + + for(int i = index + 1; i < series.size(); i++) { + Number val = series.getX(i); + if(val != null) { + region.setMax(i); + break; } } - return bounds; + return region; } /** @@ -141,4 +259,15 @@ public static void main(String[] args) { System.out.println("Benchmark avg:" + (sumTime / numIterations) + "ms."); } + + /** + * Determine the XVal order of an XYSeries. If series does not implement {@link OrderedXYSeries} + * then {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE} is assumed. + * @param series + * @return The {@link com.androidplot.xy.OrderedXYSeries.XOrder} of the series. + */ + public static OrderedXYSeries.XOrder getXYOrder(XYSeries series) { + return series instanceof OrderedXYSeries ? + ((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE; + } } 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 43c3899e..108d0c6c 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java @@ -27,7 +27,7 @@ import android.graphics.RectF; import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; /** * Renders the points in an XYSeries as bars. @@ -136,8 +136,8 @@ public FormatterType getFormatter(int index, XYSeries series) { @Override - public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { + public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { TreeMap axisMap = new TreeMap(); @@ -145,7 +145,7 @@ public void onRender(Canvas canvas, RectF plotArea, List thisPair : sfList) { + for(SeriesBundle thisPair : sfList) { BarGroup barGroup; // For each value in the series diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java index 9811ffe3..1e0ba9c5 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java @@ -121,7 +121,7 @@ public void setBubbleScaleMode(BubbleScaleMode bubbleScaleMode) { protected Region calculateBounds() { Region bounds = new Region(); - for(SeriesAndFormatter f : getSeriesAndFormatterList()) { + for(SeriesBundle f : getSeriesAndFormatterList()) { SeriesUtils.minMax(bounds, f.getSeries().getZVals()); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java index 1505d0d4..81c4363c 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java @@ -18,7 +18,7 @@ import android.graphics.*; import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import java.util.List; @@ -49,8 +49,8 @@ public CandlestickRenderer(XYPlot plot) { @Override - public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { + public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { final FormatterType formatter = sfList.get(0).getFormatter(); for(int i = 0; i < seriesSize; i++) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/EditableXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/EditableXYSeries.java new file mode 100644 index 00000000..3fbff1be --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/EditableXYSeries.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot.xy; + +/** + * An {@link XYSeries} that exposes methods to set values and resize + */ +public interface EditableXYSeries extends XYSeries { + + void setX(Number x, int index); + void setY(Number y, int index); + + /** + * Resize to accommodate the specified number of x/y pairs. If elements must be droped, those + * at the highest iVal should be removed first. + * @param size + */ + void resize(int size); +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/Estimator.java b/androidplot-core/src/main/java/com/androidplot/xy/Estimator.java new file mode 100644 index 00000000..abf3de8a --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/Estimator.java @@ -0,0 +1,10 @@ +package com.androidplot.xy; + +/** + * Base for all estimation management schemes. + */ +public abstract class Estimator { + + public abstract void run(XYPlot plot, XYSeriesBundle sf); + +} 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 62d8139f..6a646649 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java @@ -27,6 +27,7 @@ * A faster implementation of of {@link LineAndPointRenderer}. For performance reasons, has these constraints: * - Interpolation is not supported * - Does not draw fill + * - Does not support null values * @since 1.2.0 */ public class FastLineAndPointRenderer extends XYSeriesRenderer { @@ -128,13 +129,8 @@ protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) */ public static class Formatter extends LineAndPointFormatter { - public Formatter(Integer lineColor, Integer vertexColor, Integer fillColor, PointLabelFormatter plf) { - super(lineColor, vertexColor, fillColor, plf); - } - - public Formatter(Integer lineColor, Integer vertexColor, - Integer fillColor, PointLabelFormatter plf, FillDirection fillDir) { - super(lineColor, vertexColor, fillColor, plf, fillDir); + public Formatter(Integer lineColor, Integer vertexColor, PointLabelFormatter plf) { + super(lineColor, vertexColor, null, plf); } @Override diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java new file mode 100644 index 00000000..4faa4d54 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastXYSeries.java @@ -0,0 +1,15 @@ +package com.androidplot.xy; + +/** + * An implementation of {@link XYSeries} that defines additional methods to speed up rendering by + * giving a hint to the renderer about the min/max values contained in the series. + */ +public interface FastXYSeries extends XYSeries { + + /** + * TIP: You can use {@link RectRegion#union(Number, Number)} during + * to keep a running tally of min/max values when iterating. + * @return A {@link RectRegion} representing the min/max values that currently exist this series. + */ + RectRegion minMax(); +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java new file mode 100644 index 00000000..240f58dc --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/FixedSizeEditableXYSeries.java @@ -0,0 +1,82 @@ +package com.androidplot.xy; + +import com.androidplot.util.*; + +import java.util.*; + +/** + * An efficient implementation of {@link EditableXYSeries} intended for use cases where + * the total number of points visible is known ahead of time and is fairly static. + * + * {@link #resize(int)} may be used to resize the series when necessary, however it is a slow + * operation and should be avoided as much as possible. + * + */ +public class FixedSizeEditableXYSeries implements EditableXYSeries { + + private List xVals = new ArrayList<>(); + private List yVals = new ArrayList<>(); + private String title; + + public FixedSizeEditableXYSeries(String title, int size) { + setTitle(title); + resize(size); + } + + @Override + public void setX(Number x, int index) { + xVals.set(index, new FastNumber(x)); + } + + @Override + public void setY(Number y, int index) { + yVals.set(index, new FastNumber(y)); + } + + /** + * May be used to dynamically resize the series. This is a relatively slow operation, especially + * as size increases so care should be taken to avoid unnecessary usage. + * @param size + */ + @Override + public void resize(int size) { + resize(xVals, size); + resize(yVals, size); + } + + protected void resize(List list, int size) { + if (size > list.size()) { + while (list.size() < size) { + list.add(null); + } + } else if (size < list.size()) { + while (list.size() > size) { + list.remove(list.size() - 1); + } + } + } + + @Override + public String getTitle() { + return this.title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public int size() { + return xVals.size(); + } + + @Override + public Number getX(int index) { + return xVals.get(index); + } + + @Override + public Number getY(int index) { + return yVals.get(index); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java index 711b8d46..d2fe4c73 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java @@ -21,7 +21,7 @@ import android.util.Log; import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import java.util.List; @@ -46,7 +46,7 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, // get all the data associated with this renderer: - List> sfList = getSeriesAndFormatterList(); + List> sfList = getSeriesAndFormatterList(); // no data to render so exit: if(sfList == null) { @@ -80,6 +80,6 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, * @param sfList * @param stack */ - public abstract void onRender(Canvas canvas, RectF plotArea, List> sfList, int size, RenderStack stack); + public abstract void onRender(Canvas canvas, RectF plotArea, List> sfList, int size, RenderStack stack); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java b/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java new file mode 100644 index 00000000..820f2250 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/LTTBSampler.java @@ -0,0 +1,106 @@ +package com.androidplot.xy; + +import android.util.*; + +/** + * Adapted from: + * https://github.com/drcrane/downsample + * + * Note that this implementation does not yet support null values. + * + * Basic usage example: + *
+ * {@code
+ * // An instance of any implementation of XYSeries; SimpleXYSeries, etc:
+ * XYSeries origalSeries = ...;
+ *
+ * // Sampled series with half the resolution of the
+ * EditableXYSeries sampledSeries = new FixedSizeEditableXYSeries(
+ * origalSeries.getTitle(), origalSeries.size() / 2);
+ *
+ * // does the actual sampling:
+ * new LTTBSampler().run(origalSeries, sampledSeries);
+ * }
+ * 
+ */ +public class LTTBSampler implements Sampler { + + public RectRegion run(XYSeries rawData, EditableXYSeries sampled) { + RectRegion bounds = new RectRegion(); + final int threshold = sampled.size(); + final int dataLength = rawData.size(); + final int startIndex = 0; + + if (threshold >= dataLength || threshold == 0) { + //return data; // Nothing to do + // TODO: set flag to return raw data + throw new RuntimeException("Shouldnt be here!"); + } + + int sampledIndex = 0; + // Bucket size. Leave room for start and end data points + final double bucketSize = (double) (dataLength - 2) / (threshold - 2); + int a = 0; // Initially a is the first point in the triangle + int nextA = 0; + setSample(rawData, sampled, a + startIndex, sampledIndex, bounds); + sampledIndex++; + for (int i = 0; i < threshold - 2; i++) { + // Calculate point average for next bucket (containing c) + double pointCX = 0; + double pointCY = 0; + int pointCStart = (int) Math.floor((i + 1) * bucketSize) + 1; + int pointCEnd = (int) Math.floor((i + 2) * bucketSize) + 1; + pointCEnd = pointCEnd < dataLength ? pointCEnd : dataLength; + final int pointCSize = pointCEnd - pointCStart; + for (; pointCStart < pointCEnd; pointCStart++) { + if(rawData.getX(pointCStart + startIndex) != null) { + pointCX += rawData.getX(pointCStart + startIndex).doubleValue(); + } + + if(rawData.getY(pointCStart + startIndex) != null) { + pointCY += rawData.getY(pointCStart + startIndex).doubleValue(); + } + } + pointCX /= pointCSize; + pointCY /= pointCSize; + double pointAX = rawData.getX(a + startIndex).doubleValue(); + double pointAY = rawData.getY(a + startIndex).doubleValue(); + // Get the range for bucket b + int pointBStart = (int) Math.floor((i + 0) * bucketSize) + 1; + final int pointBEnd = (int) Math.floor((i + 1) * bucketSize) + 1; + double maxArea = -1; + XYCoords maxAreaPoint = null; + for (; pointBStart < pointBEnd; pointBStart++) { + final double area = Math.abs((pointAX - pointCX) * (rawData.getY(pointBStart + startIndex) + .doubleValue() - pointAY) - (pointAX - rawData.getX(pointBStart + startIndex) + .doubleValue()) + * (pointCY - pointAY)) * 0.5; + if (area > maxArea) { + if(rawData.getY(pointBStart + startIndex) == null) { + Log.i("LTTB", "Null value encountered in raw data at index: " + pointBStart); + } + maxArea = area; + maxAreaPoint = new XYCoords(rawData.getX(pointBStart + startIndex), + rawData.getY(pointBStart + startIndex)); + nextA = pointBStart; // Next a is this b + } + } + setSample(sampled, maxAreaPoint.x, maxAreaPoint.y, sampledIndex, bounds); + sampledIndex++; + a = nextA; // This a is the next a (chosen b) + } + setSample(rawData, sampled, (dataLength + startIndex) - 1, sampledIndex, bounds); + sampledIndex++; + return bounds; + } + + protected void setSample(XYSeries raw, EditableXYSeries sampled, int rawIndex, int sampleIndex, RectRegion bounds) { + setSample(sampled, raw.getX(rawIndex), raw.getY(rawIndex), sampleIndex, bounds); + } + + protected void setSample(EditableXYSeries sampled, Number x, Number y, int sampleIndex, RectRegion bounds) { + bounds.union(x, y); + sampled.setX(x, sampleIndex); + sampled.setY(y, sampleIndex); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java index 75d22119..f33501d2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java @@ -129,7 +129,7 @@ public boolean hasLinePaint() { /** * Get the {@link Paint} used to draw lines. Will instantiate and a new default instance - * if it is currently null. To check whether or not line paint has been set, use + * if it is currently null. To run whether or not line paint has been set, use * {@link #hasLinePaint()}. * @return */ @@ -154,7 +154,7 @@ public boolean hasVertexPaint() { /** * Get the {@link Paint} used to draw vertices (points). Will instantiate and a new default instance - * if it is currently null. To check whether or not vertex paint has been set, use + * if it is currently null. To run whether or not vertex paint has been set, use * {@link #hasVertexPaint()}. * @return */ @@ -178,7 +178,7 @@ public boolean hasFillPaint() { } /** * Get the {@link Paint} used to fill series areas. Will instantiate and a new default instance - * if it is currently null. To check whether or not fill paint has been set, use + * if it is currently null. To run whether or not fill paint has been set, use * {@link #hasFillPaint()}. * @return */ diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java index 5f77e0ac..48d397be 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java @@ -16,9 +16,16 @@ package com.androidplot.xy; -import android.graphics.*; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.RectF; + +import com.androidplot.Region; import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; +import com.androidplot.util.*; import java.util.ArrayList; import java.util.List; @@ -32,6 +39,8 @@ public class LineAndPointRenderer e protected static final int ZERO = 0; protected static final int ONE = 1; + private final Path path = new Path(); + public LineAndPointRenderer(XYPlot plot) { super(plot); } @@ -68,23 +77,67 @@ protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) { path.lineTo(thisPoint.x, thisPoint.y); } + final ArrayList points = new ArrayList<>(); + + // avoids needless new allocations of the points array + protected void resizePointsArray(int newSize) { + if(points.size() < newSize) { + while(points.size() < newSize) { + points.add(null); + } + } else if(points.size() > newSize) { + while(points.size() > newSize) { + points.remove(0); + } + } + } protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) { PointF thisPoint; PointF lastPoint = null; PointF firstPoint = null; - //Paint linePaint = formatter.getLinePaint(); - Path path = null; - ArrayList points = new ArrayList<>(series.size()); - for (int i = 0; i < series.size(); i++) { - Number y = series.getY(i); - Number x = series.getX(i); + final int seriesSize = series.size(); + path.reset(); + resizePointsArray(seriesSize); + + int iStart = 0; + int iEnd = seriesSize; + if(SeriesUtils.getXYOrder(series) == OrderedXYSeries.XOrder.ASCENDING) { + final Region iBounds = SeriesUtils.iBounds(series, getPlot().getBounds()); + iStart = iBounds.getMin().intValue(); + if(iStart > 0) { + iStart--; + } + iEnd = iBounds.getMax().intValue(); + if(iEnd < seriesSize - 1) { + iEnd++; + } + } + final double minX = getPlot().getBounds().getMinX().doubleValue(); + final double maxX = getPlot().getBounds().getMaxX().doubleValue(); + for (int i = iStart; i < iEnd; i++) { + final Number y = series.getY(i); + final Number x = series.getX(i); + PointF iPoint = points.get(i); + + final double dx = x.doubleValue(); + if(i > 0 && i < seriesSize - 1) { + if (dx < minX || dx > maxX) { + continue; + } + } if (y != null && x != null) { - thisPoint = getPlot().getBounds().transformScreen(x, y, plotArea); - points.add(thisPoint); + if(iPoint == null) { + iPoint = new PointF(); + points.set(i, iPoint); + } + thisPoint = iPoint; + getPlot().getBounds().transformScreen(thisPoint, x, y, plotArea); } else { thisPoint = null; + iPoint = null; + points.set(i, iPoint); } // don't need to do any of this if the line isnt going to be drawn: @@ -93,7 +146,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn // record the first point of the new Path if (firstPoint == null) { - path = new Path(); + path.reset(); firstPoint = thisPoint; // create our first point at the bottom/x position so filling will look good: @@ -114,6 +167,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn } } } + if(formatter.hasLinePaint()) { if(formatter.getInterpolationParams() != null) { List interpolatedPoints = getInterpolator( @@ -121,7 +175,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn formatter.getInterpolationParams()); firstPoint = convertPoint(interpolatedPoints.get(ZERO), plotArea); lastPoint = convertPoint(interpolatedPoints.get(interpolatedPoints.size()-ONE), plotArea); - path = new Path(); + path.reset(); path.moveTo(firstPoint.x, firstPoint.y); for(int i = 1; i < interpolatedPoints.size(); i++) { thisPoint = convertPoint(interpolatedPoints.get(i), plotArea); @@ -160,17 +214,20 @@ protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, List //PointLabelFormatter plf = formatter.getPointLabelFormatter(); if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) { int i = 0; + final Paint vertexPaint = formatter.hasVertexPaint() ? formatter.getVertexPaint() : null; + final boolean hasPointLabelFormatter = formatter.hasPointLabelFormatter(); + final PointLabelFormatter plf = hasPointLabelFormatter ? formatter.getPointLabelFormatter() : null; + final PointLabeler pointLabeler = hasPointLabelFormatter ? formatter.getPointLabeler() : null; for (PointF p : points) { - PointLabeler pointLabeler = formatter.getPointLabeler(); // if vertexPaint is available, draw vertex: - if (formatter.hasVertexPaint()) { - canvas.drawPoint(p.x, p.y, formatter.getVertexPaint()); + if (vertexPaint != null) { + canvas.drawPoint(p.x, p.y, vertexPaint); } // if textPaint and pointLabeler are available, draw point's text label: - if (formatter.hasPointLabelFormatter() && pointLabeler != null) { - final PointLabelFormatter plf = formatter.getPointLabelFormatter(); + if (pointLabeler != null) { + //final PointLabelFormatter plf = formatter.getPointLabelFormatter(); canvas.drawText(pointLabeler.getLabel(series, i), p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint()); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java new file mode 100644 index 00000000..659e38ea --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/OrderedXYSeries.java @@ -0,0 +1,33 @@ +package com.androidplot.xy; + +/** + * An implementation of {@link XYSeries} that gives hints to it's renderer about the order + * of the data being rendered. + */ +public interface OrderedXYSeries extends XYSeries { + + enum XOrder { + /** + * XVals are in strict ascending order such that: + * x(i) > x(i+1) == true + */ + ASCENDING, + + /** + * XVals are in strict descending order such that: + * x(i) < x(i+1) == true + */ + DESCENDING, + + /** + * XVals appear in no particular order. + */ + NONE + } + + /** + * The order of XVals as they appear in this series. + * @return + */ + XOrder getXOrder(); +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java index ff9435f2..441edb6f 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java @@ -1,18 +1,26 @@ package com.androidplot.xy; -import android.graphics.*; +import android.graphics.RectF; +import android.graphics.PointF; import android.view.*; +import com.androidplot.*; +import com.androidplot.util.*; + import java.util.*; /** * Enables basic pan/zoom touch behavior for an {@link XYPlot}. + * By default boundaries there are no boundaries imposed on scrolling and zooming. You can provide these boundaries + * on your {@link XYPlot} using {@link XYPlot#getOuterLimits()}. * TODO: zoom using dynamic center point * TODO: stretch both mode */ public class PanZoom implements View.OnTouchListener { protected static final float MIN_DIST_2_FING = 5f; + protected static final int FIRST_FINGER = 0; + protected static final int SECOND_FINGER = 1; private XYPlot plot; private Pan pan; @@ -20,36 +28,33 @@ public class PanZoom implements View.OnTouchListener { private boolean isEnabled = true; private DragState dragState = DragState.NONE; - private float minXLimit = Float.MAX_VALUE; - private float maxXLimit = Float.MAX_VALUE; - private float minYLimit = Float.MAX_VALUE; - private float maxYLimit = Float.MAX_VALUE; - private float lastMinX = Float.MAX_VALUE; - private float lastMaxX = Float.MAX_VALUE; - private float lastMinY = Float.MAX_VALUE; - private float lastMaxY = Float.MAX_VALUE; private PointF firstFingerPos; // rectangle created by the space between two fingers - private RectF dist; - private boolean mCalledBySelf; + protected RectF fingersRect; private View.OnTouchListener delegate; // Definition of the touch states - protected enum DragState - { + protected enum DragState { NONE, ONE_FINGER, TWO_FINGERS } public enum Pan { + NONE, HORIZONTAL, VERTICAL, BOTH } public enum Zoom { + + /** + * Comletely disable panning + */ + NONE, + /** * Zoom on the horizontal axis only */ @@ -103,55 +108,13 @@ public void setEnabled(boolean enabled) { isEnabled = enabled; } - protected void setDomainBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, - final Number upperBoundary, final BoundaryMode upperBoundaryMode) { - plot.setDomainBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode); - if(mCalledBySelf) { - mCalledBySelf = false; - } else { - final RectRegion bounds = plot.getBounds(); - minXLimit = lowerBoundaryMode == BoundaryMode.FIXED ? - lowerBoundary.floatValue() : bounds.getMinX().floatValue(); - maxXLimit = upperBoundaryMode == BoundaryMode.FIXED ? - upperBoundary.floatValue() : bounds.getMaxX().floatValue(); - lastMinX = minXLimit; - lastMaxX = maxXLimit; - } - } - - protected void setRangeBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, - final Number upperBoundary, final BoundaryMode upperBoundaryMode) { - plot.setRangeBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode); - if(mCalledBySelf) { - mCalledBySelf = false; - } else { - final RectRegion bounds = plot.getBounds(); - minYLimit = lowerBoundaryMode == BoundaryMode.FIXED ? - lowerBoundary.floatValue() : bounds.getMinY().floatValue(); - maxYLimit = upperBoundaryMode == BoundaryMode.FIXED ? - upperBoundary.floatValue() : bounds.getMaxY().floatValue(); - lastMinY = minYLimit; - lastMaxY = maxYLimit; - } - } - - protected void setDomainBoundaries(final Number lowerBoundary, -final Number upperBoundary, final BoundaryMode mode) { - plot.setDomainBoundaries(lowerBoundary, mode, upperBoundary, mode); - } - - protected synchronized void setRangeBoundaries(final Number lowerBoundary, - final Number upperBoundary, final BoundaryMode mode) { - plot.setRangeBoundaries(lowerBoundary, mode, upperBoundary, mode); - } - @Override public boolean onTouch(final View view, final MotionEvent event) { boolean isConsumed = false; - if(delegate != null) { + if (delegate != null) { isConsumed = delegate.onTouch(view, event); } - if(isEnabled() && !isConsumed) { + if (isEnabled() && !isConsumed) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: // start gesture firstFingerPos = new PointF(event.getX(), event.getY()); @@ -159,9 +122,9 @@ public boolean onTouch(final View view, final MotionEvent event) { break; case MotionEvent.ACTION_POINTER_DOWN: // second finger { - dist = getDistance(event); - // the distance check is done to avoid false alarms - if (dist.width() > MIN_DIST_2_FING || dist.width() < -MIN_DIST_2_FING) { + setFingersRect(fingerDistance(event)); + // the distance run is done to avoid false alarms + if (getFingersRect().width() > MIN_DIST_2_FING || getFingersRect().width() < -MIN_DIST_2_FING) { dragState = DragState.TWO_FINGERS; } break; @@ -169,6 +132,7 @@ public boolean onTouch(final View view, final MotionEvent event) { case MotionEvent.ACTION_POINTER_UP: // end zoom dragState = DragState.NONE; break; + case MotionEvent.ACTION_MOVE: if (dragState == DragState.ONE_FINGER) { pan(event); @@ -176,6 +140,10 @@ public boolean onTouch(final View view, final MotionEvent event) { zoom(event); } break; + + case MotionEvent.ACTION_UP: + reset(); + break; } } // we're forced to consume the event here as not consuming it will prevent future calls: @@ -184,262 +152,210 @@ public boolean onTouch(final View view, final MotionEvent event) { /** * Calculates the distance between two finger motion events. - * @param evt + * @param firstFingerX + * @param firstFingerY + * @param secondFingerX + * @param secondFingerY * @return */ - protected RectF getDistance(final MotionEvent evt) { - float left; - float right; - float top; - float bottom; - if(evt.getX(0) > evt.getX(1)) { - left = evt.getX(1); - right = evt.getX(0); - } else { - left = evt.getX(0); - right = evt.getX(1); - } - - if(evt.getY(0) > evt.getY(1)) { - top = evt.getY(1); - bottom = evt.getY(0); - } else { - top = evt.getY(0); - bottom = evt.getY(1); - } - + protected RectF fingerDistance(float firstFingerX, float firstFingerY, float secondFingerX, float secondFingerY) { + final float left = firstFingerX > secondFingerX ? secondFingerX : firstFingerX; + final float right = firstFingerX > secondFingerX ? firstFingerX : secondFingerX; + final float top = firstFingerY > secondFingerY ? secondFingerY : firstFingerY; + final float bottom = firstFingerY > secondFingerY ? firstFingerY : secondFingerY; return new RectF(left, top, right, bottom); } - private float getMinXLimit() { - if(minXLimit == Float.MAX_VALUE) { - minXLimit = plot.getBounds().getMinX().floatValue(); - lastMinX = minXLimit; - } - return minXLimit; - } - - protected float getMaxXLimit() { - if(maxXLimit == Float.MAX_VALUE) { - maxXLimit = plot.getBounds().getMaxX().floatValue(); - lastMaxX = maxXLimit; - } - return maxXLimit; - } - - protected float getMinYLimit() { - if(minYLimit == Float.MAX_VALUE) { - minYLimit = plot.getBounds().getMinY().floatValue(); - lastMinY = minYLimit; - } - return minYLimit; - } - - protected float getMaxYLimit() { - if(maxYLimit == Float.MAX_VALUE) { - maxYLimit = plot.getBounds().getMaxY().floatValue(); - lastMaxY = maxYLimit; - } - return maxYLimit; - } - - protected float getLastMinX() { - if(lastMinX == Float.MAX_VALUE) { - lastMinX = plot.getBounds().getMinX().floatValue(); - } - return lastMinX; - } - - protected float getLastMaxX() { - if(lastMaxX == Float.MAX_VALUE) { - lastMaxX = plot.getBounds().getMaxX().floatValue(); - } - return lastMaxX; - } - - protected float getLastMinY() { - if(lastMinY == Float.MAX_VALUE) { - lastMinY = plot.getBounds().getMinY().floatValue(); - } - return lastMinY; + /** + * Calculates the distance between two finger motion events. + * @param evt + * @return + */ + protected RectF fingerDistance(final MotionEvent evt) { + return fingerDistance( + evt.getX(FIRST_FINGER), + evt.getY(FIRST_FINGER), + evt.getX(SECOND_FINGER), + evt.getY(SECOND_FINGER)); } - private float getLastMaxY() { - if(lastMaxY == Float.MAX_VALUE) { - lastMaxY = plot.getBounds().getMaxY().floatValue(); + protected void pan(final MotionEvent motionEvent) { + if (pan == Pan.NONE) { + return; } - return lastMaxY; - } - protected void pan(final MotionEvent motionEvent) { final PointF oldFirstFinger = firstFingerPos; //save old position of finger firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position - PointF newX = new PointF(); - if(EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) { - calculatePan(oldFirstFinger, newX, true); - mCalledBySelf = true; - setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED); - lastMinX = newX.x; - lastMaxX = newX.y; + Region newBounds = new Region(); + if (EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) { + calculatePan(oldFirstFinger, newBounds, true); + plot.setDomainBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); } - if(EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) { - calculatePan(oldFirstFinger, newX, false); - mCalledBySelf = true; - setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED); - lastMinY = newX.x; - lastMaxY = newX.y; + if (EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) { + calculatePan(oldFirstFinger, newBounds, false); + plot.setRangeBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); } + plot.redraw(); } - protected void calculatePan(final PointF oldFirstFinger, PointF newX, final boolean horizontal) { + protected void calculatePan(final PointF oldFirstFinger, Region bounds, final boolean horizontal) { final float offset; // multiply the absolute finger movement for a factor. // the factor is dependent on the calculated min and max - if(horizontal) { - newX.x = getLastMinX(); - newX.y = getLastMaxX(); - offset = (oldFirstFinger.x - firstFingerPos.x) * ((newX.y - newX.x) / plot.getWidth()); + if (horizontal) { + bounds.setMinMax(plot.getBounds().getxRegion()); + offset = (oldFirstFinger.x - firstFingerPos.x) * + ((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getWidth()); } else { - newX.x = getLastMinY(); - newX.y = getLastMaxY(); - offset = -(oldFirstFinger.y - firstFingerPos.y) * ((newX.y - newX.x) / plot.getHeight()); + bounds.setMinMax(plot.getBounds().getyRegion()); + offset = -(oldFirstFinger.y - firstFingerPos.y) * + ((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getHeight()); } // move the calculated offset - newX.x = newX.x + offset; - newX.y = newX.y + offset; + bounds.setMin(bounds.getMin().floatValue() + offset); + bounds.setMax(bounds.getMax().floatValue() + offset); //get the distance between max and min - final float diff = newX.y - newX.x; + final float diff = bounds.length().floatValue(); - //check if we reached the limit of panning - if(horizontal) { - if(newX.x < getMinXLimit()) { - newX.x = getMinXLimit(); - newX.y = newX.x + diff; + //run if we reached the limit of panning + if (horizontal && plot.getOuterLimits().getxRegion().isDefined()) { + if (bounds.getMin().floatValue() < plot.getOuterLimits().getMinX().floatValue()) { + bounds.setMin(plot.getOuterLimits().getMinX()); + bounds.setMax(bounds.getMin().floatValue() + diff); } - if(newX.y > getMaxXLimit()) { - newX.y = getMaxXLimit(); - newX.x = newX.y - diff; + if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxX().floatValue()) { + bounds.setMax(plot.getOuterLimits().getMaxX()); + bounds.setMin(bounds.getMax().floatValue() - diff); } - } else { - if(newX.x < getMinYLimit()) { - newX.x = getMinYLimit(); - newX.y = newX.x + diff; + } else if(plot.getOuterLimits().getyRegion().isDefined()) { + if (bounds.getMin().floatValue() < plot.getOuterLimits().getMinY().floatValue()) { + bounds.setMin(plot.getOuterLimits().getMinY()); + bounds.setMax(bounds.getMin().floatValue() + diff); } - if(newX.y > getMaxYLimit()) { - newX.y = getMaxYLimit(); - newX.x = newX.y - diff; + if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxY().floatValue()) { + bounds.setMax(plot.getOuterLimits().getMaxY()); + bounds.setMin(bounds.getMax().floatValue() - diff); } } } protected boolean isValidScale(float scale) { - if(Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) { + if (Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) { return false; } return true; } protected void zoom(final MotionEvent motionEvent) { - final RectF oldDist = dist; - final RectF newDist = getDistance(motionEvent); - dist = newDist; + if (zoom == Zoom.NONE) { + return; + } + final RectF oldFingersRect = getFingersRect(); + final RectF newFingersRect = fingerDistance(motionEvent); + setFingersRect(newFingersRect); + if(oldFingersRect == null || RectFUtils.areIdentical(oldFingersRect, newFingersRect)) { + // zooming gesture has not happened yet so skip: + return; + } RectF newRect = new RectF(); float scaleX = 1; float scaleY = 1; - switch(zoom) { + switch (zoom) { case STRETCH_HORIZONTAL: - scaleX = oldDist.width() / dist.width(); - if(!isValidScale(scaleX)) { + scaleX = oldFingersRect.width() / getFingersRect().width(); + if (!isValidScale(scaleX)) { return; } break; case STRETCH_VERTICAL: - scaleY = oldDist.height() / dist.height(); - if(!isValidScale(scaleY)) { + scaleY = oldFingersRect.height() / getFingersRect().height(); + if (!isValidScale(scaleY)) { return; } break; case STRETCH_BOTH: - scaleX = oldDist.width() / dist.width(); - scaleY = oldDist.height() / dist.height(); - if(!isValidScale(scaleX) || !isValidScale(scaleY)) { + scaleX = oldFingersRect.width() / getFingersRect().width(); + scaleY = oldFingersRect.height() / getFingersRect().height(); + if (!isValidScale(scaleX) || !isValidScale(scaleY)) { return; } break; case SCALE: - scaleX = oldDist.width() / dist.width(); - scaleY = oldDist.height() / dist.height(); - - // use the greater value to scale each axis evenly: - if(scaleX > scaleY) { - scaleY = scaleX; - } else { - scaleX = scaleY; - } - if(!isValidScale(scaleX) || !isValidScale(scaleY)) { + float sc1 = (float) Math.hypot(oldFingersRect.height(), oldFingersRect.width()); + float sc2 = (float) Math.hypot(getFingersRect().height(), getFingersRect().width()); + float sc = sc1 / sc2; + scaleX = sc; + scaleY = sc; + if (!isValidScale(scaleX) || !isValidScale(scaleY)) { return; } break; } - if(EnumSet.of( + if (EnumSet.of( Zoom.STRETCH_HORIZONTAL, Zoom.STRETCH_BOTH, Zoom.SCALE).contains(zoom)) { calculateZoom(newRect, scaleX, true); - mCalledBySelf = true; - setDomainBoundaries(newRect.left, newRect.right, BoundaryMode.FIXED); - lastMinX = newRect.left; - lastMaxX = newRect.right; + plot.setDomainBoundaries(newRect.left, newRect.right, BoundaryMode.FIXED); } - if(EnumSet.of( + if (EnumSet.of( Zoom.STRETCH_VERTICAL, Zoom.STRETCH_BOTH, Zoom.SCALE).contains(zoom)) { calculateZoom(newRect, scaleY, false); - mCalledBySelf = true; - setRangeBoundaries(newRect.top, newRect.bottom, BoundaryMode.FIXED); - lastMinY = newRect.top; - lastMaxY = newRect.bottom; + plot.setRangeBoundaries(newRect.top, newRect.bottom, BoundaryMode.FIXED); } plot.redraw(); } + /** + * + * @param newRect RectF into which zoom calculation results should be placed. + * @param scale + * @param isHorizontal + */ protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) { - final float calcMax; final float span; - if(isHorizontal) { - calcMax = getLastMaxX(); - span = calcMax - getLastMinX(); + final RectRegion bounds = plot.getBounds(); + if (isHorizontal) { + calcMax = bounds.getMaxX().floatValue(); + span = calcMax - bounds.getMinX().floatValue(); } else { - calcMax = getLastMaxY(); - span = calcMax - getLastMinY(); + calcMax = bounds.getMaxY().floatValue(); + span = calcMax - bounds.getMinY().floatValue(); } final float midPoint = calcMax - (span / 2.0f); final float offset = span * scale / 2.0f; - if(isHorizontal) { - newRect.left = midPoint - offset; - newRect.right = midPoint + offset; - if(newRect.left < getMinXLimit()) { - newRect.left = getMinXLimit(); - } - if(newRect.right > getMaxXLimit()) { - newRect.right = getMaxXLimit(); + if (isHorizontal ) { + final RectRegion limits = plot.getOuterLimits(); + if(limits.isFullyDefined()) { + newRect.left = midPoint - offset; + newRect.right = midPoint + offset; + if (newRect.left < limits.getMinX().floatValue()) { + newRect.left = limits.getMinX().floatValue(); + } + if (newRect.right > limits.getMaxX().floatValue()) { + newRect.right = limits.getMaxX().floatValue(); + } } } else { - newRect.top = midPoint - offset; - newRect.bottom = midPoint + offset; - if(newRect.top < getMinYLimit()) { - newRect.top = getMinYLimit(); - } - if(newRect.bottom > getMaxYLimit()) { - newRect.bottom = getMaxYLimit(); + final RectRegion limits = plot.getOuterLimits(); + if(limits.isFullyDefined()) { + newRect.top = midPoint - offset; + newRect.bottom = midPoint + offset; + if (newRect.top < limits.getMinY().floatValue()) { + newRect.top = limits.getMinY().floatValue(); + } + if (newRect.bottom > limits.getMaxY().floatValue()) { + newRect.bottom = limits.getMaxY().floatValue(); + } } } } @@ -474,4 +390,18 @@ public View.OnTouchListener getDelegate() { public void setDelegate(View.OnTouchListener delegate) { this.delegate = delegate; } + + public void reset() { + this.firstFingerPos = null; + setFingersRect(null); + this.setFingersRect(null); + } + + protected RectF getFingersRect() { + return fingersRect; + } + + protected void setFingersRect(RectF fingersRect) { + this.fingersRect = fingersRect; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java index e7a4b09e..c2182691 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java @@ -118,10 +118,19 @@ public PointF transformScreen(Number x, Number y, RectF region2) { return transform(x, y, region2, false, true); } + public void transformScreen(PointF result, Number x, Number y, RectF region2) { + transform(result, x, y, region2, false, true); + } + + public void transform(PointF result, Number x, Number y, RectF region2, boolean flipX, boolean flipY) { + result.x = (float) xRegion.transform(x.doubleValue(), region2.left, region2.right, flipX); + result.y = (float) yRegion.transform(y.doubleValue(), region2.top, region2.bottom, flipY); + } + public PointF transform(Number x, Number y, RectF region2, boolean flipX, boolean flipY) { - float xx = (float) xRegion.transform(x.doubleValue(), region2.left, region2.right, flipX); - float yy = (float) yRegion.transform(y.doubleValue(), region2.top, region2.bottom, flipY); - return new PointF(xx, yy); + PointF result = new PointF(); + transform(result, x, y, region2, flipX, flipY); + return result; } public PointF transformScreen(XYCoords value, RectF region2) { @@ -141,6 +150,11 @@ public PointF transform(XYCoords value, RectF region2, boolean flipX, boolean fl return transform(value.x, value.y, region2, flipX, flipY); } + public void union(Number x, Number y) { + xRegion.union(x); + yRegion.union(y); + } + /** * Compares the input bounds xy min/max vals against this instance's current xy min/max vals. * If the input.min is less than this.min then this.min will be set to input.min. @@ -234,6 +248,13 @@ private Number distanceBetween(Number x, Number y) { return Math.abs(x.doubleValue() - y.doubleValue()); } + public void set(Number minX, Number maxX, Number minY, Number maxY) { + setMinX(minX); + setMaxX(maxX); + setMinY(minY); + setMaxY(maxY); + } + public boolean isMinXSet() { return xRegion.isMinSet(); } @@ -313,4 +334,49 @@ public void setyRegion(Region yRegion) { public boolean isFullyDefined() { return xRegion.isDefined() && yRegion.isDefined(); } + + /** + * True if this region contains the specified coordinates. + * @param x + * @param y + * @return + */ + public boolean contains(Number x, Number y) { + return getxRegion().contains(x) && getyRegion().contains(y); + } + + /** + * Checks to see whether the specified line. Note that this implementation will return + * true even if the line is completely enclosed by this RectRegion. + * WARNING: this implementation has problems. See associated unit test for details. + * @param x1 x-coord of the line beginning + * @param y1 y-coord of the line beginning + * @param x2 x-coord of the line end + * @param y2 y-coord of the line end + * @return True if this RectRegion overlaps any part of the specified line. + */ + public boolean intersectsWithLine(Number x1, Number y1, Number x2, Number y2) { + if(contains(x1, y1) || contains(x2, y2)) { + return true; + } + + // if true, it means that these points exist on different sides of the rect's edges + final boolean x1MinRelation = x1.doubleValue() < getMinX().doubleValue(); + final boolean x2MinRelation = x2.doubleValue() < getMinX().doubleValue(); + final boolean xMinRelation = x1MinRelation &! x2MinRelation; + + final boolean x1MaxRelation = x1.doubleValue() < getMaxX().doubleValue(); + final boolean x2MaxRelation = x2.doubleValue() < getMaxX().doubleValue(); + final boolean xMaxRelation = x1MaxRelation &! x2MaxRelation; + + final boolean y1MinRelation = y1.doubleValue() < getMinY().doubleValue(); + final boolean y2MinRelation = y2.doubleValue() < getMinY().doubleValue(); + final boolean yMinRelation = y1MinRelation &! y2MinRelation; + + final boolean y1MaxRelation = y1.doubleValue() < getMaxY().doubleValue(); + final boolean y2MaxRelation = y2.doubleValue() < getMaxY().doubleValue(); + final boolean yMaxRelation = y1MaxRelation &! y2MaxRelation; + + return ((xMinRelation | xMaxRelation) || getxRegion().contains(x1) & (yMinRelation | yMaxRelation) || getyRegion().contains(y1)); + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java new file mode 100644 index 00000000..b10903b1 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/SampledXYSeries.java @@ -0,0 +1,216 @@ +package com.androidplot.xy; + +import com.androidplot.*; +import com.androidplot.util.SeriesUtils; + +import java.util.*; + +/** + * An implementation of {@link FastXYSeries} that samples its self into multiple levels to + * achieve faster rendering / zoom behavior. By default, uses {@link LTTBSampler} as + * it's sampling algorithm. Note that this algorithm does not yet support null values. + * + * Sampling behavior is controlled by two values: + * Ratio: A value greater than 1; controls the sampling ratio of each successive series. For example, + * a step of 2 would mean that each successive series contains 2x fewer points than the previous. + * + * Threshold: A value < the original series size; controls the lower limit at which point sampling should stop. + * For example, sampling a series with size 1000 given a ratio of 2 and a threshold of 100, + * three sampled resolutions will be generated: + * + * 500 - 2x sampling + * 250 - 4x sampling + * 125 - 8x sampling + * + */ +public class SampledXYSeries implements FastXYSeries, OrderedXYSeries { + private int threshold; + private Sampler algorithm = new LTTBSampler(); + + private XYSeries rawData; + + private List zoomLevels; + + private XYSeries activeSeries; + + private RectRegion bounds; + private Exception lastResamplingException; + + private final XOrder xOrder; + private float ratio; + + /** + * + * @param rawData + * @param xOrder If your data is in ascending or descending order, specifying it here speed up + * optimize render times. + * @param ratio The ratio used to determine the size of each new sampled series. Must be > 1. + * downsampled series until threshold is reached. + * @param threshold The desired size of the smallest sample series. Must be < rawData.size. + */ + public SampledXYSeries(XYSeries rawData, XOrder xOrder, float ratio, int threshold) { + this.rawData = rawData; + this.xOrder = xOrder; + this.setRatio(ratio); + this.setThreshold(threshold); + resample(); + } + + /** + * Generate a SampledXYSeries from the input series. + * @param rawData The original series to be downsampled + * @param ratio The ratio used to determine the size of each new sampled series. Must be > 1. + * downsampled series until threshold is reached. + * @param threshold The desired size of the smallest sample series. Must be < rawData.size. + */ + public SampledXYSeries(XYSeries rawData, float ratio, int threshold) { + this(rawData, SeriesUtils.getXYOrder(rawData), ratio, threshold); + } + + public void resample() { + bounds = null; + zoomLevels = new ArrayList<>(); + int t = (int) Math.ceil(rawData.size() / getRatio()); + List threads = new ArrayList<>(zoomLevels.size()); + while (t > threshold) { + final int thisThreshold = t; + final EditableXYSeries thisSeries = new FixedSizeEditableXYSeries(getTitle(), thisThreshold); + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + try { + // TODO: make bounds a param to prevent calculating on each setZoomFactor level: + RectRegion b = getAlgorithm() + .run(rawData, thisSeries); + if (bounds == null) { + bounds = b; + } + } catch(Exception ex) { + lastResamplingException = ex; + } + } + }); + getZoomLevels().add(thisSeries); + threads.add(thread); + thread.start(); + + t = (int) Math.ceil(t / getRatio()); + } + for(Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + if(lastResamplingException != null) { + throw new RuntimeException("Exception encountered during resampling", lastResamplingException); + } + + } + + protected List getZoomLevels() { + return this.zoomLevels; + } + + /** + * Set zoom factor; 2.5 = 2.5x zoom, 10.0 = 10x zoom etc. This method will set the zoom level + * to the closest available factor to the specified factor; a specified factor of 4.5x may result + * in an actual factor of 4x. + * @param factor + */ + public void setZoomFactor(double factor) { + if(factor <= 1) { + activeSeries = rawData; + } else { + //int i = (int) Math.round(Math.sqrt(factor) - 1); + int i = getZoomIndex(factor, getRatio()); + if (i < zoomLevels.size()) { + activeSeries = zoomLevels.get(i); + } else { + activeSeries = zoomLevels.get(zoomLevels.size() - 1); + } + } + } + + protected static int getZoomIndex(double zoomFactor, double ratio) { + final double lhs = Math.log(zoomFactor); + final double rhs = Math.log(ratio); + final double log = lhs / rhs; + final int index = (int) Math.round(log); + return index > 0 ? index - 1 : 0; + } + + public double getMaxZoomFactor() { + return Math.pow(getRatio(), zoomLevels.size()); + } + + public Sampler getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(Sampler algorithm) { + this.algorithm = algorithm; + resample(); + } + + @Override + public String getTitle() { + return rawData.getTitle(); + } + + @Override + public int size() { + return activeSeries.size(); + } + + @Override + public Number getX(int index) { + return activeSeries.getX(index); + } + + @Override + public Number getY(int index) { + return activeSeries.getY(index); + } + + public int getThreshold() { + return threshold; + } + + public void setThreshold(int threshold) { + if(threshold >= rawData.size()) { + throw new IllegalArgumentException("Threshold must be < original series size."); + } + this.threshold = threshold; + } + + public RectRegion getBounds() { + return bounds; + } + + public void setBounds(RectRegion bounds) { + this.bounds = bounds; + } + + @Override + public RectRegion minMax() { + return bounds; + } + + @Override + public XOrder getXOrder() { + return xOrder; + } + + public float getRatio() { + return ratio; + } + + public void setRatio(float ratio) { + if(ratio <= 1) { + throw new IllegalArgumentException("Ratio must be greater than 1"); + } + this.ratio = ratio; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java b/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java new file mode 100644 index 00000000..ebbc8238 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/Sampler.java @@ -0,0 +1,16 @@ +package com.androidplot.xy; + +/** + * An algorithm used to to resample a larger set of data into a smaller set. + */ +public interface Sampler { + + /** + * + * @param input The original unsampled series + * @param output The destination series to contain sampled result. + * This series size should be set to the desired sampled size. + * @return min/max values encountered while processing input. + */ + RectRegion run(XYSeries input, EditableXYSeries output); +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java index 1763bc20..2032f301 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java @@ -19,6 +19,7 @@ import android.graphics.Canvas; import com.androidplot.Plot; import com.androidplot.PlotListener; +import com.androidplot.util.*; import java.util.*; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -27,7 +28,7 @@ /** * A convenience class used to create instances of XYPlot generated from Lists of Numbers. */ -public class SimpleXYSeries implements XYSeries, PlotListener { +public class SimpleXYSeries implements EditableXYSeries, PlotListener { private static final String TAG = SimpleXYSeries.class.getName(); @@ -46,11 +47,11 @@ public enum ArrayFormat { XY_VALS_INTERLEAVED } - private volatile LinkedList xVals = new LinkedList(); - private volatile LinkedList yVals = new LinkedList(); + private volatile LinkedList xVals = new LinkedList<>(); + private volatile LinkedList yVals = new LinkedList<>(); private volatile String title = null; - private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); public SimpleXYSeries(String title) { this.title = title; @@ -188,6 +189,27 @@ public void setY(Number value, int index) { } } + @Override + public void resize(int size) { + try { + lock.writeLock().lock(); + if (xVals.size() < size) { + + for (int i = xVals.size(); i < size; i++) { + xVals.add(null); + yVals.add(null); + } + } else if(xVals.size() > size) { + for(int i = xVals.size(); i > size; i--) { + xVals.removeLast(); + yVals.removeLast(); + } + } + } finally { + lock.writeLock().unlock(); + } + } + /** * Sets xy values based on index * @param xVal diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java index cb1feb79..e5b981c5 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java @@ -18,7 +18,7 @@ import android.graphics.*; import com.androidplot.ui.LayoutManager; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.Size; import com.androidplot.ui.TableModel; import com.androidplot.ui.widget.Widget; @@ -149,17 +149,17 @@ private void drawSeriesLegendCell(Canvas canvas, XYSeriesRenderer renderer, XYSe finishDrawingCell(canvas, cellRect, iconRect, seriesTitle); } - 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; - } +// 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) { @@ -171,7 +171,7 @@ protected synchronized void doOnDraw(Canvas canvas, RectF widgetRect) { TreeSet> sortedRegions = new TreeSet>(new RegionEntryComparator()); // Calculate the number of cells needed to draw the Legend: - int seriesCount = plot.getSeriesRegistry().size(); + int seriesCount = plot.getRegistry().size(); for(XYSeriesRenderer renderer : plot.getRendererList()) { Hashtable urf = renderer.getUniqueRegionFormatters(); @@ -186,7 +186,7 @@ protected synchronized void doOnDraw(Canvas canvas, RectF widgetRect) { RectF cellRect; // draw each series legend item: - for(SeriesAndFormatter sfPair : getLegendEnabledSeriesAndFormatterList()) { + for(SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) { //for(SeriesAndFormatter sfPair : plot.getSeriesRegistry()) { cellRect = it.next(); XYSeriesFormatter format = sfPair.getFormatter(); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java index 4b709de0..5563f29b 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -23,8 +23,8 @@ import android.graphics.Paint; import android.graphics.PointF; import android.util.AttributeSet; -import com.androidplot.Plot; -import com.androidplot.R; + +import com.androidplot.*; import com.androidplot.ui.*; import com.androidplot.ui.TextOrientation; import com.androidplot.ui.widget.TextLabelWidget; @@ -39,7 +39,7 @@ /** * A View to graphically display x/y coordinates. */ -public class XYPlot extends Plot { +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; @@ -83,11 +83,7 @@ public class XYPlot extends Plot private XYConstraints constraints = new XYConstraints(); - // these are the final min/max used for dispplaying data -// private Number calculatedMinX; -// private Number calculatedMaxX; -// private Number calculatedMinY; -// private Number calculatedMaxY; + // min/max used for displaying data private RectRegion bounds = RectRegion.withDefaults(new RectRegion(-1, 1, -1, 1)); // previous calculated min/max vals. @@ -97,16 +93,8 @@ public class XYPlot extends Plot private Number prevMinY; private Number prevMaxY; - // uses set boundary min and max values - // should be null if not used. - private Number rangeTopMin = null; - private Number rangeTopMax = null; - private Number rangeBottomMin = null; - private Number rangeBottomMax = null; - private Number domainLeftMin = null; - private Number domainLeftMax = null; - private Number domainRightMin = null; - private Number domainRightMax = null; + private final RectRegion innerLimits = new RectRegion(); + private final RectRegion outerLimits = new RectRegion(); private Number userDomainOrigin; private Number userRangeOrigin; @@ -123,6 +111,7 @@ public class XYPlot extends Plot private ArrayList xValueMarkers; private PreviewMode previewMode; + public enum PreviewMode { LineAndPoint, Candlestick, @@ -330,10 +319,12 @@ protected void processAttrs(TypedArray attrs) { protected void notifyListenersBeforeDraw(Canvas canvas) { super.notifyListenersBeforeDraw(canvas); + calculateMinMaxVals(); + // this call must be AFTER the notify so that if the listener // is a synchronized series, it has the opportunity to // place a read lock on it's data. - calculateMinMaxVals(); + getRegistry().estimate(this); // TODO: clean this mechanism up!!! } /** @@ -365,6 +356,14 @@ public void setCursorPosition(float x, float y) { getGraph().setCursorPosition(x, y); } + public Number getXVal(float xPix) { + return getGraph().getXVal(xPix); + } + + public Number getYVal(float yPix) { + return getGraph().getYVal(yPix); + } + public Number getYVal(PointF point) { return getGraph().getYVal(point); } @@ -387,7 +386,7 @@ public void calculateMinMaxVals() { // only calculate if we must: if(!bounds.isFullyDefined()) { - RectRegion b = SeriesUtils.minMax(constraints, getSeriesRegistry().getSeriesList()); + RectRegion b = SeriesUtils.minMax(constraints, getRegistry().getSeriesList()); if(!bounds.isMinXSet()) { bounds.setMinX(b.getMinX()); @@ -414,11 +413,11 @@ public void calculateMinMaxVals() { case EDGE: bounds.setMaxX(applyUserMinMax(getCalculatedUpperBoundary( constraints.getDomainUpperBoundaryMode(), prevMaxX, bounds.getMaxX()), - domainRightMin, domainRightMax)); + innerLimits.getMaxX(), outerLimits.getMaxX())); bounds.setMinX(applyUserMinMax(getCalculatedLowerBoundary( constraints.getDomainLowerBoundaryMode(), prevMinX, bounds.getMinX()), - domainLeftMin, domainLeftMax)); + outerLimits.getMinX(), innerLimits.getMinX())); break; default: throw new UnsupportedOperationException( @@ -430,13 +429,13 @@ public void calculateMinMaxVals() { updateRangeMinMaxForOriginModel(); break; case EDGE: - if (getSeriesRegistry().size() > 0) { + if (getRegistry().size() > 0) { bounds.setMaxY(applyUserMinMax(getCalculatedUpperBoundary( constraints.getRangeUpperBoundaryMode(), - prevMaxY, bounds.getMaxY()), rangeTopMin, rangeTopMax)); + prevMaxY, bounds.getMaxY()), innerLimits.getMaxY(), outerLimits.getMaxY())); bounds.setMinY(applyUserMinMax(getCalculatedLowerBoundary( constraints.getRangeLowerBoundaryMode(), - prevMinY, bounds.getMinY()), rangeBottomMin, rangeBottomMax)); + prevMinY, bounds.getMinY()), outerLimits.getMinY(), innerLimits.getMinY())); } break; default: @@ -1073,124 +1072,12 @@ protected List getXValueMarkers() { return xValueMarkers; } -// public RectRegion getDefaultBounds() { -// return defaultBounds; -// } -// -// public void setDefaultBounds(RectRegion defaultBounds) { -// this.defaultBounds = defaultBounds; -// } - - /** - * @return the rangeTopMin - */ - public Number getRangeTopMin() { - return rangeTopMin; + public RectRegion getInnerLimits() { + return innerLimits; } - /** - * @param rangeTopMin the rangeTopMin to set - */ - public synchronized void setRangeTopMin(Number rangeTopMin) { - this.rangeTopMin = rangeTopMin; - } - - /** - * @return the rangeTopMax - */ - public Number getRangeTopMax() { - return rangeTopMax; - } - - /** - * @param rangeTopMax the rangeTopMax to set - */ - public synchronized void setRangeTopMax(Number rangeTopMax) { - this.rangeTopMax = rangeTopMax; - } - - /** - * @return the rangeBottomMin - */ - public Number getRangeBottomMin() { - return rangeBottomMin; - } - - /** - * @param rangeBottomMin the rangeBottomMin to set - */ - public synchronized void setRangeBottomMin(Number rangeBottomMin) { - this.rangeBottomMin = rangeBottomMin; - } - - /** - * @return the rangeBottomMax - */ - public Number getRangeBottomMax() { - return rangeBottomMax; - } - - /** - * @param rangeBottomMax the rangeBottomMax to set - */ - public synchronized void setRangeBottomMax(Number rangeBottomMax) { - this.rangeBottomMax = rangeBottomMax; - } - - /** - * @return the domainLeftMin - */ - public Number getDomainLeftMin() { - return domainLeftMin; - } - - /** - * @param domainLeftMin the domainLeftMin to set - */ - public synchronized void setDomainLeftMin(Number domainLeftMin) { - this.domainLeftMin = domainLeftMin; - } - - /** - * @return the domainLeftMax - */ - public Number getDomainLeftMax() { - return domainLeftMax; - } - - /** - * @param domainLeftMax the domainLeftMax to set - */ - public synchronized void setDomainLeftMax(Number domainLeftMax) { - this.domainLeftMax = domainLeftMax; - } - - /** - * @return the domainRightMin - */ - public Number getDomainRightMin() { - return domainRightMin; - } - - /** - * @param domainRightMin the domainRightMin to set - */ - public synchronized void setDomainRightMin(Number domainRightMin) { - this.domainRightMin = domainRightMin; - } - - /** - * @return the domainRightMax - */ - public Number getDomainRightMax() { - return domainRightMax; - } - - /** - * @param domainRightMax the domainRightMax to set - */ - public synchronized void setDomainRightMax(Number domainRightMax) { - this.domainRightMax = domainRightMax; + public RectRegion getOuterLimits() { + return outerLimits; } public StepModel getDomainStepModel() { @@ -1208,4 +1095,10 @@ public StepModel getRangeStepModel() { public void setRangeStepModel(StepModel rangeStepModel) { this.rangeStepModel = rangeStepModel; } + + @Override + protected XYSeriesRegistry getRegistryInstance() { + XYSeriesRegistry registry = new XYSeriesRegistry(); + return registry; + } } \ No newline at end of file diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java index 8eb2d226..5afb8963 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeries.java @@ -47,5 +47,4 @@ public interface XYSeries extends Series { * @return The y-value. */ Number getY(int index); - } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java new file mode 100644 index 00000000..fce15b59 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesBundle.java @@ -0,0 +1,13 @@ +package com.androidplot.xy; + +import com.androidplot.ui.*; + +/** + * Created by halfhp on 10/6/16. + */ +public class XYSeriesBundle extends SeriesBundle { + + public XYSeriesBundle(XYSeries series, XYSeriesFormatter formatter) { + super(series, formatter); + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRegistry.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRegistry.java new file mode 100644 index 00000000..e7934a5a --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRegistry.java @@ -0,0 +1,36 @@ +package com.androidplot.xy; + +import com.androidplot.*; + +/** + * Maintains the "registry" of mappings of XYSeries instances and their associated formatters. + */ +public class XYSeriesRegistry extends SeriesRegistry { + + private Estimator estimator; + + public void estimate(XYPlot plot) { + if(estimator != null) { + for (XYSeriesBundle sf : getSeriesAndFormatterList()) { + getEstimator().run(plot, sf); + } + } + } + + @Override + protected XYSeriesBundle newSeriesBundle(XYSeries series, XYSeriesFormatter formatter) { + return new XYSeriesBundle(series, formatter); + } + + /** + * + * @return The currently active Estimator, or null if none is set. + */ + public Estimator getEstimator() { + return estimator; + } + + public void setEstimator(Estimator estimator) { + this.estimator = estimator; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java index c77832cd..5617d411 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesRenderer.java @@ -16,7 +16,7 @@ package com.androidplot.xy; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.SeriesRenderer; import com.androidplot.util.Layerable; @@ -40,7 +40,7 @@ public XYSeriesRenderer(XYPlot plot) { public Hashtable getUniqueRegionFormatters() { Hashtable found = new Hashtable<>(); - for(SeriesAndFormatter sfPair : getSeriesAndFormatterList()) { + for(SeriesBundle sfPair : getSeriesAndFormatterList()) { Layerable regionIndexer = sfPair.getFormatter().getRegions(); for (RectRegion region : regionIndexer.elements()) { XYRegionFormatter f = sfPair.getFormatter().getRegionFormatter(region); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java b/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java new file mode 100644 index 00000000..14b7583b --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/ZoomEstimator.java @@ -0,0 +1,26 @@ +package com.androidplot.xy; + +/** + * Estimates optimal zoom level to be applied to a {@link SampledXYSeries} based on the current + * visible bounds of the owning {@link XYPlot}. + */ +public class ZoomEstimator extends Estimator { + + @Override + public void run(XYPlot plot, XYSeriesBundle sf) { + if(sf.getSeries() instanceof SampledXYSeries) { + SampledXYSeries oxy = (SampledXYSeries) sf.getSeries(); + final double factor = calculateZoom(oxy, plot.getBounds()); + oxy.setZoomFactor(factor); + } + } + + protected double calculateZoom(SampledXYSeries series, RectRegion visibleBounds) { + RectRegion seriesBounds = series.getBounds(); + final double ratio = seriesBounds.getxRegion().ratio(visibleBounds.getxRegion()).doubleValue(); + final double maxFactor = series.getMaxZoomFactor(); + final double factor = Math.abs(Math.round(maxFactor / ratio)); + return factor > 0 ? factor : 1; + } + +} diff --git a/androidplot-core/src/test/java/com/androidplot/PlotTest.java b/androidplot-core/src/test/java/com/androidplot/PlotTest.java index 789207ea..6c013ea6 100644 --- a/androidplot-core/src/test/java/com/androidplot/PlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/PlotTest.java @@ -22,9 +22,7 @@ import com.androidplot.exception.PlotRenderException; import com.androidplot.test.*; -import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesRenderer; -import com.androidplot.ui.Formatter; +import com.androidplot.ui.*; import com.halfhp.fig.*; import org.junit.Test; import org.robolectric.RuntimeEnvironment; @@ -39,7 +37,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class PlotTest extends AndroidplotTest { @@ -131,11 +128,29 @@ public SeriesRenderer doGetRendererInstance(MockPlot plot) { } } - public static class MockPlot extends Plot { + public static class MockSeriesBundle extends SeriesBundle { + + public MockSeriesBundle(MockSeries series, Formatter formatter) { + super(series, formatter); + } + } + + public static class MockPlot extends Plot> { public MockPlot(String title) { super(RuntimeEnvironment.application, title); } + @Override + protected SeriesRegistry getRegistryInstance() { + return new SeriesRegistry() { + @Override + protected MockSeriesBundle newSeriesBundle( + MockSeries series, Formatter formatter) { + return new MockSeriesBundle(series, formatter); + } + }; + } + @Override protected void onPreInit() { @@ -172,7 +187,7 @@ public void testAddSeries() throws Exception { Class cl = MockRenderer1.class; plot.addSeries(m1, new MockFormatter1()); - assertEquals(1, plot.getSeriesRegistry().size()); + assertEquals(1, plot.getRegistry().size()); // a new copy of m1 is added: plot.addSeries(m1, new MockFormatter1()); @@ -215,7 +230,7 @@ public void testRemoveSeries() throws Exception { plot.addSeries(m3, new MockFormatter2()); - // a quick sanity check: + // a quick sanity run: assertEquals(2, plot.getRendererList().size()); assertEquals(3, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); assertEquals(3, plot.getRenderer(MockRenderer2.class).getSeriesList().size()); @@ -241,7 +256,7 @@ public void testRemoveSeries() throws Exception { plot.addSeries(m3, new MockFormatter1()); - // a quick sanity check: + // a quick sanity run: assertEquals(2, plot.getRendererList().size()); assertEquals(6, plot.getRenderer(MockRenderer1.class).getSeriesList().size()); assertEquals(3, plot.getRenderer(MockRenderer2.class).getSeriesList().size()); diff --git a/androidplot-core/src/test/java/com/androidplot/RegionTest.java b/androidplot-core/src/test/java/com/androidplot/RegionTest.java index 122320ba..1dbc6dc9 100644 --- a/androidplot-core/src/test/java/com/androidplot/RegionTest.java +++ b/androidplot-core/src/test/java/com/androidplot/RegionTest.java @@ -40,16 +40,16 @@ public void tearDown() throws Exception { @Test public void testConstructor() throws Exception { Region lr = new Region(0d, 0d); - assertEquals(0d, lr.getMin()); - assertEquals(0d, lr.getMax()); + assertEquals(0d, lr.getMin().doubleValue(), 0); + assertEquals(0d, lr.getMax().doubleValue(), 0); lr = new Region(1.5d, -2d); - assertEquals(-2d, lr.getMin()); - assertEquals(1.5d, lr.getMax()); + assertEquals(-2f, lr.getMin().floatValue(), 0); + assertEquals(1.5f, lr.getMax().floatValue(), 0); lr = new Region(10d, 20d); - assertEquals(10d, lr.getMin()); - assertEquals(20d, lr.getMax()); + assertEquals(10l, lr.getMin().longValue(), 0); + assertEquals(20l, lr.getMax().longValue(), 0); } @@ -129,6 +129,34 @@ public void testRatio() throws Exception { Region r2 = new Region(0, 100); assertEquals(0.01, r1.ratio(r2)); assertEquals(100.0, r2.ratio(r1)); + + r1 = new Region(0f, 21402646f); + r2 = new Region(0, 999); + + //assertTrue(r2.ratio(r1).doubleValue() > 0); + } + + @Test + public void testRegionRegionUnion() throws Exception { + Region r1 = new Region(1, 2); + Region r2 = new Region(0, 100); + + r1.union(r2); + assertEquals(0, r1.getMin().doubleValue(), 0); + assertEquals(100, r1.getMax().doubleValue(), 0); + } + + @Test + public void testRegionPointUnion() throws Exception { + Region r1 = new Region(1, 2); + + r1.union(0); + assertEquals(0, r1.getMin().doubleValue(), 0); + assertEquals(2, r1.getMax().doubleValue(), 0); + + r1.union(100); + assertEquals(0, r1.getMin().doubleValue(), 0); + assertEquals(100, r1.getMax().doubleValue(), 0); } } diff --git a/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java new file mode 100644 index 00000000..93e9143d --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java @@ -0,0 +1,55 @@ +package com.androidplot.test; + +import android.annotation.*; +import android.view.*; + +import com.androidplot.xy.*; + +import org.robolectric.shadows.*; + +import static org.robolectric.Shadows.shadowOf; + +/** + * Created by halfhp on 10/8/16. + */ +public abstract class TestUtils { + + public static XYSeries generateXYSeries(String title, int size) { + SimpleXYSeries series = new SimpleXYSeries(title); + for(int i = 0; i < size; i++) { + series.addLast(i, Math.random()); + } + return series; + } + + public static XYSeries generateXYSeriesWithNulls(String title, int size) { + SimpleXYSeries series = new SimpleXYSeries(title); + for(int i = 0; i < size; i++) { + if(Math.random() > 0.5) { + series.addLast(i, null); + } else { + series.addLast(i, Math.random()); + } + } + return series; + } + + /** + * Generate a two finger {@link MotionEvent}. + * @param finger1x + * @param finger1y + * @param finger2x + * @param finger2y + * @return + */ + @SuppressLint("NewApi") + public static MotionEvent newPointerDownEvent(int finger1x, int finger1y, int finger2x, int finger2y) { + MotionEvent me = MotionEvent.obtain(0, 0, 0, 0, 0, 0); + ShadowMotionEvent sme = shadowOf(me); + sme.setAction(MotionEvent.ACTION_POINTER_DOWN); + sme.setPointerIds(0, 1); + sme.setLocation(finger1x, finger1y); + sme.setPointer2(finger2x, finger2y); + return me; + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/util/InstrumentedXYPlot.java b/androidplot-core/src/test/java/com/androidplot/util/InstrumentedXYPlot.java new file mode 100644 index 00000000..ab1a71b4 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/util/InstrumentedXYPlot.java @@ -0,0 +1,24 @@ +package com.androidplot.util; + +import android.content.*; +import android.graphics.*; + +import com.androidplot.xy.*; + +/** + * Created by halfhp on 10/21/16. + */ +public class InstrumentedXYPlot extends XYPlot { + + // may be set by consumers if necessary + public Canvas canvas = new Canvas(); + + public InstrumentedXYPlot(Context context) { + super(context, "a test plot"); + } + + @Override + public void redraw() { + super.onDraw(canvas); + } +} 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 0a4d297f..83a7eb81 100644 --- a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java +++ b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java @@ -24,6 +24,10 @@ import java.util.Arrays; import java.util.List; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class SeriesUtilsTest { @@ -49,43 +53,43 @@ public void tearDown() throws Exception { public void testSeriesMinMax() { SimpleXYSeries series = new SimpleXYSeries(LINEAR, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); RectRegion minMax = SeriesUtils.minMax(series); - assertEquals(0, minMax.getMinX()); - assertEquals(7, minMax.getMaxX()); - assertEquals(1, minMax.getMinY()); - assertEquals(8, minMax.getMaxY()); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(7, minMax.getMaxX().doubleValue(), 0); + assertEquals(1, minMax.getMinY().doubleValue(), 0); + assertEquals(8, minMax.getMaxY().doubleValue(), 0); series = new SimpleXYSeries(LINEAR_INVERSE, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); minMax = SeriesUtils.minMax(series); - assertEquals(0, minMax.getMinX()); - assertEquals(7, minMax.getMaxX()); - assertEquals(1, minMax.getMinY()); - assertEquals(8, minMax.getMaxY()); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(7, minMax.getMaxX().doubleValue(), 0); + assertEquals(1, minMax.getMinY().doubleValue(), 0); + assertEquals(8, minMax.getMaxY().doubleValue(), 0); series = new SimpleXYSeries(ZIG_ZAG, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); minMax = SeriesUtils.minMax(series); - assertEquals(0, minMax.getMinX()); - assertEquals(7, minMax.getMaxX()); - assertEquals(1, minMax.getMinY()); - assertEquals(10, minMax.getMaxY()); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(7, minMax.getMaxX().doubleValue(), 0); + assertEquals(1, minMax.getMinY().doubleValue(), 0); + assertEquals(10, minMax.getMaxY().doubleValue(), 0); series = new SimpleXYSeries(NULLS, NULLS, null); minMax = SeriesUtils.minMax(series); - assertEquals(-1, minMax.getMinX()); - assertEquals(4, minMax.getMaxX()); - assertEquals(-1, minMax.getMinY()); - assertEquals(4, minMax.getMaxY()); + assertEquals(-1, minMax.getMinX().doubleValue(), 0); + assertEquals(4, minMax.getMaxX().doubleValue(), 0); + assertEquals(-1, minMax.getMinY().doubleValue(), 0); + assertEquals(4, minMax.getMaxY().doubleValue(), 0); series = new SimpleXYSeries(SINGLE_VALUE, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); minMax = SeriesUtils.minMax(series); - assertEquals(0, minMax.getMinX()); - assertEquals(0, minMax.getMaxX()); - assertEquals(3, minMax.getMinY()); - assertEquals(3, minMax.getMaxY()); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(0, minMax.getMaxX().doubleValue(), 0); + assertEquals(3, minMax.getMinY().doubleValue(), 0); + assertEquals(3, minMax.getMaxY().doubleValue(), 0); series = new SimpleXYSeries(SINGLE_VALUE_NULL, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); minMax = SeriesUtils.minMax(series); - assertEquals(0, minMax.getMinX()); - assertEquals(0, minMax.getMaxX()); + assertEquals(0, minMax.getMinX().doubleValue(), 0); + assertEquals(0, minMax.getMaxX().doubleValue(), 0); assertEquals(null, minMax.getMinY()); assertEquals(null, minMax.getMaxY()); @@ -100,24 +104,24 @@ public void testSeriesMinMax() { @Test public void testListMinMax() { Region minMax = SeriesUtils.minMax(LINEAR); - assertEquals(1, minMax.getMin()); - assertEquals(8, minMax.getMax()); + assertEquals(1, minMax.getMin().doubleValue(), 0); + assertEquals(8, minMax.getMax().doubleValue(), 0); minMax = SeriesUtils.minMax(LINEAR_INVERSE); - assertEquals(1, minMax.getMin()); - assertEquals(8, minMax.getMax()); + assertEquals(1, minMax.getMin().doubleValue(), 0); + assertEquals(8, minMax.getMax().doubleValue(), 0); minMax = SeriesUtils.minMax(ZIG_ZAG); - assertEquals(1, minMax.getMin()); - assertEquals(10, minMax.getMax()); + assertEquals(1, minMax.getMin().doubleValue(), 0); + assertEquals(10, minMax.getMax().doubleValue(), 0); minMax = SeriesUtils.minMax(NULLS); - assertEquals(-1, minMax.getMin()); - assertEquals(4, minMax.getMax()); + assertEquals(-1, minMax.getMin().doubleValue(), 0); + assertEquals(4, minMax.getMax().doubleValue(), 0); minMax = SeriesUtils.minMax(SINGLE_VALUE); - assertEquals(3, minMax.getMin()); - assertEquals(3, minMax.getMax()); + assertEquals(3, minMax.getMin().doubleValue(), 0); + assertEquals(3, minMax.getMax().doubleValue(), 0); minMax = SeriesUtils.minMax(SINGLE_VALUE_NULL); assertEquals(null, minMax.getMin()); @@ -127,4 +131,119 @@ public void testListMinMax() { assertEquals(null, minMax.getMin()); assertEquals(null, minMax.getMax()); } + + @Test + public void testGetNullRegion() { + XYSeries s1 = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "s1", + 0, 0, // 0 + 1, 1, // 1 + 2, 2, // 2 + null, null, // 3 + null, null, // 4 + 5, 5, // 5 + 6, 6); // 6 + + try { + Region r1 = SeriesUtils.getNullRegion(s1, 0); + fail("IllegalArgumentException expected."); + } catch(IllegalArgumentException e) { + // expected + } + + try { + Region r2 = SeriesUtils.getNullRegion(s1, s1.size() - 1); + fail("IllegalArgumentException expected."); + } catch(IllegalArgumentException e) { + // expected + } + + Region r3 = SeriesUtils.getNullRegion(s1, 3); + assertEquals(2, r3.getMin().intValue()); + assertEquals(5, r3.getMax().intValue()); + + Region r4 = SeriesUtils.getNullRegion(s1, 4); + assertEquals(2, r4.getMin().intValue()); + assertEquals(5, r4.getMax().intValue()); + } + + @Test + public void testIboundsMin() { + 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), + "s1"); + + assertEquals(0, SeriesUtils.iBoundsMin(s1, 0, 1)); + assertEquals(6, SeriesUtils.iBoundsMin(s1, 6, 1)); + assertEquals(12, SeriesUtils.iBoundsMin(s1, 12, 1)); + + // now test with null vals: + XYSeries s2 = new SimpleXYSeries( + Arrays.asList(null, 1, 2, null, null, 5, 6, 7, 8, null, 10, 11, null), + Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), + "s2"); + + assertEquals(0, SeriesUtils.iBoundsMin(s2, 0, 1)); + assertEquals(2, SeriesUtils.iBoundsMin(s2, 3, 1)); + assertEquals(11, SeriesUtils.iBoundsMin(s2, 12, 1)); + + // test with a higher step value: + assertEquals(0, SeriesUtils.iBoundsMin(s2, 0, 5)); + assertEquals(2, SeriesUtils.iBoundsMin(s2, 3, 5)); + assertEquals(11, SeriesUtils.iBoundsMin(s2, 12, 5)); + } + + @Test + public void testIboundsMax() { + 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), + "s1"); + + assertEquals(12, SeriesUtils.iBoundsMax(s1, 12, 1)); + assertEquals(6, SeriesUtils.iBoundsMax(s1, 6, 1)); + assertEquals(12, SeriesUtils.iBoundsMax(s1, 12, 1)); + + // now test with null vals: + XYSeries s2 = new SimpleXYSeries( + Arrays.asList(null, 1, 2, null, null, 5, 6, 7, 8, null, 10, 11, null), + Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), + "s2"); + + assertEquals(1, SeriesUtils.iBoundsMax(s2, 0, 1)); + assertEquals(5, SeriesUtils.iBoundsMax(s2, 3, 1)); + assertEquals(12, SeriesUtils.iBoundsMax(s2, 12, 1)); + + // test with a higher step value: + assertEquals(1, SeriesUtils.iBoundsMax(s2, 0, 5)); + assertEquals(5, SeriesUtils.iBoundsMax(s2, 3, 5)); + assertEquals(12, SeriesUtils.iBoundsMax(s2, 12, 5)); + } + + @Test + public void testIbounds() { + FastXYSeries series = mock(FastXYSeries.class); + when(series.size()).thenReturn(3); + when(series.getX(0)).thenReturn(0); + when(series.getX(1)).thenReturn(1); + when(series.getX(2)).thenReturn(2); + + Region result = SeriesUtils.iBounds(series, new RectRegion(0, 1, 0, 1)); + assertEquals(0, result.getMin().intValue()); + assertEquals(1, result.getMax().intValue()); + + // test with nulls: + when(series.size()).thenReturn(6); + when(series.getX(0)).thenReturn(0); + when(series.getX(1)).thenReturn(0.5); + when(series.getX(2)).thenReturn(null); + when(series.getX(3)).thenReturn(null); + when(series.getX(4)).thenReturn(1); + when(series.getX(5)).thenReturn(3); + + result = SeriesUtils.iBounds(series, new RectRegion(0, 1, 0, 1)); + assertEquals(0, result.getMin().intValue()); + assertEquals(4, result.getMax().intValue()); + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java index 354b33d0..de81e70c 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java @@ -48,7 +48,7 @@ public void testOnRender() throws Exception { XYPlot plot = new XYPlot(getContext(), "Test"); FastLineAndPointRenderer.Formatter formatter = - new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null, null); + new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null); // create a series composed of 3 "segments"; series portions separated by null values: diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java new file mode 100644 index 00000000..3269082d --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/LTTBSamplerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot.xy; + +import android.graphics.*; + +import com.androidplot.test.*; +import com.androidplot.ui.*; + +import org.junit.*; +import org.mockito.*; + +import java.util.*; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +public class LTTBSamplerTest extends AndroidplotTest { + + @Test + public void testSomething() throws Exception { + + } + +// @Test +// public void testDownsample() throws Exception { +// +// Number[][] rawNumbers = new Number[][] { {1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}, {11, 12}, {13, 14}, {15, 16}, {17, 18}, {19, 20} }; +// XYSeries rawSeries = new SimpleXYSeries( +// Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15, 17, 19), +// Arrays.asList(2, 4, 6, 8, 10, 12, 14, 16, 18, 20), "raw"); +// compareSeriesToRaw(rawNumbers, rawSeries); +// +// LTTBDownsampler downsampler = new LTTBDownsampler(); +// SimpleXYSeries sampled = new SimpleXYSeries( +// Arrays.asList(new Number[]{0, 0, 0, 0, 0}), +// Arrays.asList(new Number[]{0, 0, 0, 0, 0}), "sampled"); +// downsampler.downsample(rawSeries, sampled); +// +// Number[][] downsampled = LTTBDownsampler.downsample(rawNumbers, 5); +// compareSeriesToRaw(downsampled, sampled); +// } +// +// protected void compareSeriesToRaw(Number[][] raw, XYSeries series) { +// assertEquals(raw.length, series.size()); +// int i = 0; +// for(Number[] xy : raw) { +// assertEquals(xy[0], series.getX(i)); +// assertEquals(xy[1], series.getY(i)); +// i++; +// } +// } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java index 892dbfa8..e42eef65 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java @@ -55,7 +55,7 @@ public void testDrawSeries_withInterpolation() throws Exception { } /** - * Sanity check to make sure that at the end of the day, points are being drawn at the expected + * Sanity run to make sure that at the end of the day, points are being drawn at the expected * screen-coords. * @throws Exception */ @@ -67,7 +67,7 @@ public void testRenderPoints() throws Exception { XYPlot plot = new XYPlot(getContext(), "Test"); FastLineAndPointRenderer.Formatter formatter = - new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null, null); + new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null); // create a series composed of 3 "segments"; series portions separated by null values: XYSeries series = new SimpleXYSeries( diff --git a/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java index 32bec2a9..6bb7fb20 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java @@ -20,13 +20,15 @@ import android.graphics.*; import android.view.*; -import com.androidplot.*; +import com.androidplot.Region; import com.androidplot.test.*; import com.androidplot.ui.*; +import com.androidplot.util.*; import org.junit.*; import org.mockito.*; +import static junit.framework.Assert.assertEquals; import static org.mockito.Mockito.*; public class PanZoomTest extends AndroidplotTest { @@ -41,14 +43,16 @@ public class PanZoomTest extends AndroidplotTest { TypedArray typedArray; @Mock - SeriesRegistry seriesRegistry; + XYSeriesRegistry seriesRegistry; RectRegion bounds = new RectRegion(0, 100, 0, 100); @Before public void setUp() throws Exception { - when(xyPlot.getSeriesRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getRegistry()).thenReturn(seriesRegistry); when(xyPlot.getBounds()).thenReturn(bounds); + when(xyPlot.getInnerLimits()).thenReturn(new RectRegion()); + when(xyPlot.getOuterLimits()).thenReturn(new RectRegion()); } @After @@ -79,46 +83,115 @@ public void testOnTouch_oneFingerMovePansButDoesNotZoom() throws Exception { MotionEvent moveEvent = mock(MotionEvent.class); doNothing().when(panZoom).calculatePan( - any(PointF.class), any(PointF.class), anyBoolean()); + any(PointF.class), any(Region.class), anyBoolean()); when(moveEvent.getAction()) .thenReturn(MotionEvent.ACTION_DOWN) - .thenReturn(MotionEvent.ACTION_MOVE); + .thenReturn(MotionEvent.ACTION_MOVE) + .thenReturn(MotionEvent.ACTION_UP); panZoom.onTouch(xyPlot, moveEvent); // fires ACTION_DOWN panZoom.onTouch(xyPlot, moveEvent); // fires ACTION_MOVE + panZoom.onTouch(xyPlot, moveEvent); // fires ACTION_UP - verify(panZoom, times(1)).pan(moveEvent); - verify(panZoom, times(0)).zoom(moveEvent); + verify(panZoom).pan(moveEvent); + verify(panZoom, never()).zoom(moveEvent); + verify(panZoom).reset(); } @Test public void testOnTouch_twoFingersZoom() throws Exception { PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE)); - - View.OnTouchListener listener = mock(View.OnTouchListener.class); - panZoom.setDelegate(listener); - MotionEvent moveEvent = mock(MotionEvent.class); - doNothing().when(panZoom).calculatePan( - any(PointF.class), any(PointF.class), anyBoolean()); - + // simulate a zoom gesture sequence: when(moveEvent.getAction()) .thenReturn(MotionEvent.ACTION_DOWN) .thenReturn(MotionEvent.ACTION_POINTER_DOWN) - .thenReturn(MotionEvent.ACTION_MOVE); - - final float pinchDistance = PanZoom.MIN_DIST_2_FING + 1; + .thenReturn(MotionEvent.ACTION_MOVE) + .thenReturn(MotionEvent.ACTION_UP); - doReturn(new RectF(0, 0, pinchDistance, pinchDistance)) - .when(panZoom).getDistance(any(MotionEvent.class)); + when(panZoom.fingerDistance(moveEvent)) + .thenReturn(new RectF(0, 0, 10, 10)) + .thenReturn(new RectF(0, 0, 11, 11)) + .thenReturn(new RectF(0, 0, 12, 12)) + .thenReturn(new RectF(0, 0, 13, 13)); panZoom.onTouch(xyPlot, moveEvent); // ACTION_DOWN panZoom.onTouch(xyPlot, moveEvent); // ACTION_POINTER_DOWN panZoom.onTouch(xyPlot, moveEvent); // ACTION_MOVE + panZoom.onTouch(xyPlot, moveEvent); // ACTION_UP - verify(panZoom, times(1)).zoom(moveEvent); + verify(xyPlot).redraw(); + verify(panZoom, never()).pan(any(MotionEvent.class)); + verify(panZoom).zoom(moveEvent); + verify(panZoom).reset(); } + @Test + public void testZoom() { + xyPlot = spy(new InstrumentedXYPlot(getContext())); + xyPlot.setDomainBoundaries(0, 100, BoundaryMode.FIXED); + xyPlot.setRangeBoundaries(0, 100, BoundaryMode.FIXED); + xyPlot.redraw(); + + PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE)); + + // cap our pan/zoom boundaries: + xyPlot.getOuterLimits().set(0, 100, 0, 100); + + panZoom.setFingersRect(new RectF(0, 0, 20, 20)); + + InOrder inOrder = inOrder(xyPlot); + inOrder.verify(xyPlot).setDomainBoundaries(0, 100, BoundaryMode.FIXED); + + // should result in a 2x zoom on domain centerpoint: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 40, 40)); + inOrder.verify(xyPlot).setDomainBoundaries(25f, 75f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(25f, 75f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // should result in another 2x zoom on domain centerpoint: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 80, 80)); + inOrder.verify(xyPlot).setDomainBoundaries(37.5f, 62.5f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(37.5f, 62.5f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // should zoom out and take us back to the original bounds: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 20, 20)); + inOrder.verify(xyPlot).setDomainBoundaries(0f, 100f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(0f, 100f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).redraw(); + + // zooming out past capped bounds should not result in any change: + panZoom.zoom(TestUtils.newPointerDownEvent(0, 0, 1, 1)); + inOrder.verify(xyPlot).setDomainBoundaries(0f, 100f, BoundaryMode.FIXED); + inOrder.verify(xyPlot).setRangeBoundaries(0f, 100f, BoundaryMode.FIXED); + // TODO: if nothing changed, then why bother redrawing?? + inOrder.verify(xyPlot).redraw(); + + // redraw should not be called again + inOrder.verify(xyPlot, never()).redraw(); + + // make sure no panning took place during these zoom ops: + verify(panZoom, never()).pan(any(MotionEvent.class)); + + } + + @Test + public void testFingerDistance() { + PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE)); + RectF distance = panZoom.fingerDistance(TestUtils.newPointerDownEvent(0, 0, 10, 10)); + assertEquals(0f, distance.left); + assertEquals(0f, distance.top); + assertEquals(10f, distance.right); + assertEquals(10f, distance.bottom); + + // no matter what order the coords are supplied, make sure the same rect is calculated: + distance = panZoom.fingerDistance(10, 10, 0, 0); + assertEquals(0f, distance.left); + assertEquals(0f, distance.top); + assertEquals(10f, distance.right); + assertEquals(10f, distance.bottom); + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java index 1836dc20..70a9bbe1 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java @@ -21,6 +21,7 @@ import com.androidplot.test.AndroidplotTest; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import static junit.framework.Assert.assertEquals; @@ -127,10 +128,10 @@ public void testClip_sameDimensions() throws Exception { RectRegion r2 = new RectRegion(0, 10, 0, 10); r1.intersect(r2); - assertEquals(0, r1.getMinX()); - assertEquals(10, r1.getMaxX()); - assertEquals(0, r1.getMinY()); - assertEquals(10, r1.getMaxY()); + assertEquals(0, r1.getMinX().doubleValue(), 0); + assertEquals(10, r1.getMaxX().doubleValue(), 0); + assertEquals(0, r1.getMinY().doubleValue(), 0); + assertEquals(10, r1.getMaxY().doubleValue(), 0); } @Test @@ -139,19 +140,19 @@ public void testClip_intersectingDimensions() throws Exception { RectRegion r2 = new RectRegion(5, 15, 5, 15); r1.intersect(r2); - assertEquals(5, r1.getMinX()); - assertEquals(10, r1.getMaxX()); - assertEquals(5, r1.getMinY()); - assertEquals(10, r1.getMaxY()); + assertEquals(5, r1.getMinX().doubleValue(), 0); + assertEquals(10, r1.getMaxX().doubleValue(), 0); + assertEquals(5, r1.getMinY().doubleValue(), 0); + assertEquals(10, r1.getMaxY().doubleValue(), 0); r1 = new RectRegion(0, 10, 0, 10); r2 = new RectRegion(-5, 5, -5, 5); r1.intersect(r2); - assertEquals(0, r1.getMinX()); - assertEquals(5, r1.getMaxX()); - assertEquals(0, r1.getMinY()); - assertEquals(5, r1.getMaxY()); + assertEquals(0, r1.getMinX().doubleValue(), 0); + assertEquals(5, r1.getMaxX().doubleValue(), 0); + assertEquals(0, r1.getMinY().doubleValue(), 0); + assertEquals(5, r1.getMaxY().doubleValue(), 0); } @Test @@ -182,17 +183,68 @@ public void testUnion() throws Exception { r1.union(r2); - assertEquals(0, r1.getMinX()); - assertEquals(200, r1.getMaxX()); - assertEquals(0, r1.getMinY()); - assertEquals(200, r1.getMaxY()); + assertEquals(0, r1.getMinX().doubleValue(), 0); + assertEquals(200, r1.getMaxX().doubleValue(), 0); + assertEquals(0, r1.getMinY().doubleValue(), 0); + assertEquals(200, r1.getMaxY().doubleValue(), 0); r1 = new RectRegion(0, 10, 0, 10); r2.union(r1); - assertEquals(0, r2.getMinX()); - assertEquals(200, r2.getMaxX()); - assertEquals(0, r2.getMinY()); - assertEquals(200, r2.getMaxY()); + assertEquals(0, r2.getMinX().doubleValue(), 0); + assertEquals(200, r2.getMaxX().doubleValue(), 0); + assertEquals(0, r2.getMinY().doubleValue(), 0); + assertEquals(200, r2.getMaxY().doubleValue(), 0); + } + + /** + * Currently ignored as the base implementation does not currently pass. + */ + @Ignore + @Test + public void testOverlapsLine() { + RectRegion r1 = new RectRegion(0, 100, 0, 100); + +// assertFalse(r1.intersectsWithLine(200, 200, 400, 400)); +// assertFalse(r1.intersectsWithLine(-1, -1, -1, 100)); +// assertTrue(r1.intersectsWithLine(0, 0, 100, 100)); +// assertTrue(r1.intersectsWithLine(50, 50, 200, 200)); + + // lines running parallel to region edges: + assertTrue(r1.intersectsWithLine(0, 0, 0, 100)); + assertTrue(r1.intersectsWithLine(0, 0, 100, 0)); + assertTrue(r1.intersectsWithLine(0, 100, 0, 0)); + assertTrue(r1.intersectsWithLine(100, 0, 0, 0)); + + assertTrue(r1.intersectsWithLine(100, 0, 0, 0)); + assertTrue(r1.intersectsWithLine(0, 100, 0, 0)); + assertTrue(r1.intersectsWithLine(0, 0, 100, 0)); + assertTrue(r1.intersectsWithLine(0, 0, 0, 100)); + + // lines passing through top & bottom edges only: + assertTrue(r1.intersectsWithLine(50, -1000, 50, 1000)); + assertTrue(r1.intersectsWithLine(50, 1000, 50, -1000)); + + // lines passing through left & right edges only: + assertTrue(r1.intersectsWithLine(-1000, 50, 1000, 50)); + assertTrue(r1.intersectsWithLine(1000, 50, -100, 50)); + + // diagonal passing through bottom-left and top-right corners: + assertTrue(r1.intersectsWithLine(-100, -100, 200, 200)); + + // diagonal passing inside upper-left edge + assertTrue(r1.intersectsWithLine(-20, 80, 20, 120)); + assertTrue(r1.intersectsWithLine(20, 120, -20, 80)); + + // diagonal passing outside upper-left edge + + // diagonal passing inside lower-left edge + // diagonal passing outside lower-left edge + + // diagonal passing inside upper-right edge + // diagonal passing outside upper-right edge + + // diagonal passing inside lower-right edge + // diagonal passing outside lower-right edge } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java new file mode 100644 index 00000000..df07b4f5 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java @@ -0,0 +1,92 @@ +package com.androidplot.xy; + +import com.androidplot.test.*; + +import org.junit.*; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Created by halfhp on 10/8/16. + */ +public class SampledXYSeriesTest extends AndroidplotTest { + + @Test + public void testInit() throws Exception { + + XYSeries rawData = TestUtils.generateXYSeries("my series", 2000); + SampledXYSeries s1 = spy(new SampledXYSeries(rawData, 2, 100)); + + // expect 5 setZoomFactor levels (besides 1x): 2x, 4x, 8x, 16x, 32x + assertEquals(4, s1.getZoomLevels().size()); + assertEquals(1000, s1.getZoomLevels().get(0).size()); // 2x + assertEquals(500, s1.getZoomLevels().get(1).size()); // 4x + assertEquals(250, s1.getZoomLevels().get(2).size()); // 8x + assertEquals(125, s1.getZoomLevels().get(3).size()); // 16x + + SampledXYSeries s2 = spy(new SampledXYSeries(rawData, 4, 100)); + + // expect 2 setZoomFactor levels (besides 1x): 4x & 16x: + assertEquals(2, s2.getZoomLevels().size()); + assertEquals(500, s2.getZoomLevels().get(0).size()); // 4x + assertEquals(125, s2.getZoomLevels().get(1).size()); // 16x + } + + @Test + public void testGetZoomIndex() throws Exception { + assertEquals(0, SampledXYSeries.getZoomIndex(2, 2)); + assertEquals(1, SampledXYSeries.getZoomIndex(3, 2)); + assertEquals(1, SampledXYSeries.getZoomIndex(4, 2)); + assertEquals(2, SampledXYSeries.getZoomIndex(8, 2)); + assertEquals(2, SampledXYSeries.getZoomIndex(9, 2)); + assertEquals(2, SampledXYSeries.getZoomIndex(10, 2)); + assertEquals(3, SampledXYSeries.getZoomIndex(15, 2)); + assertEquals(3, SampledXYSeries.getZoomIndex(16, 2)); + assertEquals(3, SampledXYSeries.getZoomIndex(17, 2)); + assertEquals(4, SampledXYSeries.getZoomIndex(31, 2)); + assertEquals(4, SampledXYSeries.getZoomIndex(32, 2)); + + assertEquals(0, SampledXYSeries.getZoomIndex(1, 4)); + assertEquals(0, SampledXYSeries.getZoomIndex(4, 4)); + assertEquals(1, SampledXYSeries.getZoomIndex(15, 4)); + assertEquals(1, SampledXYSeries.getZoomIndex(16, 4)); + assertEquals(1, SampledXYSeries.getZoomIndex(17, 4)); + assertEquals(2, SampledXYSeries.getZoomIndex(63, 4)); + assertEquals(2, SampledXYSeries.getZoomIndex(64, 4)); + assertEquals(2, SampledXYSeries.getZoomIndex(65, 4)); + } + + @Test + public void testSetZoomFactor() throws Exception { + XYSeries rawData = TestUtils.generateXYSeries("my series", 2000); + SampledXYSeries sampledXYSeries = spy(new SampledXYSeries(rawData, 2, 100)); + sampledXYSeries.setZoomFactor(2); + assertEquals(1000, sampledXYSeries.size()); + sampledXYSeries.setZoomFactor(4); + assertEquals(500, sampledXYSeries.size()); + sampledXYSeries.setZoomFactor(8); + assertEquals(250, sampledXYSeries.size()); + sampledXYSeries.setZoomFactor(16); + assertEquals(125, sampledXYSeries.size()); + } + + @Test + public void testResample() { + XYSeries rawData = TestUtils.generateXYSeries("my series", 10000); + SampledXYSeries sampledXYSeries = new SampledXYSeries(rawData, 2, 200); + assertEquals(5, sampledXYSeries.getZoomLevels().size()); + } + + /** + * Ignored until null support is added to {@link LTTBSampler}. + */ + @Ignore + @Test + public void testResample_supportsNullVals() { + XYSeries rawData = TestUtils.generateXYSeriesWithNulls("my series", 10000); + SampledXYSeries sampledXYSeries = new SampledXYSeries(rawData, 2, 200); + assertEquals(5, sampledXYSeries.getZoomLevels().size()); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java index 4128034d..397e7700 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/SimpleXYSeriesTest.java @@ -127,4 +127,23 @@ public void testSet() throws Exception { assertEquals(size, series.size()); } + @Test + public void testResize() throws Exception { + SimpleXYSeries series = new SimpleXYSeries("series"); + series.resize(10); + assertEquals(10, series.size()); + + series.resize(20); + assertEquals(20, series.size()); + + series.resize(1); + assertEquals(1, series.size()); + + series.resize(0); + assertEquals(0, series.size()); + + series.resize(0); + assertEquals(0, series.size()); + } + } 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 25ebc513..f0e0c76e 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java @@ -19,7 +19,6 @@ import android.content.res.*; import android.graphics.*; -import com.androidplot.*; import com.androidplot.test.*; import com.androidplot.ui.*; @@ -30,10 +29,8 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyDouble; import static org.mockito.Matchers.anyFloat; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -53,7 +50,7 @@ public class XYGraphWidgetTest extends AndroidplotTest { TypedArray typedArray; @Mock - SeriesRegistry seriesRegistry; + XYSeriesRegistry seriesRegistry; RectRegion bounds = new RectRegion(0, 100, 0, 100); @@ -71,7 +68,7 @@ public class XYGraphWidgetTest extends AndroidplotTest { public void setUp() throws Exception { size = spy(new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE)); xyPlot = spy(new XYPlot(getContext(), "XYPlot")); - when(xyPlot.getSeriesRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getRegistry()).thenReturn(seriesRegistry); when(xyPlot.getBounds()).thenReturn(bounds); when(xyPlot.getDomainOrigin()).thenReturn(0); when(xyPlot.getRangeOrigin()).thenReturn(0); 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 bd98c386..bbb2bec5 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java @@ -56,7 +56,7 @@ public void testDoOnDraw() throws Exception { plot.addSeries(s1, new LineAndPointFormatter( Color.RED, Color.GREEN, Color.BLUE, null)); - assertEquals(1, plot.getSeriesRegistry().size()); + assertEquals(1, plot.getRegistry().size()); plot.exposedOnSizeChanged(100, 100, 100, 100); plot.redraw(); @@ -65,7 +65,7 @@ public void testDoOnDraw() throws Exception { plot.exposedOnDraw(new Canvas()); plot.removeSeries(s1); - assertEquals(0, plot.getSeriesRegistry().size()); + assertEquals(0, plot.getRegistry().size()); plot.addSeries(s1, new BarFormatter(Color.RED, Color.GREEN)); plot.redraw(); diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java index 01f4ac34..7e443b29 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java @@ -61,8 +61,8 @@ public void testOriginFixedMode() throws Exception { plot.calculateMinMaxVals(); - assertEquals(3.0, plot.getBounds().getMinX()); - assertEquals(7.0, plot.getBounds().getMaxX()); + assertEquals(3.0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(7.0, plot.getBounds().getMaxX().doubleValue(), 0); } @Test @@ -71,14 +71,14 @@ public void testOriginAutoMode() throws Exception { plot.centerOnDomainOrigin(5); plot.calculateMinMaxVals(); - assertEquals(10.0, plot.getBounds().getMaxX()); // symmetry is @ 10, not 9 - assertEquals(0.0, plot.getBounds().getMinX()); + assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0); // symmetry is @ 10, not 9 + assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0); plot.centerOnRangeOrigin(50); plot.calculateMinMaxVals(); - assertEquals(100.0, plot.getBounds().getMaxY()); - assertEquals(0.0, plot.getBounds().getMinY()); + assertEquals(100.0, plot.getBounds().getMaxY().doubleValue(), 0); + assertEquals(0.0, plot.getBounds().getMinY().doubleValue(), 0); } @@ -88,22 +88,22 @@ public void testOriginGrowMode() throws Exception { plot.centerOnDomainOrigin(5, null, BoundaryMode.GROW); plot.calculateMinMaxVals(); - assertEquals(0.0, plot.getBounds().getMinX()); - assertEquals(10.0, plot.getBounds().getMaxX()); + assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0); // introduce a larger domain set. boundaries should change series1.setModel(numList2, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); - assertEquals(-1.0, plot.getBounds().getMinX()); - assertEquals(11.0, plot.getBounds().getMaxX()); + assertEquals(-1.0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11.0, plot.getBounds().getMaxX().doubleValue(), 0); // revert series model back to the previous set. boundaries should remain the same series1.setModel(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); - assertEquals(-1.0, plot.getBounds().getMinX()); - assertEquals(11.0, plot.getBounds().getMaxX()); + assertEquals(-1.0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11.0, plot.getBounds().getMaxX().doubleValue(), 0); } @Test @@ -112,14 +112,14 @@ public void testOriginShrinkMode() throws Exception { plot.centerOnDomainOrigin(5, null, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); - assertEquals(0.0, plot.getBounds().getMinX()); - assertEquals(10.0, plot.getBounds().getMaxX()); + assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0); // update with more extreme values...nothing should change in shrink mode: series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); - assertEquals(0.0, plot.getBounds().getMinX()); - assertEquals(10.0, plot.getBounds().getMaxX()); + assertEquals(0.0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(10.0, plot.getBounds().getMaxX().doubleValue(), 0); } @@ -130,49 +130,49 @@ public void testsetDomainBoundaries() throws Exception { plot.addSeries(series1, new LineAndPointFormatter()); plot.calculateMinMaxVals(); - // default to auto so check them - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(9, plot.getBounds().getMaxX()); + // default to auto so run them + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); plot.setDomainBoundaries(2, BoundaryMode.FIXED, 8, BoundaryMode.FIXED); plot.calculateMinMaxVals(); // fixed - assertEquals(2, plot.getBounds().getMinX()); - assertEquals(8, plot.getBounds().getMaxX()); + assertEquals(2, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(8, plot.getBounds().getMaxX().doubleValue(), 0); // back to auto plot.setDomainBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); - // check again - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(9, plot.getBounds().getMaxX()); + // run again + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); // we are not testing MinY well with this dataset. // try grow plot.setDomainBoundaries(2, BoundaryMode.GROW, 8, BoundaryMode.GROW); plot.calculateMinMaxVals(); - // check inital - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(9, plot.getBounds().getMaxX()); + // run inital + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); // update with more extreme values... series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after growing - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(11, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // back to previous series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(11, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // back to big series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); @@ -180,33 +180,33 @@ public void testsetDomainBoundaries() throws Exception { plot.setDomainBoundaries(2, BoundaryMode.SHRINK, 8, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); - // check inital - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(11, plot.getBounds().getMaxX()); + // run inital + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // now small series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after shrinking - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(9, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); // back to previous series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(9, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); // back to auto plot.setDomainBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); // should of changed. - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(11, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); } @Test @@ -214,48 +214,48 @@ public void testsetRangeBoundaries() throws Exception { plot.addSeries(series1, new LineAndPointFormatter()); plot.calculateMinMaxVals(); - // default to auto so check them - assertEquals(0, plot.getBounds().getMinY()); - assertEquals(100, plot.getBounds().getMaxY()); + // default to auto so run them + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); plot.setRangeBoundaries(5, BoundaryMode.FIXED, 80, BoundaryMode.FIXED); plot.calculateMinMaxVals(); // fixed - assertEquals(5, plot.getBounds().getMinY()); - assertEquals(80, plot.getBounds().getMaxY()); + assertEquals(5, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(80, plot.getBounds().getMaxY().doubleValue(), 0); // back to auto plot.setRangeBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); - // check again - assertEquals(0, plot.getBounds().getMinY()); - assertEquals(100, plot.getBounds().getMaxY()); + // run again + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); // try grow plot.setRangeBoundaries(2, BoundaryMode.GROW, 8, BoundaryMode.GROW); plot.calculateMinMaxVals(); - // check inital - assertEquals(0, plot.getBounds().getMinY()); - assertEquals(100, plot.getBounds().getMaxY()); + // run inital + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); // update with more extreme values... series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after growing - assertEquals(-100, plot.getBounds().getMinY()); - assertEquals(200, plot.getBounds().getMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // back to previous series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. - assertEquals(-100, plot.getBounds().getMinY()); - assertEquals(200, plot.getBounds().getMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // back to big series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); @@ -263,33 +263,33 @@ public void testsetRangeBoundaries() throws Exception { plot.setRangeBoundaries(2, BoundaryMode.SHRINK, 8, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); - // check inital - assertEquals(-100, plot.getBounds().getMinY()); - assertEquals(200, plot.getBounds().getMaxY()); + // run inital + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // now small series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after shrinking - assertEquals(0, plot.getBounds().getMinY()); - assertEquals(100, plot.getBounds().getMaxY()); + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); // back to previous series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. - assertEquals(0, plot.getBounds().getMinY()); - assertEquals(100, plot.getBounds().getMaxY()); + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); // back to auto plot.setRangeBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); // should of changed. - assertEquals(-100, plot.getBounds().getMinY()); - assertEquals(200, plot.getBounds().getMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); } @Test @@ -297,54 +297,58 @@ public void testSetDomainRightMinMax() throws Exception { plot.addSeries(series1, new LineAndPointFormatter()); plot.calculateMinMaxVals(); - // default to auto so check them - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(9, plot.getBounds().getMaxX()); - - plot.setDomainRightMax(10); + // default to auto so run them + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); + + plot.getOuterLimits().setMaxX(10); + //plot.setDomainRightMax(10); plot.calculateMinMaxVals(); // same values. - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(9, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on RightMax - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(10, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(10, plot.getBounds().getMaxX().doubleValue(), 0); - plot.setDomainRightMax(null); + //plot.setDomainRightMax(null); + plot.getOuterLimits().setMaxX(null); plot.calculateMinMaxVals(); // back to full - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(11, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // now the RightMin - plot.setDomainRightMin(10); + //plot.setDomainRightMin(10); + plot.getInnerLimits().setMaxX(10); plot.calculateMinMaxVals(); // still to full - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(11, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // small list series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on RightMin - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(10, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(10, plot.getBounds().getMaxX().doubleValue(), 0); // now off again - plot.setDomainRightMin(null); + //plot.setDomainRightMin(null); + plot.getInnerLimits().setMaxX(null); plot.calculateMinMaxVals(); // small values. - assertEquals(0, plot.getBounds().getMinX()); - assertEquals(9, plot.getBounds().getMaxX()); + assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); + assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); } @Test @@ -352,58 +356,66 @@ public void testSetRangeTopBottomMinMax() throws Exception { plot.addSeries(series1, new LineAndPointFormatter()); plot.calculateMinMaxVals(); - // default to auto so check them - assertEquals(0, plot.getBounds().getMinY()); - assertEquals(100, plot.getBounds().getMaxY()); - - plot.setRangeTopMax(110); - plot.setRangeBottomMin(-50); + // default to auto so run them + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); + + plot.getOuterLimits().setMaxY(110); + plot.getOuterLimits().setMinY(-50); + //plot.setRangeTopMax(110); + //plot.setRangeBottomMin(-50); plot.calculateMinMaxVals(); // same values. - assertEquals(0, plot.getBounds().getMinY()); - assertEquals(100, plot.getBounds().getMaxY()); + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on Limits - assertEquals(-50, plot.getBounds().getMinY()); - assertEquals(110, plot.getBounds().getMaxY()); + assertEquals(-50, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(110, plot.getBounds().getMaxY().doubleValue(), 0); - plot.setRangeTopMax(null); - plot.setRangeBottomMin(null); + plot.getOuterLimits().setMaxY(null); + plot.getOuterLimits().setMinY(null); + //plot.setRangeTopMax(null); + //plot.setRangeBottomMin(null); plot.calculateMinMaxVals(); // back to full - assertEquals(-100, plot.getBounds().getMinY()); - assertEquals(200, plot.getBounds().getMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // now the Min - plot.setRangeTopMin(150); - plot.setRangeBottomMax(-60); + plot.getInnerLimits().setMaxY(150); + plot.getInnerLimits().setMinY(-60); + //plot.setRangeTopMin(150); + //plot.setRangeBottomMax(-60); plot.calculateMinMaxVals(); // still to full - assertEquals(-100, plot.getBounds().getMinY()); - assertEquals(200, plot.getBounds().getMaxY()); + assertEquals(-100, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(200, plot.getBounds().getMaxY().doubleValue(), 0); // small list series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on Limits - assertEquals(-60, plot.getBounds().getMinY()); - assertEquals(150, plot.getBounds().getMaxY()); + assertEquals(-60, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(150, plot.getBounds().getMaxY().doubleValue(), 0); // now off again - plot.setRangeTopMin(null); - plot.setRangeBottomMax(null); + plot.getInnerLimits().setMaxY(null); + plot.getInnerLimits().setMinY(null); + //plot.setRangeTopMin(null); + //plot.setRangeBottomMax(null); plot.calculateMinMaxVals(); // small values. - assertEquals(0, plot.getBounds().getMinY()); - assertEquals(100, plot.getBounds().getMaxY()); + assertEquals(0, plot.getBounds().getMinY().doubleValue(), 0); + assertEquals(100, plot.getBounds().getMaxY().doubleValue(), 0); } @Test diff --git a/androidplot-core/src/test/java/com/androidplot/xy/ZoomEstimatorTest.java b/androidplot-core/src/test/java/com/androidplot/xy/ZoomEstimatorTest.java new file mode 100644 index 00000000..e33ff402 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/ZoomEstimatorTest.java @@ -0,0 +1,65 @@ +package com.androidplot.xy; + +import com.androidplot.test.*; + +import org.junit.*; +import org.mockito.*; + +import static junit.framework.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Created by halfhp on 10/8/16. + */ +public class ZoomEstimatorTest extends AndroidplotTest { + + @Mock + XYPlot xyPlot; + + + @Test + public void testCheck() throws Exception { + ZoomEstimator estimator = spy(new ZoomEstimator()); + + SampledXYSeries series = + spy(new SampledXYSeries(TestUtils + .generateXYSeries("test series", 1000), 2, 100)); + series.resample(); + assertEquals(8d, series.getMaxZoomFactor()); + + XYSeriesBundle bundle = new XYSeriesBundle(series, null); + + when(xyPlot.getBounds()) + .thenReturn(new RectRegion(0, 1000, 0, 1000)) + .thenReturn(new RectRegion(0, 500, 0, 500)) + .thenReturn(new RectRegion(0, 1, 0, 1)); + + estimator.run(xyPlot, bundle); + estimator.run(xyPlot, bundle); + estimator.run(xyPlot, bundle); + + verify(series).setZoomFactor(8); + verify(series).setZoomFactor(4); + verify(series).setZoomFactor(1); + } + + @Test + public void testCalculateZoom() { + ZoomEstimator estimator = spy(new ZoomEstimator()); + + SampledXYSeries series = + spy(new SampledXYSeries(TestUtils + .generateXYSeries("test series", 1000), 2, 100)); + series.resample(); + + when(xyPlot.getBounds()) + .thenReturn(new RectRegion(0, 1000, 0, 1000)); + + // fully zoomed out so max zoom factor should be applied: + assertEquals(series.getMaxZoomFactor(), estimator.calculateZoom(series, new RectRegion(0, 1000, 0, 1000))); + + // fully zoomed in so min zoom factor should be applied: + assertEquals(1d, estimator.calculateZoom(series, new RectRegion(0, 1, 0, 1))); + + } +} diff --git a/build.gradle b/build.gradle index 6d67f3fe..d74e02dc 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 24 theTargetSdkVersion = 24 theMinSdkVersion = 5 - theVersionName = '1.2.3' + theVersionName = '1.3.0' theVersionCode = 0 } diff --git a/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java index 983cd1b8..e3400ec5 100644 --- a/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java @@ -333,9 +333,9 @@ private void updatePlot(SeriesSize seriesSize) { renderer.setBarGap(sbVariableWidth.getProgress()); if (BarRenderer.Style.STACKED.equals(spRenderStyle.getSelectedItem())) { - plot.setRangeTopMin(15); + plot.getInnerLimits().setMaxY(15); } else { - plot.setRangeTopMin(0); + plot.getInnerLimits().setMaxY(0); } plot.redraw(); @@ -355,8 +355,8 @@ private void onPlotClicked(PointF point) { double yDistance = 0; // find the closest value to the selection: - for (SeriesAndFormatter sfPair : plot - .getSeriesRegistry()) { + for (SeriesBundle sfPair : plot + .getRegistry().getSeriesAndFormatterList()) { XYSeries series = sfPair.getSeries(); for (int i = 0; i < series.size(); i++) { Number thisX = series.getX(i); diff --git a/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java b/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java index 1ba23dd6..47acf1cb 100644 --- a/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/ListViewActivity.java @@ -26,8 +26,7 @@ import android.widget.ArrayAdapter; import android.widget.ListView; import com.androidplot.Plot; -import com.androidplot.Series; -import com.androidplot.ui.SeriesAndFormatter; +import com.androidplot.ui.SeriesBundle; import com.androidplot.util.PixelUtils; import com.androidplot.xy.*; @@ -41,7 +40,7 @@ public class ListViewActivity extends Activity { private static final int NUM_SERIES_PER_PLOT = 5; private ListView lv; - private List>> seriesData = new ArrayList<>(NUM_PLOTS); + private List>> seriesData = new ArrayList<>(NUM_PLOTS); public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -55,7 +54,7 @@ public void onCreate(Bundle savedInstanceState) { protected void generateData() { Random generator = new Random(); for(int i = 0; i < NUM_PLOTS; i++) { - List> seriesList + List> seriesList = new ArrayList<>(NUM_SERIES_PER_PLOT); for (int k = 0; k < NUM_SERIES_PER_PLOT; k++) { @@ -83,7 +82,7 @@ protected void generateData() { lpf.setInterpolationParams( new CatmullRomInterpolator.Params(20, CatmullRomInterpolator.Type.Centripetal)); - seriesList.add(new SeriesAndFormatter( + seriesList.add(new SeriesBundle( new SimpleXYSeries(nums, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "S" + k), lpf)); } @@ -114,8 +113,8 @@ public View getView(int pos, View convertView, ViewGroup parent) { p.clear(); p.getTitle().setText("plot" + pos); - List> thisSeriesList = seriesData.get(pos); - for(SeriesAndFormatter sf : thisSeriesList) { + List> thisSeriesList = seriesData.get(pos); + for(SeriesBundle sf : thisSeriesList) { p.addSeries(sf.getSeries(), sf.getFormatter()); } p.redraw(); diff --git a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java index b72d1eba..3ef6d04f 100644 --- a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java @@ -38,7 +38,7 @@ // See: http://developer.android.com/reference/android/hardware/SensorEvent.html public class OrientationSensorExampleActivity extends Activity implements SensorEventListener { - private static final int HISTORY_SIZE = 300; + private static final int HISTORY_SIZE = 1000; private SensorManager sensorMgr = null; private Sensor orSensor = null; @@ -108,13 +108,13 @@ public void onCreate(Bundle savedInstanceState) { aprHistoryPlot.setRangeBoundaries(-180, 359, BoundaryMode.FIXED); aprHistoryPlot.setDomainBoundaries(0, HISTORY_SIZE, BoundaryMode.FIXED); aprHistoryPlot.addSeries(azimuthHistorySeries, - new FastLineAndPointRenderer.Formatter( + new LineAndPointFormatter( Color.rgb(100, 100, 200), null, null, null)); aprHistoryPlot.addSeries(pitchHistorySeries, - new FastLineAndPointRenderer.Formatter( + new LineAndPointFormatter( Color.rgb(100, 200, 100), null, null, null)); aprHistoryPlot.addSeries(rollHistorySeries, - new FastLineAndPointRenderer.Formatter( + new LineAndPointFormatter( Color.rgb(200, 100, 100), null, null, null)); aprHistoryPlot.setDomainStepMode(StepMode.INCREMENT_BY_VAL); aprHistoryPlot.setDomainStepValue(HISTORY_SIZE/10); diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java index fb18f7af..216af0ea 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java @@ -82,7 +82,7 @@ private SegmentFormatter getFormatter(Segment segment) { } private void deselectAll() { - List segments = pie.getSeriesRegistry().getSeriesList(); + List segments = pie.getRegistry().getSeriesList(); for(Segment segment : segments) { setSelected(segment, false); } diff --git a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java index ef9821cc..5dc26fcd 100644 --- a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java @@ -19,32 +19,23 @@ import java.text.DecimalFormat; import java.util.Random; -import android.app.Activity; +import android.app.*; import android.graphics.Color; -import android.graphics.PointF; -import android.os.Bundle; -import android.view.View; +import android.os.*; +import android.view.*; import android.widget.*; import com.androidplot.Plot; import com.androidplot.xy.*; -/*********************************** - * @author David Buezas (david.buezas at gmail.com) - * Feel free to copy, modify and use the source as it fits you. - * 09/27/2012 nfellows - updated for 0.5.1 and made a few simplifications - */ public class TouchZoomExampleActivity extends Activity { - private static final int SERIES_SIZE = 200; + private static final int SERIES_SIZE = 10000; private static final int SERIES_ALPHA = 255; private XYPlot plot; private PanZoom panZoom; private Button resetButton; private Spinner panSpinner; private Spinner zoomSpinner; - private SimpleXYSeries[] series = null; - private PointF minXY; - private PointF maxXY; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -53,10 +44,7 @@ public void onCreate(Bundle savedInstanceState) { resetButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - minXY.x = series[0].getX(0).floatValue(); - maxXY.x = series[3].getX(series[3].size() - 1).floatValue(); - plot.setDomainBoundaries(minXY.x, maxXY.x, BoundaryMode.FIXED); - plot.redraw(); + reset(); } }); plot = (XYPlot) findViewById(R.id.plot); @@ -65,8 +53,8 @@ public void onClick(View view) { // move dynamically with the data when the users pans or zooms: plot.setUserDomainOrigin(0); plot.setUserRangeOrigin(0); - plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 20); - plot.setRangeStep(StepMode.INCREMENT_BY_VAL, 10); + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1000); + plot.setRangeStep(StepMode.INCREMENT_BY_VAL, 100); panSpinner = (Spinner) findViewById(R.id.pan_spinner); zoomSpinner = (Spinner) findViewById(R.id.zoom_spinner); @@ -82,42 +70,73 @@ public void onClick(View view) { plot.setDomainLabel(""); plot.setBorderStyle(Plot.BorderStyle.NONE, null, null); - series = new SimpleXYSeries[4]; - int scale = 1; - for (int i = 0; i < 4; i++, scale *= 5) { - series[i] = new SimpleXYSeries("S" + i); - populateSeries(series[i], scale); + + panZoom = PanZoom.attach(plot); + plot.getOuterLimits().set(0, 10000, 0, 1000); + initSpinners(); + + // enable autoselect of sampling level based on visible boundaries: + plot.getRegistry().setEstimator(new ZoomEstimator()); + + if(savedInstanceState != null && savedInstanceState.containsKey("seriesRegistry")) { + XYSeriesRegistry registry = (XYSeriesRegistry) savedInstanceState.getSerializable("seriesRegistry"); + plot.setRegistry(registry); + } else { + generateSeriesData(); } - plot.addSeries(series[3], - new LineAndPointFormatter(Color.rgb(50, 0, 0), null, + reset(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putSerializable("seriesRegistry", plot.getRegistry()); + } + + private void reset() { + plot.setDomainBoundaries(0, 10000, BoundaryMode.FIXED); + plot.setRangeBoundaries(0, 1000, BoundaryMode.FIXED); + plot.redraw(); + } + + private ProgressDialog progress; + + private void generateSeriesData() { + progress = ProgressDialog.show(this, "Loading", "Please wait...", true); + new AsyncTask() { + + @Override + protected Object doInBackground(Object[] objects) { + generateAndAddSeries(625, new LineAndPointFormatter(Color.rgb(50, 0, 0), null, Color.argb(SERIES_ALPHA, 100, 0, 0), null)); - plot.addSeries(series[2], - new LineAndPointFormatter(Color.rgb(50, 50, 0), null, + generateAndAddSeries(125, new LineAndPointFormatter(Color.rgb(50, 50, 0), null, Color.argb(SERIES_ALPHA, 100, 100, 0), null)); - plot.addSeries(series[1], - new LineAndPointFormatter(Color.rgb(0, 50, 0), null, + generateAndAddSeries(25, new LineAndPointFormatter(Color.rgb(0, 50, 0), null, Color.argb(SERIES_ALPHA, 0, 100, 0), null)); - plot.addSeries(series[0], - new LineAndPointFormatter(Color.rgb(0, 0, 0), null, + generateAndAddSeries(5, new LineAndPointFormatter(Color.rgb(0, 0, 0), null, Color.argb(SERIES_ALPHA, 0, 0, 150), null)); - plot.redraw(); - - // record min/max for the reset button: - plot.calculateMinMaxVals(); - final RectRegion bounds = plot.getBounds(); - minXY = new PointF(bounds.getMinX().floatValue(), bounds.getMinY().floatValue()); - maxXY = new PointF(bounds.getMaxX().floatValue(), bounds.getMaxY().floatValue()); + return null; + } - // enable pan/zoom behavior: - panZoom = PanZoom.attach(plot); - initSpinners(); + @Override + protected void onPostExecute(Object result) { + progress.dismiss(); + plot.redraw(); + } + }.execute(); } - private void populateSeries(SimpleXYSeries series, int max) { + private void generateAndAddSeries(int max, LineAndPointFormatter formatter) { + final FixedSizeEditableXYSeries series = new FixedSizeEditableXYSeries("s" + max, SERIES_SIZE); Random r = new Random(); for(int i = 0; i < SERIES_SIZE; i++) { - series.addLast(i, r.nextInt(max)); + series.setX(i, i); + series.setY(r.nextInt(max), i); } + + // wrap our series in a SampledXYSeries with a threshold of 1000. + final SampledXYSeries sampledSeries = + new SampledXYSeries(series, OrderedXYSeries.XOrder.ASCENDING, 2,100); + plot.addSeries(sampledSeries, formatter); } private void initSpinners() { diff --git a/demoapp/src/main/java/com/androidplot/demos/Util.java b/demoapp/src/main/java/com/androidplot/demos/Util.java new file mode 100644 index 00000000..617f3976 --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/Util.java @@ -0,0 +1,17 @@ +package com.androidplot.demos; + +import android.app.*; +import android.content.*; + +/** + * Created by halfhp on 10/29/16. + */ +public class Util { + + + + public ProgressDialog showLoadingDialog(Context context) { + return ProgressDialog.show(context, "Loading", "Please wait...", true); + } + +} diff --git a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java index 50266b62..c9cc953d 100644 --- a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java @@ -142,7 +142,7 @@ private void onS2CheckBoxClicked() { } /** - * Processes a check box event + * Processes a run box event * @param cb The checkbox event origin * @param lpf LineAndPointFormatter with which rr and rf are to be added/removed * @param rf The XYRegionFormatter with which rr should be rendered diff --git a/demoapp/src/main/res/layout/touch_zoom_example.xml b/demoapp/src/main/res/layout/touch_zoom_example.xml index fee12de8..654b0c67 100644 --- a/demoapp/src/main/res/layout/touch_zoom_example.xml +++ b/demoapp/src/main/res/layout/touch_zoom_example.xml @@ -24,6 +24,7 @@ Date: Sat, 3 Dec 2016 09:44:20 -0600 Subject: [PATCH 02/86] * fixed a bug causing points scrolled off-screen to accumulate and render along the left edge of the graph (#20) * fixed a bug that would cause render jitter when extreme zoom levels were applied * fixed a bug that prevented PanZoom from working properly on plots that did not specify outer limits. * added basic implementation of a normalizing xyseries wrapper class * added dual scale xy example * added rotation property to Widget * added graphRotation XML attr to XYPlot --- .../java/com/androidplot/SeriesRegistry.java | 12 ++ .../com/androidplot/ui/LayoutManager.java | 1 - .../com/androidplot/ui/widget/Widget.java | 63 ++++++- .../java/com/androidplot/util/AttrUtils.java | 10 ++ .../com/androidplot/util/SeriesUtils.java | 21 ++- .../androidplot/xy/LineAndPointRenderer.java | 85 ++++++---- .../com/androidplot/xy/NormedXYSeries.java | 136 +++++++++++++++ .../main/java/com/androidplot/xy/PanZoom.java | 11 +- .../com/androidplot/xy/SimpleXYSeries.java | 52 ++++-- .../com/androidplot/xy/XYGraphWidget.java | 5 +- .../main/java/com/androidplot/xy/XYPlot.java | 6 + .../src/main/res/values/attrs.xml | 13 +- .../test/java/com/androidplot/RegionTest.java | 2 +- .../com/androidplot/SeriesRegistryTest.java | 136 +++++++++++++++ .../java/com/androidplot/test/TestUtils.java | 16 +- .../xy/LineAndPointRendererTest.java | 92 +++++++++- .../androidplot/xy/NormedXYSeriesTest.java | 87 ++++++++++ .../androidplot/xy/SampledXYSeriesTest.java | 2 +- build.gradle | 2 +- demoapp/src/main/AndroidManifest.xml | 1 + .../androidplot/demos/DualScaleActivity.java | 158 ++++++++++++++++++ .../com/androidplot/demos/MainActivity.java | 8 + .../demos/TouchZoomExampleActivity.java | 6 +- .../main/res/layout/dual_scale_example.xml | 35 ++++ demoapp/src/main/res/layout/main.xml | 5 + docs/advanced_xy_plot.md | 90 +++++++++- docs/quickstart.md | 2 +- docs/release_notes.md | 8 + docs/xyplot.md | 71 +++++++- 29 files changed, 1045 insertions(+), 91 deletions(-) create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java create mode 100644 androidplot-core/src/test/java/com/androidplot/SeriesRegistryTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/NormedXYSeriesTest.java create mode 100644 demoapp/src/main/java/com/androidplot/demos/DualScaleActivity.java create mode 100644 demoapp/src/main/res/layout/dual_scale_example.xml diff --git a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java index 948d1d11..78f3d805 100644 --- a/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java +++ b/androidplot-core/src/main/java/com/androidplot/SeriesRegistry.java @@ -54,6 +54,9 @@ public boolean isEmpty() { } public boolean add(SeriesType series, FormatterType formatter) { + if(series == null || formatter == null) { + throw new IllegalArgumentException("Neither series nor formatter param may be null."); + } return registry.add(newSeriesBundle(series, formatter)); } @@ -122,4 +125,13 @@ public List> getLegendEnabledItems() { } return sfList; } + + public boolean contains(SeriesType series, Class formatterClass) { + for(BundleType b : registry) { + if(b.getFormatter().getClass() == formatterClass && b.getSeries() == series) { + return true; + } + } + return false; + } } 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 013cb003..d9107442 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/LayoutManager.java @@ -85,7 +85,6 @@ public void draw(Canvas canvas) throws PlotRenderException { drawSpacing(canvas, displayDims.marginatedRect, displayDims.paddedRect, paddingPaint); } for (Widget widget : elements()) { - //int canvasState = canvas.save(Canvas.ALL_SAVE_FLAG); // preserve clipping etc try { canvas.save(Canvas.ALL_SAVE_FLAG); PositionMetrics metrics = widget.getPositionMetrics(); diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java index e12934f1..6009ce36 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java @@ -41,6 +41,15 @@ public abstract class Widget implements BoxModelable, Resizable { private PositionMetrics positionMetrics; private LayoutManager layoutManager; + private Rotation rotation = Rotation.NONE; + + public enum Rotation { + NINETY_DEGREES, + NEGATIVE_NINETY_DEGREES, + ONE_HUNDRED_EIGHTY_DEGREES, + NONE, + } + public Widget(LayoutManager layoutManager, SizeMetric heightMetric, SizeMetric widthMetric) { this(layoutManager, new Size(heightMetric, widthMetric)); } @@ -113,7 +122,6 @@ public void onPostInit() { * @return */ public boolean containsPoint(PointF point) { - //return outlineRect != null && outlineRect.contains(point.x, point.y); return widgetDimensions.canvasRect.contains(point.x, point.y); } @@ -344,14 +352,55 @@ public void draw(Canvas canvas) throws PlotRenderException { if (backgroundPaint != null) { drawBackground(canvas, widgetDimensions.canvasRect); } - doOnDraw(canvas, widgetDimensions.paddedRect); + canvas.save(); + final RectF paddedRect = applyRotation(canvas, widgetDimensions.paddedRect); + doOnDraw(canvas, paddedRect); + canvas.restore(); if (borderPaint != null) { - drawBorder(canvas, widgetDimensions.paddedRect); + drawBorder(canvas, paddedRect); } } } + protected RectF applyRotation(Canvas canvas, RectF rect) { + float rotationDegs = 0; + final float cx = widgetDimensions.paddedRect.centerX(); + final float cy = widgetDimensions.paddedRect.centerY(); + final float halfHeight = widgetDimensions.paddedRect.height() / 2; + final float halfWidth = widgetDimensions.paddedRect.width() / 2; + switch (rotation) { + case NINETY_DEGREES: + rotationDegs = 90; + rect = new RectF( + cx - halfHeight, + cy - halfWidth, + cx + halfHeight, + cy + halfWidth); + break; + case NEGATIVE_NINETY_DEGREES: + rotationDegs = -90; + rect = new RectF( + cx - halfHeight, + cy - halfWidth, + cx + halfHeight, + cy + halfWidth); + break; + case ONE_HUNDRED_EIGHTY_DEGREES: + rotationDegs = 180; + // fall through + case NONE: + break; + default: + throw new UnsupportedOperationException("Not yet implemented."); + + } + if(rotation != Rotation.NONE) { + canvas.rotate(rotationDegs, cx, cy); + } + return rect; + } + protected void drawBorder(Canvas canvas, RectF paddedRect) { canvas.drawRect(paddedRect, borderPaint); } @@ -405,4 +454,12 @@ public PositionMetrics getPositionMetrics() { public void setPositionMetrics(PositionMetrics positionMetrics) { this.positionMetrics = positionMetrics; } + + public Rotation getRotation() { + return rotation; + } + + public void setRotation(Rotation rotation) { + this.rotation = rotation; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java index b8b6c7f7..b34e7d47 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java @@ -188,6 +188,12 @@ public static void configureWidget(TypedArray attrs, Widget widget, int heightSi } } + public static void configureWidgetRotation(TypedArray attrs, Widget widget, int rotationAttr) { + if(attrs != null) { + widget.setRotation(getWidgetRotation(attrs, rotationAttr, Widget.Rotation.NONE)); + } + } + /** * Configure a {@link Widget} from xml attrs. * @param attrs @@ -245,6 +251,10 @@ private static VerticalPositioning getYLayoutStyle(TypedArray attrs, int attr, V return VerticalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())]; } + private static Widget.Rotation getWidgetRotation(TypedArray attrs, int attr, Widget.Rotation defaultValue) { + return Widget.Rotation.values()[attrs.getInt(attr, defaultValue.ordinal())]; + } + private static Anchor getAnchorPosition(TypedArray attrs, int attr, Anchor defaultValue) { return Anchor.values()[attrs.getInt(attr, defaultValue.ordinal())]; } 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 922e51b9..2b269757 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java @@ -34,6 +34,26 @@ public static RectRegion minMax(XYSeries... seriesList) { return minMax(null, seriesList); } + public static Region minMaxX(XYSeries... seriesList) { + final Region bounds = new Region(); + for (XYSeries series : seriesList) { + for (int i = 0; i < series.size(); i++) { + bounds.union(series.getX(i)); + } + } + return bounds; + } + + public static Region minMaxY(XYSeries... seriesList) { + final Region bounds = new Region(); + for (XYSeries series : seriesList) { + for (int i = 0; i < series.size(); i++) { + bounds.union(series.getY(i)); + } + } + return bounds; + } + /** * @param constraints may be null. * @param seriesList @@ -101,7 +121,6 @@ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArr public static Region minMax(Region bounds, List... lists) { for (final List list : lists) { for (final Number i : list) { - //minMax(bounds, i); bounds.union(i); } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java index 48d397be..bbe145d4 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java @@ -22,6 +22,8 @@ import android.graphics.PointF; import android.graphics.RectF; +import com.androidplot.Plot; +import com.androidplot.PlotListener; import com.androidplot.Region; import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; @@ -29,6 +31,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; /** * Renders a point as a line with the vertices marked. Requires 2 or more points to @@ -41,8 +44,22 @@ public class LineAndPointRenderer e private final Path path = new Path(); + protected final ConcurrentHashMap> pointsCaches + = new ConcurrentHashMap<>(2, 0.75f, 2); + public LineAndPointRenderer(XYPlot plot) { super(plot); + plot.addListener(new PlotListener() { + @Override + public void onBeforeDraw(Plot source, Canvas canvas) { + cullPointsCache(); + } + + @Override + public void onAfterDraw(Plot source, Canvas canvas) { + + } + }); } @Override @@ -77,17 +94,38 @@ protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) { path.lineTo(thisPoint.x, thisPoint.y); } - final ArrayList points = new ArrayList<>(); + /** + * Retrieves or initializes a list for storing calculated screen-coords to render as points. + * Also handles automatic resizing and culling of unused caches. + * Should only be called once per render cycle. + * @param series + * @return + */ + protected ArrayList getPointsCache(XYSeries series) { + ArrayList pointsCache = pointsCaches.get(series); + final int seriesSize = series.size(); + if(pointsCache == null) { + pointsCache = new ArrayList<>(seriesSize); + pointsCaches.put(series, pointsCache); + } - // avoids needless new allocations of the points array - protected void resizePointsArray(int newSize) { - if(points.size() < newSize) { - while(points.size() < newSize) { - points.add(null); + if(pointsCache.size() < seriesSize) { + while(pointsCache.size() < seriesSize) { + pointsCache.add(null); } - } else if(points.size() > newSize) { - while(points.size() > newSize) { - points.remove(0); + } else if(pointsCache.size() > seriesSize) { + while(pointsCache.size() > seriesSize) { + pointsCache.remove(0); + } + } + return pointsCache; + } + + protected void cullPointsCache() { + for(XYSeries series : pointsCaches.keySet()) { + if(!getPlot().getRegistry().contains(series, LineAndPointFormatter.class)) { + //pointsCaches.put(series, null); + pointsCaches.remove(series); } } } @@ -96,37 +134,27 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn PointF thisPoint; PointF lastPoint = null; PointF firstPoint = null; - final int seriesSize = series.size(); path.reset(); - resizePointsArray(seriesSize); + final List points = getPointsCache(series); int iStart = 0; - int iEnd = seriesSize; + int iEnd = series.size(); if(SeriesUtils.getXYOrder(series) == OrderedXYSeries.XOrder.ASCENDING) { final Region iBounds = SeriesUtils.iBounds(series, getPlot().getBounds()); iStart = iBounds.getMin().intValue(); if(iStart > 0) { iStart--; } - iEnd = iBounds.getMax().intValue(); - if(iEnd < seriesSize - 1) { + iEnd = iBounds.getMax().intValue() + 1; + if(iEnd < series.size() - 1) { iEnd++; } } - final double minX = getPlot().getBounds().getMinX().doubleValue(); - final double maxX = getPlot().getBounds().getMaxX().doubleValue(); for (int i = iStart; i < iEnd; i++) { final Number y = series.getY(i); final Number x = series.getX(i); PointF iPoint = points.get(i); - final double dx = x.doubleValue(); - if(i > 0 && i < seriesSize - 1) { - if (dx < minX || dx > maxX) { - continue; - } - } - if (y != null && x != null) { if(iPoint == null) { iPoint = new PointF(); @@ -187,7 +215,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter); } } - renderPoints(canvas, plotArea, series, points, formatter); + renderPoints(canvas, plotArea, series, iStart, iEnd, points, formatter); } /** @@ -209,16 +237,15 @@ protected PointF convertPoint(XYCoords coord, RectF plotArea) { return getPlot().getBounds().transformScreen(coord, plotArea); } - protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, List points, + protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, int iStart, int iEnd, List points, LineAndPointFormatter formatter) { - //PointLabelFormatter plf = formatter.getPointLabelFormatter(); if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) { - int i = 0; final Paint vertexPaint = formatter.hasVertexPaint() ? formatter.getVertexPaint() : null; final boolean hasPointLabelFormatter = formatter.hasPointLabelFormatter(); final PointLabelFormatter plf = hasPointLabelFormatter ? formatter.getPointLabelFormatter() : null; final PointLabeler pointLabeler = hasPointLabelFormatter ? formatter.getPointLabeler() : null; - for (PointF p : points) { + for(int i = iStart; i < iEnd; i++) { + PointF p = points.get(i); // if vertexPaint is available, draw vertex: if (vertexPaint != null) { @@ -227,11 +254,9 @@ protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, List // if textPaint and pointLabeler are available, draw point's text label: if (pointLabeler != null) { - //final PointLabelFormatter plf = formatter.getPointLabelFormatter(); canvas.drawText(pointLabeler.getLabel(series, i), p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint()); } - i++; } } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java new file mode 100644 index 00000000..03760b39 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/NormedXYSeries.java @@ -0,0 +1,136 @@ +package com.androidplot.xy; + +import com.androidplot.Region; +import com.androidplot.util.SeriesUtils; + +/** + * Wrapper implementation of {@link XYSeries} that wraps another XYSeries, normalizing values in the range of 0 to 1. + * Note that it's possible to push normed values outside of the standard 0, 1 range by applying + * a sufficiently large offset. + */ +public class NormedXYSeries implements XYSeries { + + private XYSeries rawData; + + private Region minMaxX; + private Region minMaxY; + + private Region transformX; + private Region transformY; + + public static class Norm { + + final Region minMax; + final double offset; + final boolean useOffsetCompression; + + public Norm(Region minMax) { + this(minMax, 0, false); + } + + /** + * + * @param minMax Boundary to use when calculating the norm coefficient. Set to null to let + * Androidplot auto calculate the bounds. (Very inefficient) + * @param offset An extra offset to apply, generally within the range of -1 and 1. + * This value is useful for adjusting the positioning of a series relative to another normalized series. + * @param useOffsetCompression If true, the offset value will result in further scaling down + * of the series data in order to ensure that all points within the specified bounds remain + * visible on the screen. If set to true, the specified offset MUST be > -1 and < 1. Will be + * ignored if bounds != null. + */ + public Norm(Region minMax, double offset, boolean useOffsetCompression) { + this.minMax = minMax; + this.offset = offset; + this.useOffsetCompression = useOffsetCompression; + + if (useOffsetCompression && (offset <= -1 || offset >= 1)) { + throw new IllegalArgumentException( + "When useOffsetCompression is true, offset must be > -1 and < 1."); + } + } + } + + /** + * Normalizes yVals only, auto calculating min/max. + * @param rawData + */ + public NormedXYSeries(XYSeries rawData) { + this(rawData, null, new Norm(null, 0, false)); + } + + /** + * + * @param rawData The XYSeries to be normalized. + * @param x Normalization to apply to xVals. Set to null to disable normalization on the x axis. + * @param y Normalization to apply to yVals. Set to null to disable normalization on the y axis. + */ + public NormedXYSeries(XYSeries rawData, Norm x, Norm y) { + this.rawData = rawData; + normalize(x, y); + } + + protected void normalize(Norm x, Norm y) { + if( x != null) { + this.minMaxX = x.minMax != null ? x.minMax : SeriesUtils.minMaxX(rawData); + this.transformX = calculateTransform(x); + } + + if( y != null) { + this.minMaxY = y.minMax != null ? y.minMax : SeriesUtils.minMaxY(rawData); + this.transformY = calculateTransform(y); + } + } + + protected Region calculateTransform(Norm norm) { + if(norm.useOffsetCompression) { + return new Region( + norm.offset > 0 ? norm.offset : 0, + norm.offset < 0 ? 1 + norm.offset : 1); + } else { + return new Region(0 + norm.offset, 1 + norm.offset); + } + } + + @Override + public String getTitle() { + return rawData.getTitle(); + } + + @Override + public int size() { + return rawData.size(); + } + + public Number denormalizeXVal(Number xVal) { + if(xVal != null) { + return transformX.transform(xVal.doubleValue(), minMaxX); + } + return null; + } + + public Number denormalizeYVal(Number yVal) { + if(yVal != null) { + return transformY.transform(yVal.doubleValue(), minMaxY); + } + return null; + } + + @Override + public Number getX(int index) { + final Number xVal = rawData.getX(index); + if(xVal != null && transformX != null) { + return minMaxX.transform(xVal.doubleValue(), transformX); + } + return xVal; + } + + @Override + public Number getY(int index) { + final Number yVal = rawData.getY(index); + if(yVal != null && transformY != null) { + return minMaxY.transform(yVal.doubleValue(), transformY); + } + return yVal; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java index 441edb6f..00faa739 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java @@ -186,12 +186,13 @@ protected void pan(final MotionEvent motionEvent) { final PointF oldFirstFinger = firstFingerPos; //save old position of finger firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position - Region newBounds = new Region(); if (EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) { + Region newBounds = new Region(); calculatePan(oldFirstFinger, newBounds, true); plot.setDomainBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); } if (EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) { + Region newBounds = new Region(); calculatePan(oldFirstFinger, newBounds, false); plot.setRangeBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED); } @@ -335,9 +336,9 @@ protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) { if (isHorizontal ) { final RectRegion limits = plot.getOuterLimits(); + newRect.left = midPoint - offset; + newRect.right = midPoint + offset; if(limits.isFullyDefined()) { - newRect.left = midPoint - offset; - newRect.right = midPoint + offset; if (newRect.left < limits.getMinX().floatValue()) { newRect.left = limits.getMinX().floatValue(); } @@ -347,9 +348,9 @@ protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) { } } else { final RectRegion limits = plot.getOuterLimits(); + newRect.top = midPoint - offset; + newRect.bottom = midPoint + offset; if(limits.isFullyDefined()) { - newRect.top = midPoint - offset; - newRect.bottom = midPoint + offset; if (newRect.top < limits.getMinY().floatValue()) { newRect.top = limits.getMinY().floatValue(); } 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 2032f301..f5ed5458 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java @@ -19,7 +19,6 @@ import android.graphics.Canvas; import com.androidplot.Plot; import com.androidplot.PlotListener; -import com.androidplot.util.*; import java.util.*; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -28,31 +27,22 @@ /** * A convenience class used to create instances of XYPlot generated from Lists of Numbers. */ -public class SimpleXYSeries implements EditableXYSeries, PlotListener { +public class SimpleXYSeries implements EditableXYSeries, OrderedXYSeries, PlotListener { private static final String TAG = SimpleXYSeries.class.getName(); - @Override - public void onBeforeDraw(Plot source, Canvas canvas) { - lock.readLock().lock(); - } + private volatile LinkedList xVals = new LinkedList<>(); + private volatile LinkedList yVals = new LinkedList<>(); + private volatile String title = null; - @Override - public void onAfterDraw(Plot source, Canvas canvas) { - lock.readLock().unlock(); - } + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + private XOrder xOrder = XOrder.NONE; public enum ArrayFormat { Y_VALS_ONLY, XY_VALS_INTERLEAVED } - private volatile LinkedList xVals = new LinkedList<>(); - private volatile LinkedList yVals = new LinkedList<>(); - private volatile String title = null; - - private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); - public SimpleXYSeries(String title) { this.title = title; } @@ -61,6 +51,36 @@ public SimpleXYSeries(ArrayFormat format, String title, Number... model) { this(asNumberList(model), format, title); } + /** + * Retrieve the current x-ordering specified for this series. Default is + * {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE}. + * @return + */ + @Override + public XOrder getXOrder() { + return xOrder; + } + + /** + * If XVals are in strict ascending order, use this method to set + * {@link com.androidplot.xy.OrderedXYSeries.XOrder#ASCENDING} to provide an optimization + * hint to the renderer. + * @param xOrder + */ + public void setXOrder(XOrder xOrder) { + this.xOrder = xOrder; + } + + @Override + public void onBeforeDraw(Plot source, Canvas canvas) { + lock.readLock().lock(); + } + + @Override + public void onAfterDraw(Plot source, Canvas canvas) { + lock.readLock().unlock(); + } + protected static List asNumberList(Number... model) { List numbers = new ArrayList<>(); for(Number n : model) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java index 63a74f5b..b474addc 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java @@ -355,7 +355,10 @@ public void processAttrs(TypedArray attrs) { R.styleable.xy_XYPlot_rangeTitleVerticalPositioning, R.styleable.xy_XYPlot_rangeTitleVerticalPosition, R.styleable.xy_XYPlot_rangeTitleAnchor, R.styleable.xy_XYPlot_rangeTitleVisible); - // graphWidget + // rotation + AttrUtils.configureWidgetRotation(attrs, this, R.styleable.xy_XYPlot_graphRotation); + + // padding & margin AttrUtils.configureBoxModelable(attrs, this, R.styleable.xy_XYPlot_graphMarginTop, R.styleable.xy_XYPlot_graphMarginBottom, R.styleable.xy_XYPlot_graphMarginLeft, R.styleable.xy_XYPlot_graphMarginRight, 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 5563f29b..d1230ea1 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -93,6 +93,12 @@ public class XYPlot extends Plot + + + + + + + + + @@ -327,6 +336,7 @@ + @@ -383,8 +393,7 @@ - + diff --git a/androidplot-core/src/test/java/com/androidplot/RegionTest.java b/androidplot-core/src/test/java/com/androidplot/RegionTest.java index 1dbc6dc9..c38ded3c 100644 --- a/androidplot-core/src/test/java/com/androidplot/RegionTest.java +++ b/androidplot-core/src/test/java/com/androidplot/RegionTest.java @@ -20,7 +20,7 @@ import org.junit.Before; import org.junit.Test; -import static junit.framework.Assert.fail; +import static junit.framework.Assert.assertNotSame; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/androidplot-core/src/test/java/com/androidplot/SeriesRegistryTest.java b/androidplot-core/src/test/java/com/androidplot/SeriesRegistryTest.java new file mode 100644 index 00000000..7352193f --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/SeriesRegistryTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot; + +import com.androidplot.test.AndroidplotTest; +import com.androidplot.ui.Formatter; +import com.androidplot.xy.BarFormatter; +import com.androidplot.xy.LineAndPointFormatter; +import com.androidplot.xy.SimpleXYSeries; +import com.androidplot.xy.XYSeriesRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class SeriesRegistryTest extends AndroidplotTest { + + SeriesRegistry seriesRegistry; + + @Before + public void setUp() throws Exception { + seriesRegistry = new XYSeriesRegistry(); + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testAdd() { + assertEquals(0, seriesRegistry.size()); + seriesRegistry.add(new SimpleXYSeries("s1"), new LineAndPointFormatter()); + assertEquals(1, seriesRegistry.size()); + } + + @Test + public void testAdd_failsOnNullArgument() throws Exception { + try { + seriesRegistry.add(null, null); + fail("IllegalArgumentException expected."); + } catch(IllegalArgumentException e) { + // expected + } + + try { + seriesRegistry.add(new SimpleXYSeries("s1"), null); + fail("IllegalArgumentException expected."); + } catch(IllegalArgumentException e) { + // expected + } + + try { + seriesRegistry.add(null, new LineAndPointFormatter()); + fail("IllegalArgumentException expected."); + } catch(IllegalArgumentException e) { + // expected + } + } + + @Test + public void testGet() { + Series s1 = new SimpleXYSeries("s1"); + Formatter f1 = new LineAndPointFormatter(); + Series s2 = new SimpleXYSeries("s2"); + Formatter f2 = new LineAndPointFormatter(); + Formatter f3 = new LineAndPointFormatter(); + seriesRegistry.add(s1, f1); + seriesRegistry.add(s1, f3); + seriesRegistry.add(s2, f2); + + + assertEquals(2, seriesRegistry.get(s1).size()); + assertEquals(1, seriesRegistry.get(s2).size()); + } + + @Test + public void testRemove() { + Series series = new SimpleXYSeries("s1"); + seriesRegistry.add(series, new LineAndPointFormatter()); + assertEquals(1, seriesRegistry.size()); + + seriesRegistry.remove(new SimpleXYSeries("s2")); + assertEquals(1, seriesRegistry.size()); + + seriesRegistry.remove(series); + assertEquals(0, seriesRegistry.size()); + } + + @Test + public void testClear() { + seriesRegistry.add(new SimpleXYSeries("s1"), new LineAndPointFormatter()); + seriesRegistry.add(new SimpleXYSeries("s2"), new LineAndPointFormatter()); + assertEquals(2, seriesRegistry.size()); + + seriesRegistry.clear(); + assertEquals(0, seriesRegistry.size()); + + } + + @Test + public void testContains() { + Series s1 = new SimpleXYSeries("s1"); + Series s2 = new SimpleXYSeries("s1"); + Series s3 = new SimpleXYSeries("s1"); + + seriesRegistry.add(s1, new LineAndPointFormatter()); + seriesRegistry.add(s2, new LineAndPointFormatter()); + + assertTrue(seriesRegistry.contains(s1, LineAndPointFormatter.class)); + assertFalse(seriesRegistry.contains(s1, BarFormatter.class)); + assertTrue(seriesRegistry.contains(s2, LineAndPointFormatter.class)); + assertFalse(seriesRegistry.contains(s3, LineAndPointFormatter.class)); + + } + +} diff --git a/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java index 93e9143d..ecb78333 100644 --- a/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java +++ b/androidplot-core/src/test/java/com/androidplot/test/TestUtils.java @@ -10,14 +10,26 @@ import static org.robolectric.Shadows.shadowOf; /** - * Created by halfhp on 10/8/16. + * Utilities to help with unit testing */ public abstract class TestUtils { public static XYSeries generateXYSeries(String title, int size) { + return generateXYSeries(title, size, 0, 1); + } + + /** + * Generate a series of random numbers within a min/max range + * @param title + * @param size + * @param min + * @param max + * @return + */ + public static XYSeries generateXYSeries(String title, int size, double min, double max) { SimpleXYSeries series = new SimpleXYSeries(title); for(int i = 0; i < size; i++) { - series.addLast(i, Math.random()); + series.addLast(i, Math.random() * max - min); } return series; } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java index e42eef65..ef67bb36 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java @@ -21,7 +21,6 @@ import com.androidplot.test.*; import org.junit.*; -import org.junit.runner.*; import org.mockito.*; import java.util.*; @@ -60,12 +59,10 @@ public void testDrawSeries_withInterpolation() throws Exception { * @throws Exception */ @Test - public void testRenderPoints() throws Exception { + public void testDrawSeries() throws Exception { // 100x100 plot space: - RectF plotArea = new RectF(0, 0, 99, 99); - XYPlot plot = new XYPlot(getContext(), "Test"); - + plotArea = new RectF(0, 0, 99, 99); FastLineAndPointRenderer.Formatter formatter = new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null); @@ -73,12 +70,12 @@ public void testRenderPoints() throws Exception { XYSeries series = new SimpleXYSeries( SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - LineAndPointRenderer renderer = Mockito.spy(new LineAndPointRenderer(plot)); + LineAndPointRenderer renderer = Mockito.spy(new LineAndPointRenderer(xyPlot)); - plot.addSeries(series, formatter); + xyPlot.addSeries(series, formatter); - plot.calculateMinMaxVals(); - renderer.onRender(canvas, plotArea, series, formatter, null); + xyPlot.calculateMinMaxVals(); + renderer.drawSeries(canvas, plotArea, series, formatter); PointF[] expectedPoints = new PointF[] { new PointF(0, 99), @@ -98,6 +95,8 @@ public void testRenderPoints() throws Exception { eq(canvas), eq(plotArea), eq(series), + eq(0), + eq(expectedPoints.length), capturedPoints.capture(), eq(formatter)); @@ -143,4 +142,79 @@ public void testRenderPoints() throws Exception { assertEquals(expectedPoints[9].x, pList.get(9).x); assertEquals(expectedPoints[9].y, pList.get(9).y); } + + @Test + public void testDrawSeries_supportsOrderedXYSeries() throws Exception { + // 100x100 plot space: + //RectF plotArea = new RectF(0, 0, 99, 99); + FastLineAndPointRenderer.Formatter formatter = + new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null); + + // create a series composed of 3 "segments"; series portions separated by null values: + SimpleXYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + LineAndPointRenderer renderer = Mockito.spy(new LineAndPointRenderer(xyPlot)); + + xyPlot.addSeries(series, formatter); + + xyPlot.calculateMinMaxVals(); + renderer.drawSeries(canvas, plotArea, series, formatter); + + verify(renderer, times(1)).renderPoints( + eq(canvas), + eq(plotArea), + eq(series), + eq(0), + eq(series.size()), + any(List.class), + eq(formatter)); + + xyPlot.setDomainBoundaries(5, 6, BoundaryMode.FIXED); + series.setXOrder(OrderedXYSeries.XOrder.ASCENDING); + xyPlot.calculateMinMaxVals(); + renderer.drawSeries(canvas, plotArea, series, formatter); + + verify(renderer, times(1)).renderPoints( + eq(canvas), + eq(plotArea), + eq(series), + eq(4), + eq(8), + any(List.class), + eq(formatter)); + + } + + @Test + public void testCullPointsCache() throws Exception { + LineAndPointFormatter formatter = + new LineAndPointFormatter(0, 0, 0, null); + + // create a series composed of 3 "segments"; series portions separated by null values: + SimpleXYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + xyPlot.addSeries(series, formatter); + LineAndPointRenderer renderer = xyPlot.getRenderer(LineAndPointRenderer.class); + + assertEquals(0, renderer.pointsCaches.size()); + + // should generate a new pointCache: + renderer.getPointsCache(series); + assertEquals(1, renderer.pointsCaches.size()); + + // culling should not delete it since it is + // registered in the series registry: + renderer.getPointsCache(series); + assertEquals(1, renderer.pointsCaches.size()); + renderer.cullPointsCache(); + assertEquals(1, renderer.pointsCaches.size()); + + // unregister the series. this time, culling should remove the series + // from the points cache: + xyPlot.removeSeries(series); + renderer.cullPointsCache(); + assertEquals(0, renderer.pointsCaches.size()); + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/NormedXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/NormedXYSeriesTest.java new file mode 100644 index 00000000..8e8b341d --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/NormedXYSeriesTest.java @@ -0,0 +1,87 @@ +package com.androidplot.xy; + +import com.androidplot.test.AndroidplotTest; +import com.androidplot.test.TestUtils; + +import org.junit.Ignore; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.spy; + +/** + * Tests {@link NormedXYSeries}. + */ +public class NormedXYSeriesTest extends AndroidplotTest { + + // account for precision issues inherent in floating point math: + private static final double DELTA = 0.0000001; + + @Test + public void testConstructor_withNoOffset() { + XYSeries rawData = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 2, 4, 6, 8, 10); + NormedXYSeries normedData = new NormedXYSeries(rawData, + new NormedXYSeries.Norm(null), + new NormedXYSeries.Norm(null)); + + assertEquals(0d, normedData.getY(0).doubleValue(), DELTA); + assertEquals(0.2d, normedData.getY(1).doubleValue(), DELTA); + assertEquals(0.4d, normedData.getY(2).doubleValue(), DELTA); + assertEquals(0.6d, normedData.getY(3).doubleValue(), DELTA); + assertEquals(0.8d, normedData.getY(4).doubleValue(), DELTA); + assertEquals(1.0d, normedData.getY(5).doubleValue(), DELTA); + } + + @Test + public void testConstructor_withNullYVals() { + XYSeries rawData = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, + "s1", + 0, null, 4, null, 8, 10); + NormedXYSeries normedData = new NormedXYSeries(rawData, + new NormedXYSeries.Norm(null), + new NormedXYSeries.Norm(null)); + + assertEquals(0d, normedData.getY(0).doubleValue(), DELTA); + assertEquals(0.4d, normedData.getY(2).doubleValue(), DELTA); + assertEquals(0.8d, normedData.getY(4).doubleValue(), DELTA); + assertEquals(1.0d, normedData.getY(5).doubleValue(), DELTA); + } + + @Test + public void testConstructor_withPositiveOffsetAndOffsetCompression() { + XYSeries rawData = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 2, 4, 6, 8, 10); + NormedXYSeries normedData = new NormedXYSeries(rawData, + new NormedXYSeries.Norm(null, 0.5, true), + new NormedXYSeries.Norm(null, 0.5, true)); + + assertEquals(0.5d, normedData.getY(0).doubleValue(), DELTA); + assertEquals(0.6d, normedData.getY(1).doubleValue(), DELTA); + assertEquals(1.0d, normedData.getY(5).doubleValue(), DELTA); + } + + @Test + public void testConstructor_withNegativeOffsetAndOffsetCompression() { + XYSeries rawData = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 2, 4, 6, 8, 10); + NormedXYSeries normedData = new NormedXYSeries(rawData, + new NormedXYSeries.Norm(null, -0.5, true), + new NormedXYSeries.Norm(null, -0.5, true)); + + assertEquals(0d, normedData.getY(0).doubleValue(), DELTA); + assertEquals(0.1d, normedData.getY(1).doubleValue(), DELTA); + assertEquals(0.2d, normedData.getY(2).doubleValue(), DELTA); + assertEquals(0.5d, normedData.getY(5).doubleValue(), DELTA); + } + + @Test + public void testConstructor_withOffsetAndNoOffsetCompression() { + XYSeries rawData = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 2, 4, 6, 8, 10); + NormedXYSeries normedData = new NormedXYSeries(rawData, + new NormedXYSeries.Norm(null, 0.5, false), + new NormedXYSeries.Norm(null, 0.5, false)); + + assertEquals(0.5d, normedData.getY(0).doubleValue(), DELTA); + assertEquals(0.7d, normedData.getY(1).doubleValue(), DELTA); + assertEquals(1.5d, normedData.getY(5).doubleValue(), DELTA); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java index df07b4f5..95cea342 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/SampledXYSeriesTest.java @@ -9,7 +9,7 @@ import static org.mockito.Mockito.verify; /** - * Created by halfhp on 10/8/16. + * Tests {@link SampledXYSeries} */ public class SampledXYSeriesTest extends AndroidplotTest { diff --git a/build.gradle b/build.gradle index d74e02dc..0cd58cd4 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 24 theTargetSdkVersion = 24 theMinSdkVersion = 5 - theVersionName = '1.3.0' + theVersionName = '1.3.1' theVersionCode = 0 } diff --git a/demoapp/src/main/AndroidManifest.xml b/demoapp/src/main/AndroidManifest.xml index 09d9e761..f7ac2f6f 100644 --- a/demoapp/src/main/AndroidManifest.xml +++ b/demoapp/src/main/AndroidManifest.xml @@ -99,6 +99,7 @@ + + + + + + + \ No newline at end of file diff --git a/demoapp/src/main/res/layout/main.xml b/demoapp/src/main/res/layout/main.xml index 7295e172..82ca3831 100644 --- a/demoapp/src/main/res/layout/main.xml +++ b/demoapp/src/main/res/layout/main.xml @@ -60,6 +60,11 @@ style="@style/toc_button" android:text="Realtime Orientation Sensor Plot"/> +