From 3e18f395f9d9416b66462268a1295f98282fa790 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Fri, 16 Dec 2016 11:24:32 -0600 Subject: [PATCH 01/84] 1.4.0 Changes (#22) * documentation * * more documentation * domain/range cursors are now disabled by default * * better support for converting series<->screen coordinate values. * documentation and unit tests for the above. * * Added ScalingXYSeries implementation * Added AnimatedXYPlotActivity to the DemoApp demonstrating ScalingXYSeries being used to create an animated intro. * Updated documentation & unit tests. * deleted SeriesAnimator * BarRenderer refactoring / unit tests * More BarRenderer refactorings. * More BarRenderer refactorings. * More BarRenderer refactorings. * documentation * more documentation * * added margin params to BarFormatter. * more documentation * * more unit tests for PieRenderer * more documentation for pie charts. * * refactoring of PieRenderer * piechart documentation updates * documentation, uprev to 1.4.0 * code cleanup, added intro animation to SimplePieChartActivity * corrected release notes --- README.md | 2 +- .../java/com/androidplot/pie/PieRenderer.java | 214 ++++--- .../java/com/androidplot/ui/RenderStack.java | 3 - .../com/androidplot/ui/SeriesRenderer.java | 1 - .../java/com/androidplot/util/RectFUtils.java | 20 + .../java/com/androidplot/util/Redrawer.java | 2 +- .../java/com/androidplot/xy/BarFormatter.java | 37 ++ .../java/com/androidplot/xy/BarRenderer.java | 576 +++++++++--------- .../xy/FastLineAndPointRenderer.java | 5 - .../com/androidplot/xy/ScalingXYSeries.java | 67 ++ .../com/androidplot/xy/XYGraphWidget.java | 107 +++- .../main/java/com/androidplot/xy/XYPlot.java | 79 ++- .../com/androidplot/pie/PieRendererTest.java | 92 ++- .../com/androidplot/xy/BarRendererTest.java | 290 +++++++++ .../androidplot/xy/ScalingXYSeriesTest.java | 58 ++ .../com/androidplot/xy/XYGraphWidgetTest.java | 80 ++- .../java/com/androidplot/xy/XYPlotTest.java | 12 - build.gradle | 2 +- demoapp/src/main/AndroidManifest.xml | 6 + .../demos/AnimatedXYPlotActivity.java | 155 +++++ .../demos/BarPlotExampleActivity.java | 36 +- .../com/androidplot/demos/MainActivity.java | 8 + .../OrientationSensorExampleActivity.java | 4 +- .../demos/SimplePieChartActivity.java | 70 ++- demoapp/src/main/res/layout/main.xml | 5 + docs/advanced_xy_plot.md | 7 + docs/barchart.md | 109 +++- docs/bubblechart.md | 29 +- docs/candlestick.md | 79 ++- docs/contributing.md | 56 ++ docs/images/bargroup_overlaid.png | Bin 0 -> 2940 bytes docs/images/bargroup_side_by_side.png | Bin 0 -> 2711 bytes docs/images/bargroup_spacing.png | Bin 0 -> 4600 bytes docs/images/bargroup_stacked.png | Bin 0 -> 3011 bytes docs/images/pie_donut_size.png | Bin 0 -> 18616 bytes docs/images/pie_extent.png | Bin 0 -> 18494 bytes docs/images/pie_inner_inset.png | Bin 0 -> 12925 bytes docs/images/pie_offset.png | Bin 0 -> 12916 bytes docs/images/pie_orientation.png | Bin 0 -> 20823 bytes docs/images/pie_outer_inset.png | Bin 0 -> 12514 bytes docs/images/pie_radial_inset.png | Bin 0 -> 13320 bytes docs/index.md | 5 +- docs/piechart.md | 76 ++- docs/quickstart.md | 4 +- docs/release_notes.md | 24 +- docs/versioning.md | 15 + docs/xyplot.md | 166 +++-- 47 files changed, 1944 insertions(+), 557 deletions(-) create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/ScalingXYSeriesTest.java create mode 100644 demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java create mode 100644 docs/contributing.md create mode 100644 docs/images/bargroup_overlaid.png create mode 100644 docs/images/bargroup_side_by_side.png create mode 100644 docs/images/bargroup_spacing.png create mode 100644 docs/images/bargroup_stacked.png create mode 100644 docs/images/pie_donut_size.png create mode 100644 docs/images/pie_extent.png create mode 100644 docs/images/pie_inner_inset.png create mode 100644 docs/images/pie_offset.png create mode 100644 docs/images/pie_orientation.png create mode 100644 docs/images/pie_outer_inset.png create mode 100644 docs/images/pie_radial_inset.png create mode 100644 docs/versioning.md diff --git a/README.md b/README.md index 7736e7d4..a4d72f57 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ compatible with all versions of Android from 1.6 onward and is **used by over * [Demo App (Google Play Store)](https://play.google.com/store/apps/details?id=com.androidplot.demos&hl=en) * [Demo App Soure Code](https://bitbucket.org/androidplot/androidplot/src/1538c5dfa56aed0d2cfdcbc7cdc6173e605543cd/demoapp/?at=master) * [Bugs](https://github.com/halfhp/androidplot/issues) :ant: -* [Contributing Source Code](http://androidplot.com/docs/working-with-androidplot-source/) +* [Contributing Source Code](docs/contributing.md) # Help Technical questions should be posted using the [androidplot tag](http://stackoverflow.com/questions/tagged/androidplot) on Stack Overflow. For everything else use the [Google Groups forum](https://groups.google.com/d/forum/androidplot). 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 5d52d94b..8b663a82 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java @@ -31,9 +31,10 @@ public class PieRenderer extends SeriesRenderer { // starting angle to use when drawing the first radial line of the first segment. - @SuppressWarnings("FieldCanBeLocal") - private float startDeg = 0; - private float endDeg = 360; + private float startDegs = 0; + + // number of degrees to extend from startDegs; can be used to "shape" the pie chart. + private float extentDegs = 360; // TODO: express donut in units other than px. private float donutSize = 0.5f; @@ -41,7 +42,6 @@ public class PieRenderer extends SeriesRenderer sfPair : getSeriesAndFormatterList()) { float lastOffset = offset; - float sweep = (float) (scale * (values[i]) * 360); + float sweep = (float) (scale * (values[i]) * extentDegs); offset += sweep; - drawSegment(canvas, rec, sfPair.getSeries(), sfPair.getFormatter(), radius, lastOffset, sweep); + drawSegment(canvas, rec, sfPair.getSeries(), sfPair.getFormatter(), radius, lastOffset, + sweep); i++; } } protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f, - float rad, float startAngle, float sweep) { + float rad, float startAngle, float sweep) { canvas.save(); startAngle = startAngle + f.getRadialInset(); sweep = sweep - (f.getRadialInset() * 2); // midpoint angle between startAngle and endAngle - final float halfSweepEndAngle = startAngle + (sweep/2); + final float halfSweepEndAngle = startAngle + (sweep / 2); PointF translated = calculateLineEnd( bounds.centerX(), bounds.centerY(), f.getOffset(), halfSweepEndAngle); @@ -95,24 +98,22 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm final float cy = translated.y; float donutSizePx; - switch(donutMode) { + switch (donutMode) { case PERCENT: donutSizePx = donutSize * rad; break; case PIXELS: - donutSizePx = (donutSize > 0)?donutSize:(rad + donutSize); + donutSizePx = (donutSize > 0) ? donutSize : (rad + donutSize); break; default: - throw new UnsupportedOperationException("Not yet implemented."); + throw new UnsupportedOperationException("Unsupported DonutMde: " + donutMode); } final float outerRad = rad - f.getOuterInset(); - final float innerRad = donutSizePx == 0 ? 0 : donutSizePx + f.getInnerInset(); - //final float outerRad = rad; - //final float innerRad = donutSizePx; + final float innerRad = donutSizePx == 0 ? 0 : donutSizePx + f.getInnerInset(); - // do we have a pie chart of less than 100% - if(Math.abs(sweep - 360f) > Float.MIN_VALUE) { + // do we have a segment of less than 100% + if (Math.abs(sweep - extentDegs) > Float.MIN_VALUE) { // vertices of the first radial: PointF r1Outer = calculateLineEnd(cx, cy, outerRad, startAngle); PointF r1Inner = calculateLineEnd(cx, cy, innerRad, startAngle); @@ -129,9 +130,9 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm // and cannot be easily anti aliased. Really we only care about masking off the // radial edges. clip.arcTo(new RectF(bounds.left - outerRad, - bounds.top - outerRad, - bounds.right + outerRad, - bounds.bottom + outerRad), + bounds.top - outerRad, + bounds.right + outerRad, + bounds.bottom + outerRad), startAngle, sweep); clip.lineTo(cx, cy); clip.close(); @@ -141,20 +142,20 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm // outer arc: p.arcTo(new RectF( - cx - outerRad, - cy - outerRad, - cx + outerRad, - cy + outerRad) + cx - outerRad, + cy - outerRad, + cx + outerRad, + cy + outerRad) , startAngle, sweep); p.lineTo(r2Inner.x, r2Inner.y); // inner arc: // sweep back to original angle: p.arcTo(new RectF( - cx - innerRad, - cy - innerRad, - cx + innerRad, - cy + innerRad), + cx - innerRad, + cy - innerRad, + cx + innerRad, + cy + innerRad), startAngle + sweep, -sweep); p.close(); @@ -165,8 +166,7 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm // draw radial lines canvas.drawLine(r1Inner.x, r1Inner.y, r1Outer.x, r1Outer.y, f.getRadialEdgePaint()); canvas.drawLine(r2Inner.x, r2Inner.y, r2Outer.x, r2Outer.y, f.getRadialEdgePaint()); - } - else { + } else { canvas.save(Canvas.CLIP_SAVE_FLAG); Path chart = new Path(); chart.addCircle(cx, cy, outerRad, Path.Direction.CW); @@ -185,18 +185,18 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm canvas.restore(); PointF labelOrigin = calculateLineEnd(cx, cy, - (outerRad-((outerRad- innerRad)/2)), halfSweepEndAngle); + (outerRad - ((outerRad - innerRad) / 2)), halfSweepEndAngle); // TODO: move segment labelling outside the segment drawing loop // TODO: so that the labels will not be clipped by the edge of the next // TODO: segment being drawn. - if(f.getLabelPaint() != null) { + if (f.getLabelPaint() != null) { drawSegmentLabel(canvas, labelOrigin, seg, f); } } protected void drawSegmentLabel(Canvas canvas, PointF origin, - Segment seg, SegmentFormatter f) { + Segment seg, SegmentFormatter f) { canvas.drawText(seg.getTitle(), origin.x, origin.y, f.getLabelPaint()); } @@ -213,22 +213,26 @@ protected void doDrawLegendIcon(Canvas canvas, RectF rect, SegmentFormatter form protected double calculateScale(double[] values) { double total = 0; for (int i = 0; i < values.length; i++) { - total += values[i]; - } + total += values[i]; + } return (1d / total); } - - protected double[] getValues() { + + /** + * Retreive the raw values being rendered from each {@link Segment}. + * @return + */ + protected double[] getValues() { List> seriesList = getSeriesAndFormatterList(); - double[] result = new double[seriesList.size()]; - int i = 0; - for (SeriesBundle sfPair : seriesList) { - result[i] = sfPair.getSeries().getValue().doubleValue(); - i++; - } - return result; - } + double[] result = new double[seriesList.size()]; + int i = 0; + for (SeriesBundle sfPair : seriesList) { + result[i] = sfPair.getSeries().getValue().doubleValue(); + i++; + } + return result; + } protected PointF calculateLineEnd(float x, float y, float rad, float deg) { return calculateLineEnd(new PointF(x, y), rad, deg); @@ -244,16 +248,26 @@ protected PointF calculateLineEnd(PointF origin, float rad, float deg) { return new PointF(origin.x + (float) x, origin.y + (float) y); } + /** + * Set the size of the pie's empty inner space. May be specified as either pixels or as a percentage. + * If using {@link DonutMode#PIXELS}, the best practice is to specify values in dp using + * {@link com.androidplot.util.PixelUtils#dpToPix(float)}. + * + * If using {@link DonutMode#PERCENT} the value must be within the range 0 - 1. The value being + * set corresponds to the size of the donut radius relative to the pie's total radius. + * @param size + * @param mode + */ public void setDonutSize(float size, DonutMode mode) { - switch(mode) { + switch (mode) { case PERCENT: - if(size < 0 || size > 1) { + if (size < 0 || size > 1) { throw new IllegalArgumentException( "Size parameter must be between 0 and 1 when operating in PERCENT mode."); } break; case PIXELS: - break; + break; default: throw new UnsupportedOperationException("Not yet implemented."); } @@ -264,7 +278,7 @@ public void setDonutSize(float size, DonutMode mode) { /** * Retrieve the segment containing the specified point. This current implementation * only matches against angle; clicks outside of the pie/donut inner/outer boundaries - * will still trigger a match on the segment whose begining and ending angle contains + * will still trigger a match on the segment whose beginning and ending angle contains * the angle of the line drawn between the pie chart's center point and the clicked point. * @param point The clicked point * @return Segment containing the clicked point. @@ -278,11 +292,11 @@ public Segment getContainingSegment(PointF point) { float dx = point.x - origin.x; float dy = point.y - origin.y; double theta = Math.atan2(dy, dx); - double angle = (theta * (180f/Math.PI)); - if(angle < 0) { + double angle = (theta * (180f / Math.PI)); + if (angle < 0) { // convert angle to 0-360 range with 0 being in the - // traditional "westerly" orientation: - angle += 360; + // traditional "east" orientation: + angle += 360f; } // find the segment whose starting and ending angle (degs) contains @@ -291,33 +305,91 @@ public Segment getContainingSegment(PointF point) { int i = 0; double[] values = getValues(); double scale = calculateScale(values); - float offset = startDeg; + float offset = degsToScreenDegs(startDegs); for (SeriesBundle sfPair : seriesList) { float lastOffset = offset; - float sweep = (float) (scale * (values[i]) * 360); + float sweep = (float) (scale * (values[i]) * extentDegs); offset += sweep; + offset = offset % 360; - if(angle >= lastOffset && angle <= offset) { + final double dist = signedDistance(offset, angle); + if(dist > 0 && dist <= signedDistance(offset, lastOffset)) { return sfPair.getSeries(); } i++; } return null; } - - public void setStartDeg(float deg) { - startDeg = deg; + + /** + * convert conventional degrees (90 degrees is north) to screen degrees (90 degrees is south) + * Values >= 369 will be converted back into the range of 0 - 359.999... + * @param degs + * @return + */ + protected static float degsToScreenDegs(float degs) { + degs = degs % 360; + + if (degs > 0) { + return 360 - degs; + } else { + return degs; + } + } + + /** + * Compute the signed shortest angular distance between two angles + * @param angle1 + * @param angle2 + * @return + */ + protected static double signedDistance(double angle1, double angle2) { + double d = Math.abs(angle1 - angle2) % 360; + double r = d > 180 ? 360 - d : d; + + //calculate sign + int sign = (angle1 - angle2 >= 0 && angle1 - angle2 <= 180) + || (angle1 - angle2 <= -180 && angle1 - angle2 >= -360) ? 1 : -1; + r *= sign; + return r; } - public float getStartDeg() { - return startDeg; + /** + * Throws an IllegalArgumentException if the input value is outside of the range of 0.0 to 360.0 + * @param degs + */ + protected static void validateInputDegs(float degs) { + if(degs < 0 || degs > 360) { + throw new IllegalArgumentException("Degrees values must be between 0.0 and 360."); + } } - - public void setEndDeg(float deg) { - endDeg = deg; + + /** + * Set the starting point in degrees from which series will be drawn in order. + * The input value must be within the range 0 - 360. + * @param degs + */ + public void setStartDegs(float degs) { + validateInputDegs(degs); + startDegs = degs; + } + + public float getStartDegs() { + return startDegs; + } + + /** + * Set the size in degrees of the pie chart, extending from the startDegs. The input value + * must be within the range 0 - 360. An input value would represent 100% of the pie as a half + * circle while an input value of 360 would be a full circle. + * @param degs + */ + public void setExtentDegs(float degs) { + validateInputDegs(degs); + extentDegs = degs; } - public float getEndDeg() { - return endDeg; + public float getExtentDegs() { + return extentDegs; } } 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 a1a07c25..d9b9d812 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java @@ -80,9 +80,6 @@ public RenderStack(Plot plot) { * rendering series data to an XYGraphWidget. */ public void sync() { - /** - * TODO: rendering performance *might* be improved by reusing StackElement instances but I'm skeptical... - */ getElements().clear(); List> pairList = plot.getRegistry().getSeriesAndFormatterList(); 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 f535ee32..882bdcfc 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SeriesRenderer.java @@ -97,7 +97,6 @@ public void drawSeriesLegendIcon(Canvas canvas, RectF rect, SeriesFormatterType 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); diff --git a/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java b/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java index 0b5db607..3915e2ad 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java @@ -59,4 +59,24 @@ public static RectF applyInsets(RectF rect, Insets insets) { return rect; } } + + /** + * Generates a RectF from two height and two width values; the h and w values will + * be passed into the RectF constructor such that RectF.left <= RectF.right and + * RectF.top <= RectF.bottom. + * @param w1 width1 + * @param h1 height1 + * @param w2 width2 + * @param h2 height2 + * @return + */ + public static RectF createFromEdges(float w1, float h1, float w2, float h2) { + final boolean w1IsLeft = w1 <= w2; + final boolean h1IsTop = h1 <= h2; + return new RectF( + w1IsLeft ? w1 : w2, + h1IsTop ? h1 : h2, + w1IsLeft ? w2 : w1, + h1IsTop ? h2 : h1); + } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java index 266541d2..00174027 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java +++ b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java @@ -26,7 +26,7 @@ /** * Utility class for invoking Plot.redraw() on a background thread - * at a set frequency. Callers should be sure to create a separate thread from which to use this class. + * at a set frequency. */ public class Redrawer implements Runnable { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java index ca7bcbea..3272748d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java @@ -40,6 +40,11 @@ public void setBorderPaint(Paint borderPaint) { private Paint fillPaint; private Paint borderPaint; + private float marginTop; + private float marginBottom; + private float marginLeft; + private float marginRight; + /** * Should only be used in conjunction with calls to configure()... */ @@ -72,4 +77,36 @@ public Class getRendererClass() { public SeriesRenderer doGetRendererInstance(XYPlot plot) { return new BarRenderer(plot); } + + public float getMarginTop() { + return marginTop; + } + + public void setMarginTop(float marginTop) { + this.marginTop = marginTop; + } + + public float getMarginBottom() { + return marginBottom; + } + + public void setMarginBottom(float marginBottom) { + this.marginBottom = marginBottom; + } + + public float getMarginLeft() { + return marginLeft; + } + + public void setMarginLeft(float marginLeft) { + this.marginLeft = marginLeft; + } + + public float getMarginRight() { + return marginRight; + } + + public void setMarginRight(float marginRight) { + this.marginRight = marginRight; + } } 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 108d0c6c..c696bbf2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java @@ -20,105 +20,96 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Map.Entry; -import java.util.TreeMap; import android.graphics.Canvas; import android.graphics.RectF; import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesBundle; +import com.androidplot.util.PixelUtils; +import com.androidplot.util.RectFUtils; /** - * Renders the points in an XYSeries as bars. + * Renders the points in an XYSeries as bars. See {@link BarOrientation} javadoc for details on supported + * presentation styles. + * */ public class BarRenderer extends GroupRenderer { - private Style style = Style.OVERLAID; // default Render Style - private BarWidthMode barWidthMode = BarWidthMode.FIXED_WIDTH; // default Width Style - private float barWidth = 5; - private float barGap = 1; - private Comparator barComparator = new BarComparator(); + private BarOrientation barOrientation = BarOrientation.OVERLAID; // default Render Style + private BarGroupWidthMode barGroupWidthMode = BarGroupWidthMode.FIXED_WIDTH; // default Width Style - /** - * How bars should be laid out when in a group of 2 or more series. - */ - public enum Style { + /** + * Represents the size in pixels of either bar width or bar gap width, depending on the current + * value of barWidthMode. + */ + private float width = PixelUtils.dpToPix(3); + + /** + * How bars should be laid out when in a group of 2 or more series. + */ + public enum BarOrientation { + + /** + * Bars are drawn "overlapping" one another, with taller bars being drawn behind + * the shorter ones. + */ OVERLAID, // bars are overlaid in descending y-val order (largest val in back) + + /** + * Bars are stacked on top of one another so that the sum of their yVals produces the final + * height of that bar. + */ STACKED, // bars are drawn stacked vertically on top of each other + + /** + * Bars are drawn next to one another, grouped by iVal + */ SIDE_BY_SIDE // bars are drawn horizontally next to each-other } - /** - * Mode with which to calculate the width of each bar. - */ - public enum BarWidthMode { - FIXED_WIDTH, // bar width is always barWidth - VARIABLE_WIDTH // bar width is calculated so that there is only barGap between each bar + /** + * Mode with which to calculate the width of each bar. + */ + public enum BarGroupWidthMode { + FIXED_WIDTH, // bar width is always barWidth + FIXED_GAP // bar width is calculated relative to a fixed gap width between each bar } public BarRenderer(XYPlot plot) { super(plot); } - /** - * Sets the width of the bars when using the FIXED_WIDTH render style - * @param barWidth - */ - public void setBarWidth(float barWidth) { - this.barWidth = barWidth; + public void setBarOrientation(BarOrientation renderBarOrientation) { + this.barOrientation = renderBarOrientation; } - /** - * Sets the size of the gap between the bar (or bar groups) when using the VARIABLE_WIDTH render style - * @param barGap - */ - public void setBarGap(float barGap) { - this.barGap = barGap; + public BarOrientation getBarOrientation() { + return this.barOrientation; } - public void setStyle(Style renderStyle) { - this.style = renderStyle; + public BarGroupWidthMode getBarGroupWidthMode() { + return this.barGroupWidthMode; } - public Style getStyle() { - return this.style; - } - - public void setBarWidthMode(BarWidthMode widthStyle) { - this.barWidthMode = widthStyle; + public float getBarGroupWidth() { + return this.width; } - public BarWidthMode getBarWidthMode() { - return this.barWidthMode; - } - - public void setBarWidth(BarWidthMode mode, float value) { - setBarWidthMode(mode); - switch (mode) { - case FIXED_WIDTH: - setBarWidth(value); - break; - case VARIABLE_WIDTH: - setBarGap(value); - break; - default: - break; - } + public void setBarGroupWidth(BarGroupWidthMode mode, float width) { + this.barGroupWidthMode = mode; + this.width = width; } - - /** - * Sets a {@link Comparator} used for sorting bars. - */ - public void setBarComparator(Comparator barComparator) { - this.barComparator = barComparator; + + protected BarComparator getBarComparator(float rangeOriginPx) { + return new BarComparator(getBarOrientation(), rangeOriginPx); } @Override public void doDrawLegendIcon(Canvas canvas, RectF rect, BarFormatter formatter) { - if(formatter.hasFillPaint()) { - canvas.drawRect(rect, formatter.getFillPaint()); - } + if (formatter.hasFillPaint()) { + canvas.drawRect(rect, formatter.getFillPaint()); + } canvas.drawRect(rect, formatter.getBorderPaint()); } @@ -134,274 +125,253 @@ public FormatterType getFormatter(int index, XYSeries series) { return null; } + @Override + public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { - @Override - public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { - - TreeMap axisMap = new TreeMap(); + List barGroups = new ArrayList<>(); /* * Build the axisMap (yVal,BarGroup)... a TreeMap of BarGroups * BarGroups represent a point on the X axis where a single or group of bars need to be drawn. */ - for(SeriesBundle thisPair : sfList) { - BarGroup barGroup; - - // For each value in the series - for(int i = 0; i < seriesSize; i++) { - - if (thisPair.getSeries().getX(i) != null) { - - // get a new bar object - Bar bar = new Bar(thisPair.getSeries(), thisPair.getFormatter(),i,plotArea); - - // Find or create the barGroup - if (axisMap.containsKey(bar.intX)) { - barGroup = axisMap.get(bar.intX); - } else { - barGroup = new BarGroup(bar.intX,plotArea); - axisMap.put(bar.intX, barGroup); - } - barGroup.addBar(bar); - } + for(int i = 0; i < seriesSize; i++) { + final BarGroup group = new BarGroup(i, 0, plotArea); + int seriesOrder = 0; + for(SeriesBundle bundle : sfList) { + // TODO: is this null check really necessary? + if(bundle.getSeries().getX(i) != null) { + Bar bar = new Bar(getPlot(), bundle.getSeries(), + bundle.getFormatter(), seriesOrder, i, plotArea); + group.addBar(bar); + group.centerPix = bar.xPix; + } + seriesOrder++; } + barGroups.add(group); } - // Loop through the axisMap linking up prev pointers - BarGroup prev, current; - prev = null; - for(Entry mapEntry : axisMap.entrySet()) { - current = mapEntry.getValue(); - current.prev = prev; - prev = current; - } - - // The default gap between each bar section - int gap = (int) barGap; - - // Determine roughly how wide (rough_width) this bar should be. This is then used as a default width - // when there are gaps in the data or for the first/last bars. - float f_rough_width = ((plotArea.width() - ((axisMap.size() - 1) * gap)) / (axisMap.size() - 1)); - int rough_width = (int) f_rough_width; - if (rough_width < 0) rough_width = 0; - if (gap > rough_width) { - gap = rough_width / 2; - } - /* - * Calculate the dimensions of each barGroup and then draw each bar within it according to + * Calculate the dimensions of each barGroup and then draw each bar within it according to * the Render Style and Width Style. */ - for(Number key : axisMap.keySet()) { - - BarGroup barGroup = axisMap.get(key); - - // Determine the exact left and right X for the Bar Group - switch (barWidthMode) { - case FIXED_WIDTH: - // use intX and go halfwidth either side. - barGroup.leftX = barGroup.intX - (int) (barWidth / 2); - barGroup.width = (int) barWidth; - barGroup.rightX = barGroup.leftX + barGroup.width; - break; - case VARIABLE_WIDTH: - if (barGroup.prev != null) { - if (barGroup.intX - barGroup.prev.intX - gap - 1 > (int)(rough_width * 1.5)) { - // use intX and go halfwidth either side. - barGroup.leftX = barGroup.intX - (rough_width / 2); - barGroup.width = rough_width; - barGroup.rightX = barGroup.leftX + barGroup.width; - } else { - // base left off prev right to get the gap correct. - barGroup.leftX = barGroup.prev.rightX + gap + 1; - if (barGroup.leftX > barGroup.intX) barGroup.leftX = barGroup.intX; - // base right off intX + halfwidth. - barGroup.rightX = barGroup.intX + (rough_width / 2); - // calculate the width - barGroup.width = barGroup.rightX - barGroup.leftX; - } - } else { - // use intX and go halfwidth either side. - barGroup.leftX = barGroup.intX - (rough_width / 2); - barGroup.width = rough_width; - barGroup.rightX = barGroup.leftX + barGroup.width; - } - break; - default: - break; - } + final int groupCount = barGroups.size(); + for(BarGroup barGroup : barGroups) { + + // Determine the exact left and right X for the Bar Group + switch (barGroupWidthMode) { + case FIXED_WIDTH: + barGroup.leftPix = barGroup.centerPix - (width / 2); + barGroup.rightPix = barGroup.leftPix + width; + break; + case FIXED_GAP: + float barWidth = plotArea.width(); + if(groupCount > 1) { + barWidth = (barGroups.get(1).centerPix - barGroups.get(0).centerPix) - width; + } + + final float halfWidth = barWidth / 2; + barGroup.leftPix = barGroup.centerPix - halfWidth; + barGroup.rightPix = barGroup.centerPix + halfWidth; + break; + default: + break; + } /* * Draw the bars within the barGroup area. */ double rangeOrigin = getPlot().getRangeOrigin().doubleValue(); - float basePositionY = (float) getPlot().getBounds().yRegion - .transform(rangeOrigin, plotArea.top, plotArea.bottom, true); - - switch (style) { - case OVERLAID: - Collections.sort(barGroup.bars, barComparator); - for (Bar bar : barGroup.bars) { - BarFormatter formatter = bar.getFormatter(); - PointLabelFormatter plf = formatter.hasPointLabelFormatter() - ? formatter.getPointLabelFormatter() : null; - PointLabeler pointLabeler = null; - if (formatter != null) { - pointLabeler = formatter.getPointLabeler(); - } - - if (bar.yVal= 2) { - canvas.drawRect(bar.barGroup.leftX, basePositionY, bar.barGroup.rightX, bar.intY, formatter.getFillPaint()); - } - canvas.drawRect(bar.barGroup.leftX, basePositionY, bar.barGroup.rightX, bar.intY, formatter.getBorderPaint()); - } else { // rising bar - if (formatter.hasFillPaint() && bar.barGroup.width >= 2) { - canvas.drawRect(bar.barGroup.leftX, bar.intY, bar.barGroup.rightX, basePositionY, formatter.getFillPaint()); - } - canvas.drawRect(bar.barGroup.leftX, bar.intY, bar.barGroup.rightX, basePositionY, formatter.getBorderPaint()); - } - if(plf != null && plf.hasTextPaint() && pointLabeler != null) { - canvas.drawText(pointLabeler.getLabel(bar.series, bar.seriesIndex), bar.intX + plf.hOffset, bar.intY + plf.vOffset, plf.getTextPaint()); - } - } - break; - case SIDE_BY_SIDE: - int width = barGroup.width / barGroup.bars.size(); - int leftX = barGroup.leftX; - Collections.sort(barGroup.bars, barComparator); - for (Bar bar : barGroup.bars) { - BarFormatter formatter = bar.getFormatter(); - PointLabelFormatter plf = formatter.hasPointLabelFormatter() - ? formatter.getPointLabelFormatter() : null; - PointLabeler pointLabeler = null; - if (formatter != null) { - pointLabeler = formatter.getPointLabeler(); - } - - if (bar.yVal= 2) { - canvas.drawRect(leftX, basePositionY, leftX + width, bar.intY, formatter.getFillPaint()); - } - canvas.drawRect(leftX, basePositionY, leftX + width, bar.intY, formatter.getBorderPaint()); - } else { // rising bar - if (formatter.hasFillPaint() && bar.barGroup.width >= 2) { - canvas.drawRect(leftX, bar.intY, leftX + width, basePositionY, formatter.getFillPaint()); - } - canvas.drawRect(leftX, bar.intY, leftX + width, basePositionY, formatter.getBorderPaint()); - } - if(plf != null && plf.hasTextPaint() && pointLabeler != null) { - canvas.drawText(pointLabeler.getLabel(bar.series, bar.seriesIndex), leftX + width/2 + plf.hOffset, bar.intY + plf.vOffset, plf.getTextPaint()); - } - leftX = leftX + width; - } - break; - case STACKED: - int bottom = (int) barGroup.plotArea.bottom; - Collections.sort(barGroup.bars, barComparator); - for (Bar b : barGroup.bars) { - BarFormatter formatter = b.getFormatter(); - PointLabelFormatter plf = formatter.hasPointLabelFormatter() - ? formatter.getPointLabelFormatter() : null; - PointLabeler pointLabeler = null; - if (formatter != null) { - pointLabeler = formatter.getPointLabeler(); - } - int height = (int) b.barGroup.plotArea.bottom - b.intY; - int top = bottom - height; - if (formatter.hasFillPaint() && b.barGroup.width >= 2) { - canvas.drawRect(b.barGroup.leftX, top, b.barGroup.rightX, bottom, formatter.getFillPaint()); - } - - canvas.drawRect(b.barGroup.leftX, top, b.barGroup.rightX, bottom, formatter.getBorderPaint()); - if(plf != null && plf.hasTextPaint() && pointLabeler != null) { - //canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), b.intX + plf.hOffset, b.intY + plf.vOffset, plf.getTextPaint()); - // b.intY should be replaced by top as Text label should be drawn on top of each bar - canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), b.intX + plf.hOffset, top + plf.vOffset, plf.getTextPaint()); - } - bottom = top; - } - break; - default: - break; - } - } + float rangeOriginPx = (float) getPlot().getBounds().yRegion + .transform(rangeOrigin, plotArea.top, plotArea.bottom, true); + + final BarComparator comparator = getBarComparator(rangeOriginPx); + switch (barOrientation) { + case OVERLAID: + Collections.sort(barGroup.bars, comparator); + for (Bar bar : barGroup.bars) { + drawBar(canvas, bar, createBarRect( + bar.barGroup.leftPix, + bar.yPix, + bar.barGroup.rightPix, + rangeOriginPx, bar.formatter)); + } + break; + case SIDE_BY_SIDE: + final float width = barGroup.getWidth() / barGroup.bars.size(); + float leftX = barGroup.leftPix; + Collections.sort(barGroup.bars, comparator); + for (Bar bar : barGroup.bars) { + drawBar(canvas, bar, createBarRect( + leftX, bar.yPix, + leftX + width, rangeOriginPx, + bar.formatter)); + leftX = leftX + width; + } + break; + case STACKED: + float bottom = (int) barGroup.plotArea.bottom; + Collections.sort(barGroup.bars, comparator); + for (Bar bar : barGroup.bars) { + // TODO: handling sub range-origin values for the purpose of labeling + final float height = (int) bar.barGroup.plotArea.bottom - bar.yPix; + final float top = bottom - height; + drawBar(canvas, bar, createBarRect( + bar.barGroup.leftPix, top, + bar.barGroup.rightPix, bottom, + bar.formatter)); + bottom = top; + } + break; + default: + throw new UnsupportedOperationException("Unexpected BarOrientation: " + barOrientation); + } + } + } + + protected RectF createBarRect(float w1, float h1, float w2, float h2, BarFormatter formatter) { + final RectF result = RectFUtils.createFromEdges(w1, h1,w2, h2); + result.left += formatter.getMarginLeft(); + result.right -= formatter.getMarginRight(); + result.top += formatter.getMarginTop(); + result.bottom -= formatter.getMarginBottom(); + return result; + } + + protected void drawBar(Canvas canvas, Bar bar, RectF rect) { + + // null yVals are skipped: + if(bar.getY() == null) { + return; + } + + BarFormatter formatter = getFormatter(bar.i, bar.series); + if(formatter == null) { + formatter = bar.formatter; + } + if(rect.height() > 0 && rect.width() > 0) { + if (formatter.hasFillPaint()) { + canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom, + formatter.getFillPaint()); + } + + if (formatter.hasLinePaint()) { + canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom, + formatter.getBorderPaint()); + } + } + + PointLabelFormatter plf = + formatter.hasPointLabelFormatter() ? formatter.getPointLabelFormatter() : null; + + PointLabeler pointLabeler = + formatter != null ? formatter.getPointLabeler() : null; + + if (plf != null && plf.hasTextPaint() && pointLabeler != null) { + canvas.drawText(pointLabeler.getLabel(bar.series, bar.i), + rect.centerX() + plf.hOffset, bar.yPix + plf.vOffset, + plf.getTextPaint()); + } } - public class Bar { + /** + * + * @param + */ + public static class Bar { + public final XYSeries series; - private final FormatterType formatter; - public final int seriesIndex; - public final double yVal, xVal; - public final int intX, intY; - public final double pixX, pixY; - protected BarGroup barGroup; - - public Bar(XYSeries series, FormatterType formatter, int seriesIndex, RectF plotArea) { - this.series = series; + public final FormatterType formatter; + public final int i; + public final int seriesOrder; + public final float xPix; + public final float yPix; + protected BarGroup barGroup; + + // TODO: factor out plot param + public Bar(XYPlot plot, XYSeries series, FormatterType formatter, int seriesOrder, int i, RectF plotArea) { + this.series = series; this.formatter = formatter; - this.seriesIndex = seriesIndex; - - this.xVal = series.getX(seriesIndex).doubleValue(); - this.pixX = getPlot().getBounds().getxRegion() - .transform(xVal, plotArea.left, plotArea.right, false); - this.intX = (int) Math.round(pixX); - - if (series.getY(seriesIndex) != null) { - this.yVal = series.getY(seriesIndex).doubleValue(); - this.pixY = getPlot().getBounds().yRegion - .transform(yVal, plotArea.top, plotArea.bottom, true); - this.intY = (int) Math.round(pixY); - } else { - this.yVal = 0; - this.pixY = plotArea.bottom; - this.intY = (int) pixY; - } - } - - public FormatterType getFormatter() { - FormatterType f = BarRenderer.this.getFormatter(seriesIndex, series); - return f != null ? f : formatter; + this.i = i; + this.seriesOrder = seriesOrder; + + final double xVal = series.getX(i).doubleValue(); + xPix = (float) plot.getBounds().getxRegion() + .transform(xVal, plotArea.left, plotArea.right, false); + + if (series.getY(i) != null) { + final double yVal = series.getY(i).doubleValue(); + this.yPix = (float) plot.getBounds().yRegion + .transform(yVal, plotArea.top, plotArea.bottom, true); + } else { + this.yPix = 0; + } + } + + public Number getY() { + return series.getY(i); } } - - private class BarGroup { - public ArrayList bars; - public int intX; - public int width, leftX, rightX; - public RectF plotArea; - public BarGroup prev; - - public BarGroup(int intX, RectF plotArea) { - // Setup the TreeMap with the required comparator - this.bars = new ArrayList(); // create a comparator that compares series title given the index. - this.intX = intX; - this.plotArea = plotArea; - } - - public void addBar(Bar bar) { - bar.barGroup = this; - this.bars.add(bar); - } + + /** + * A collection of one or more {@Bar} instances sharing a common iVal. + */ + private static class BarGroup { + + public ArrayList bars; + public int i; + public float centerPix; + public float leftPix; + public float rightPix; + public RectF plotArea; + + public BarGroup(int i, float centerPix, RectF plotArea) { + // Setup the TreeMap with the required comparator + this.bars = new ArrayList<>(); // create a comparator that compares series title given the index. + this.centerPix = centerPix; + this.plotArea = plotArea; + this.i = i; + } + + public void addBar(Bar bar) { + bar.barGroup = this; + this.bars.add(bar); + } + + protected float getWidth() { + return rightPix - leftPix; + } } + /** + * Used to determine the order in which bars of the same group will be drawn. + */ @SuppressWarnings("WeakerAccess") - public class BarComparator implements Comparator{ + public static class BarComparator implements Comparator { + + private final BarOrientation barOrientation; + private final float rangeOriginPx; + + public BarComparator(BarOrientation barOrientation, float rangeOriginPx) { + this.rangeOriginPx = rangeOriginPx; + this.barOrientation = barOrientation; + } @Override public int compare(Bar bar1, Bar bar2) { - switch (style) { - case OVERLAID: - return Integer.valueOf(bar1.intY).compareTo(bar2.intY); - case SIDE_BY_SIDE: - return bar1.series.getTitle().compareToIgnoreCase(bar2.series.getTitle()); - case STACKED: - return bar1.series.getTitle().compareToIgnoreCase(bar2.series.getTitle()); - default: - return 0; - } + switch (barOrientation) { + case OVERLAID: + if(bar1.yPix > rangeOriginPx && bar2.yPix > rangeOriginPx) { + return Float.valueOf(bar2.yPix).compareTo(bar1.yPix); + } else { + return Float.valueOf(bar1.yPix).compareTo(bar2.yPix); + } + case SIDE_BY_SIDE: + case STACKED: + default: + return Integer.valueOf(bar1.seriesOrder).compareTo(bar2.seriesOrder); + } } - } - + } + } 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 6a646649..e5aa7bc9 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java @@ -51,11 +51,6 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte points = new float[series.size()*2]; } -// final Number minX = getPlot().getCalculatedMinX(); -// final Number maxX = getPlot().getCalculatedMaxX(); -// final Number minY = getPlot().getCalculatedMinY(); -// final Number maxY = getPlot().getCalculatedMaxY(); - int segmentLen = 0; boolean isLastPointNull = true; for (int i = 0, j = 0; i < series.size(); i++, j+=2) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java new file mode 100644 index 00000000..2e194f17 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/ScalingXYSeries.java @@ -0,0 +1,67 @@ +package com.androidplot.xy; + +/** + * Wraps an existing {@link XYSeries} allowing easy scaling of that series' xy values. + */ +public class ScalingXYSeries implements XYSeries { + + private double scale; + private XYSeries series; + private Mode mode; + + public enum Mode { + X_ONLY, + Y_ONLY, + X_AND_Y + } + + /** + * + * @param series The {@link XYSeries} to be scaled + * @param scale The initial scale to be applied + * @param mode Determines which axis (or both) to which scaling will be applied. + */ + public ScalingXYSeries(XYSeries series, double scale, Mode mode) { + this.series = series; + this.scale = scale; + this.mode = mode; + } + + @Override + public String getTitle() { + return series.getTitle(); + } + + @Override + public int size() { + return series.size(); + } + + @Override + public Number getX(int index) { + Number x = series.getX(index); + if(mode == Mode.X_ONLY || mode == Mode.X_AND_Y) { + return x == null ? null : x.doubleValue() * scale; + } else { + return x; + } + } + + @Override + public Number getY(int index) { + Number y = series.getY(index); + if(mode == Mode.Y_ONLY || mode == Mode.X_AND_Y) { + return y == null ? null : y.doubleValue() * scale; + } else { + return y; + } + } + + public double getScale() { + return scale; + } + + public void setScale(double scale) { + this.scale = scale; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java index b474addc..7f9114b4 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java @@ -112,8 +112,8 @@ public class XYGraphWidget extends Widget { private Paint domainOriginLinePaint; private Paint rangeOriginLinePaint; - private float domainCursorPosition; - private float rangeCursorPosition; + private Float domainCursorPosition; + private Float rangeCursorPosition; private boolean drawMarkersEnabled = true; private boolean drawGridOnTop; @@ -391,46 +391,54 @@ public void processAttrs(TypedArray attrs) { } /** - * Convenience method. Wraps getYVal(float) + * Convenience method. Wraps screenToSeriesY(float) + * This is a relatively slow operation and should not be used for operations that are a part of + * the main render loop of a dynamic plot. * * @param point * @return */ - public Number getYVal(PointF point) { - return getYVal(point.y); + protected XYCoords screenToSeries(PointF point) { + if(!plot.getBounds().isFullyDefined()) { + return null; + } + return new RectRegion(gridRect) + .transform(point.x, point.y, plot.getBounds(), false, true); } /** - * Converts a y pixel to a y value. + * Convenience method. Wraps screenToSeriesX(float) + * This is a relatively slow operation and should not be used for operations that are a part of + * the main render loop of a dynamic plot. * - * @param yPix + * @param point * @return */ - public Number getYVal(float yPix) { - if (!plot.getBounds().getyRegion().isDefined()) { - return null; - } - return new Region(gridRect.top, gridRect.bottom) - .transform(yPix, plot.getBounds().getyRegion(), true); + protected Number screenToSeriesX(PointF point) { + return screenToSeriesX(point.x); } /** - * Convenience method. Wraps getXVal(float) + * Convenience method. Wraps screenToSeriesY(float) + * This is a relatively slow operation and should not be used for operations that are a part of + * the main render loop of a dynamic plot. * * @param point * @return */ - public Number getXVal(PointF point) { - return getXVal(point.x); + protected Number screenToSeriesY(PointF point) { + return screenToSeriesY(point.y); } /** * Converts an x pixel into an x value. + * This is a relatively slow operation and should not be used for operations that are a part of + * the main render loop of a dynamic plot. * * @param xPix * @return */ - public Number getXVal(float xPix) { + protected Number screenToSeriesX(float xPix) { if (!plot.getBounds().xRegion.isDefined()) { return null; } @@ -438,6 +446,38 @@ public Number getXVal(float xPix) { .transform(xPix, plot.getBounds().getxRegion()); } + /** + * Converts a y pixel to a y value. + * This is a relatively slow operation and should not be used for operations that are a part of + * the main render loop of a dynamic plot. + * @param yPix + * @return + */ + protected Number screenToSeriesY(float yPix) { + if (!plot.getBounds().getyRegion().isDefined()) { + return null; + } + return new Region(gridRect.top, gridRect.bottom) + .transform(yPix, plot.getBounds().getyRegion(), true); + } + + protected PointF seriesToScreen(XYCoords xy) { + if(!plot.getBounds().isFullyDefined()) { + return null; + } + return plot.getBounds().transform(xy, gridRect, false, true); + } + + protected float seriesToScreenX(Number x) { + return (float) plot.getBounds().getxRegion(). + transform(x.doubleValue(), gridRect.left, gridRect.right, false); + } + + protected float seriesToScreenY(Number y) { + return (float) plot.getBounds().getyRegion(). + transform(y.doubleValue(), gridRect.left, gridRect.right, true); + } + @Override protected void doOnDraw(Canvas canvas, RectF widgetRect) throws PlotRenderException { @@ -687,6 +727,7 @@ protected void drawCursors(Canvas canvas) { boolean hasDomainCursor = false; // draw the domain cursor: if (domainCursorPaint != null + && domainCursorPosition != null && domainCursorPosition <= gridRect.right && domainCursorPosition >= gridRect.left) { hasDomainCursor = true; @@ -698,6 +739,7 @@ protected void drawCursors(Canvas canvas) { boolean hasRangeCursor = false; // draw the range cursor: if (rangeCursorPaint != null + && rangeCursorPosition != null && rangeCursorPosition >= gridRect.top && rangeCursorPosition <= gridRect.bottom) { hasRangeCursor = true; @@ -885,36 +927,53 @@ public void setRangeOriginLinePaint(Paint rangeOriginLinePaint) { this.rangeOriginLinePaint = rangeOriginLinePaint; } - public void setCursorPosition(float x, float y) { + /** + * Set domain and range cursor position using screen coordinates + * @param x + * @param y + */ + public void setCursorPosition(Float x, Float y) { setDomainCursorPosition(x); setRangeCursorPosition(y); } + /** + * Set domain and range cursor position using screen coordinates + * @param point + */ public void setCursorPosition(PointF point) { setCursorPosition(point.x, point.y); } - public float getDomainCursorPosition() { + public Float getDomainCursorPosition() { return domainCursorPosition; } public Number getDomainCursorVal() { - return getXVal(getDomainCursorPosition()); + return screenToSeriesX(getDomainCursorPosition()); } - public void setDomainCursorPosition(float domainCursorPosition) { + /** + * Set domain cursor position using screen coordinates + * @param domainCursorPosition + */ + public void setDomainCursorPosition(Float domainCursorPosition) { this.domainCursorPosition = domainCursorPosition; } - public float getRangeCursorPosition() { + public Float getRangeCursorPosition() { return rangeCursorPosition; } public Number getRangeCursorVal() { - return getYVal(getRangeCursorPosition()); + return screenToSeriesY(getRangeCursorPosition()); } - public void setRangeCursorPosition(float rangeCursorPosition) { + /** + * Set range cursor position using screen coordinates + * @param rangeCursorPosition + */ + public void setRangeCursorPosition(Float rangeCursorPosition) { this.rangeCursorPosition = rangeCursorPosition; } 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 d1230ea1..9f72cf18 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -362,20 +362,91 @@ public void setCursorPosition(float x, float y) { getGraph().setCursorPosition(x, y); } + /** + * Convert a screen xVal into a series xVal. + * @param xPix + * @return + * @deprecated Use {@link #screenToSeriesY(float)}. + */ + @Deprecated public Number getXVal(float xPix) { - return getGraph().getXVal(xPix); + return getGraph().screenToSeriesX(xPix); } + /** + * Convert a screen yVal into a series yVal. + * @param yPix + * @return + * @deprecated Use {@link #screenToSeriesY(float)}. + */ public Number getYVal(float yPix) { - return getGraph().getYVal(yPix); + return getGraph().screenToSeriesY(yPix); } + /** + * Convert the y coord of a PointF into a series yVal. + * @param point + * @return + * @deprecated Use {@link #screenToSeriesY(float)}. + */ + @Deprecated public Number getYVal(PointF point) { - return getGraph().getYVal(point); + return getGraph().screenToSeriesY(point); } + /** + * Convert the x coord of a PointF into a series xVal. + * @param point + * @return + * @deprecated Use {@link #screenToSeriesY(float)}. + */ + @Deprecated public Number getXVal(PointF point) { - return getGraph().getXVal(point); + return getGraph().screenToSeriesX(point); + } + + public Number screenToSeriesX(float x) { + return getGraph().screenToSeriesX(x); + } + + public Number screenToSeriesY(float y) { + return getGraph().screenToSeriesY(y); + } + + /** + * Convert a series xVal into a screen x coord + * @param x + * @return + */ + public float seriesToScreenX(Number x) { + return getGraph().seriesToScreenX(x); + } + + /** + * Convert a series yVal into a screen y coord + * @param y + * @return + */ + public float seriesToScreenY(Number y) { + return getGraph().seriesToScreenY(y); + } + + /** + * Convert a series xy value into a screen point. + * @param xy + * @return + */ + public PointF seriesToScreen(XYCoords xy) { + return getGraph().seriesToScreen(xy); + } + + /** + * Convert a screen point into a series xy value. + * @param point + * @return + */ + public XYCoords screentoSeries(PointF point) { + return getGraph().screenToSeries(point); } public void calculateMinMaxVals() { diff --git a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java index e787afaa..dba4ce43 100644 --- a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java @@ -27,6 +27,8 @@ import org.junit.*; import org.mockito.*; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.fail; import static org.mockito.Mockito.*; public class PieRendererTest extends AndroidplotTest { @@ -47,17 +49,18 @@ public class PieRendererTest extends AndroidplotTest { @Mock RenderStack renderStack; + Canvas canvas; + @Before public void setUp() throws Exception { pieChart = spy(new PieChart(getContext(), "My Pie")); + canvas = spy(new Canvas()); } @Test public void testDrawSegment_withoutTextPaintDoesntDrawLabel() throws Exception { PieRenderer pieRenderer = spy(new PieRenderer(pieChart)); Segment segment = spy(new Segment("My Segment", 100)); - Canvas canvas = new Canvas(); - SegmentFormatter formatterWithoutTextPaint = new SegmentFormatter(Color.GREEN); formatterWithoutTextPaint.setLabelPaint(null); @@ -96,7 +99,6 @@ public void testDrawSegment_withoutTextPaintDoesntDrawLabel() throws Exception { @Test public void testOnRender() throws Exception { Segment segment = spy(new Segment("My Segment", 100)); - Canvas canvas = new Canvas(); SegmentFormatter formatter = spy( new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN)); PieRenderer pr = formatter.getRendererInstance(pieChart); @@ -106,4 +108,88 @@ public void testOnRender() throws Exception { pieChart.addSegment(segment, formatter); renderer.onRender(canvas, plotArea, segment, formatter, renderStack); } + + @Test + public void testGetContainingSegment() throws Exception { + Segment segment1 = spy(new Segment("s1", 25)); + Segment segment2 = spy(new Segment("s2", 25)); + Segment segment3 = spy(new Segment("s3", 25)); + Segment segment4 = spy(new Segment("s4", 25)); + SegmentFormatter formatter = spy( + new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN)); + PieRenderer renderer = formatter.getRendererInstance(pieChart); + + pieChart.addSegment(segment1, formatter); + pieChart.addSegment(segment2, formatter); + pieChart.addSegment(segment3, formatter); + pieChart.addSegment(segment4, formatter); + + // southeast + assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 100))); + + // southwest + assertEquals(segment2, renderer.getContainingSegment(new PointF(0, 100))); + + // northwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0))); + + // northeast + assertEquals(segment4, renderer.getContainingSegment(new PointF(100, 0))); + + renderer.setStartDegs(90); + // southeast + assertEquals(segment2, renderer.getContainingSegment(new PointF(100, 100))); + + // southwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 100))); + + // northwest + assertEquals(segment4, renderer.getContainingSegment(new PointF(0, 0))); + + // northeast + assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0))); + } + + @Test + public void testDegsToScreenDegs() throws Exception { + assertEquals(0f, PieRenderer.degsToScreenDegs(0)); + assertEquals(359f, PieRenderer.degsToScreenDegs(1)); + assertEquals(271f, PieRenderer.degsToScreenDegs(89)); + assertEquals(270f, PieRenderer.degsToScreenDegs(90)); + assertEquals(269f, PieRenderer.degsToScreenDegs(91)); + assertEquals(181f, PieRenderer.degsToScreenDegs(179)); + assertEquals(180f, PieRenderer.degsToScreenDegs(180)); + assertEquals(179f, PieRenderer.degsToScreenDegs(181)); + assertEquals(91f, PieRenderer.degsToScreenDegs(269)); + assertEquals(90f, PieRenderer.degsToScreenDegs(270)); + assertEquals(89f, PieRenderer.degsToScreenDegs(271)); + assertEquals(1f, PieRenderer.degsToScreenDegs(359)); + assertEquals(0f, PieRenderer.degsToScreenDegs(360)); + + } + + @Test + public void testSetDonutSize() throws Exception { + + Segment segment1 = spy(new Segment("s1", 25)); + Segment segment2 = spy(new Segment("s2", 25)); + Segment segment3 = spy(new Segment("s3", 25)); + Segment segment4 = spy(new Segment("s4", 25)); + + SegmentFormatter formatter = spy( + new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN)); + PieRenderer renderer = formatter.getRendererInstance(pieChart); + + pieChart.addSegment(segment1, formatter); + pieChart.addSegment(segment2, formatter); + pieChart.addSegment(segment3, formatter); + pieChart.addSegment(segment4, formatter); + + renderer.setDonutSize(0.25f, PieRenderer.DonutMode.PERCENT); + + renderer.onRender(canvas, plotArea, segment1, formatter, renderStack); + + // TODO: verify radials are drown at the correct offsets from center + //verify(canvas).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), any(Paint.class)); + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java new file mode 100644 index 00000000..2bb317b5 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/BarRendererTest.java @@ -0,0 +1,290 @@ +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot.xy; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; + +import com.androidplot.test.AndroidplotTest; +import com.androidplot.ui.RenderStack; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; + +import java.util.List; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class BarRendererTest extends AndroidplotTest { + + XYPlot xyPlot; + + Canvas canvas; + + RectF plotArea = new RectF(0, 0, 100, 100); + + BarFormatter barFormatter; + + @Mock + RenderStack renderStack; + + @Captor + ArgumentCaptor barCaptor; + + @Captor + ArgumentCaptor rectCaptor; + + @Before + public void setUp() throws Exception { + canvas = spy(new Canvas()); + xyPlot = spy(new XYPlot(getContext(), "My Plot")); + barFormatter = spy(new BarFormatter(Color.RED, Color.RED)); + } + + @Test + public void testOnRender_handlesNullValues() throws Exception { + XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", null, 5, null); + XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", null, 5, null); + + BarRenderer renderer = setupRendererForTesting(s1, s2); + + xyPlot.setRangeBoundaries(0, 10, BoundaryMode.FIXED); + xyPlot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack); + + verify(renderer, times(6)) + .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture()); + } + + @Test + public void testOnRender_stacked() throws Exception { + XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 2, 5, 7); + XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 8, 5, 3); + + BarRenderer renderer = setupRendererForTesting(s1, s2); + + renderer.setBarOrientation(BarRenderer.BarOrientation.STACKED); + + xyPlot.setRangeBoundaries(0, 10, BoundaryMode.FIXED); + xyPlot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack); + + // make sure the expected number of bars draws were attempted: + verify(renderer, times(s1.size() + s2.size())). + drawBar(eq(canvas), any(BarRenderer.Bar.class), any(RectF.class)); + + // s1[0] + verifyBarHeight(80, 100, barFormatter, 1); + + // s2[0] + verifyBarHeight(0, 80, barFormatter, 1); + + // s1[1] + verifyBarHeight(50, 100, barFormatter, 1); + + // s2[1] + verifyBarHeight(0, 50, barFormatter, 1); + + // s1[2] + verifyBarHeight(30, 100, barFormatter, 1); + + // s2[2] + verifyBarHeight(0, 30, barFormatter, 1); + } + + @Test + public void testOnRender_sideBySide() throws Exception { + XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); + XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10); + + BarRenderer renderer = setupRendererForTesting(s1, s2); + renderer.setBarOrientation(BarRenderer.BarOrientation.SIDE_BY_SIDE); + + xyPlot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack); + + // make sure the expected number of bars draws were attempted: + verify(renderer, times(s1.size() + s2.size())). + drawBar(eq(canvas), any(BarRenderer.Bar.class), any(RectF.class)); + + // s1[0] has zero height so should not be drawn: + verifyBarHeight(100, 100, barFormatter, 0); + + // s1[1]: + verifyBarHeight(50, 100, barFormatter, 1); + + // s1[2] & s2[2]: + verifyBarHeight(0, 100, barFormatter, 2); + + // s2[0]: + verifyBarHeight(90, 100, barFormatter, 1); + + // s2[1]: + verifyBarHeight(25, 100, barFormatter, 1); + } + + @Test + public void testOnRender_overlaid() throws Exception { + XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); + XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10); + + BarRenderer renderer = setupRendererForTesting(s1, s2); + renderer.setBarOrientation(BarRenderer.BarOrientation.OVERLAID); + + xyPlot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack); + + // make sure the expected number of bars draws were attempted: + verify(renderer, times(s1.size() + s2.size())). + drawBar(eq(canvas), any(BarRenderer.Bar.class), any(RectF.class)); + + // s1[0] has zero height so should not be drawn: + verifyBarHeight(100, 100, barFormatter, 0); + + // s1[1]: + verifyBarHeight(50, 100, barFormatter, 1); + + // s1[2] & s2[2]: + verifyBarHeight(0, 100, barFormatter, 2); + + // s2[0]: + verifyBarHeight(90, 100, barFormatter, 1); + + // s2[1]: + verifyBarHeight(25, 100, barFormatter, 1); + } + + /** + * Verify that positive values are drawn in order of highest yVal, while negative values are + * drawn in order of lowest yVal. + * @throws Exception + */ + @Test + public void testOnRender_overlaid_drawsBarsInOrder() throws Exception { + XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", -1, -2, 1, 2); + XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", -2, -1, 2, 1); + + BarRenderer renderer = setupRendererForTesting(s1, s2); + renderer.setBarOrientation(BarRenderer.BarOrientation.OVERLAID); + + xyPlot.setUserRangeOrigin(0); + xyPlot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack); + + verify(renderer, times(8)) + .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture()); + + // list of all bars drawn, in the exact order they were drawn. + List bars = barCaptor.getAllValues(); + + assertEquals(s2.getY(0), bars.get(0).getY()); + assertEquals(s1.getY(0), bars.get(1).getY()); + + assertEquals(s1.getY(1), bars.get(2).getY()); + assertEquals(s2.getY(1), bars.get(3).getY()); + } + + @Test + public void testFixedBarWidth() throws Exception { + XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); + XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10); + + BarRenderer renderer = setupRendererForTesting(s1, s2); + final float barWidth = 10; + renderer.setBarGroupWidth(BarRenderer.BarGroupWidthMode.FIXED_WIDTH, barWidth); + + xyPlot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack); + + verify(renderer, times(6)) + .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture()); + + List barRects = rectCaptor.getAllValues(); + + for(RectF rect : barRects) { + assertEquals(barWidth, rect.width()); + } + } + + @Test + public void testFixedGapWidth() throws Exception { + XYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); + XYSeries s2 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s2", 1, 7.5, 10); + + BarRenderer renderer = setupRendererForTesting(s1, s2); + + final float gap = 5; + renderer.setBarGroupWidth(BarRenderer.BarGroupWidthMode.FIXED_GAP, gap); + + xyPlot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, s1, barFormatter, renderStack); + + verify(renderer, times(6)) + .drawBar(eq(canvas), barCaptor.capture(), rectCaptor.capture()); + + List barRects = rectCaptor.getAllValues(); + + // verify that the spacing between each bar group is exactly what was set: + assertEquals(gap, barRects.get(2).left - barRects.get(0).right); + assertEquals(gap, barRects.get(3).left - barRects.get(1).right); + + assertEquals(gap, barRects.get(4).left - barRects.get(2).right); + assertEquals(gap, barRects.get(5).left - barRects.get(3).right); + } + + private void verifyBarHeight(float top, float bottom, BarFormatter formatter, int times) { + final Paint borderPaint = formatter.getBorderPaint(); + verify(canvas, times(times)).drawRect( + anyFloat(), + eq(top), + anyFloat(), + eq(bottom), + eq(borderPaint)); + + final Paint fillPaint = formatter.getFillPaint(); + verify(canvas, times(times)).drawRect( + anyFloat(), + eq(top), + anyFloat(), + eq(bottom), + eq(fillPaint)); + } + + protected BarRenderer setupRendererForTesting(XYSeries... series) { + BarRenderer renderer = spy((BarRenderer)barFormatter.getRendererInstance(xyPlot)); + renderer.setBarOrientation(BarRenderer.BarOrientation.OVERLAID); + + for(XYSeries s : series) { + xyPlot.addSeries(s, barFormatter); + } + + doReturn(renderer.getClass()).when(barFormatter).getRendererClass(); + return renderer; + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/ScalingXYSeriesTest.java b/androidplot-core/src/test/java/com/androidplot/xy/ScalingXYSeriesTest.java new file mode 100644 index 00000000..90756428 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/ScalingXYSeriesTest.java @@ -0,0 +1,58 @@ +package com.androidplot.xy; + +import com.androidplot.test.AndroidplotTest; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +/** + * Tests {@link ScalingXYSeries}. + */ +public class ScalingXYSeriesTest extends AndroidplotTest { + + @Test + public void testScale_yOnly() { + SimpleXYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); + ScalingXYSeries scaled1 = new ScalingXYSeries(s1, 0.5, ScalingXYSeries.Mode.Y_ONLY); + + assertEquals(0d, scaled1.getX(0).doubleValue()); + assertEquals(0d, scaled1.getY(0).doubleValue()); + + assertEquals(1d, scaled1.getX(1).doubleValue()); + assertEquals(2.5d, scaled1.getY(1).doubleValue()); + + assertEquals(2d, scaled1.getX(2).doubleValue()); + assertEquals(5d, scaled1.getY(2).doubleValue()); + } + + @Test + public void testScale_xOnly() { + SimpleXYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); + ScalingXYSeries scaled1 = new ScalingXYSeries(s1, 0.5, ScalingXYSeries.Mode.X_ONLY); + + assertEquals(0d, scaled1.getX(0).doubleValue()); + assertEquals(0d, scaled1.getY(0).doubleValue()); + + assertEquals(0.5d, scaled1.getX(1).doubleValue()); + assertEquals(5d, scaled1.getY(1).doubleValue()); + + assertEquals(1d, scaled1.getX(2).doubleValue()); + assertEquals(10d, scaled1.getY(2).doubleValue()); + } + + @Test + public void testScale_xAndY() { + SimpleXYSeries s1 = new SimpleXYSeries(SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1", 0, 5, 10); + ScalingXYSeries scaled1 = new ScalingXYSeries(s1, 0.5, ScalingXYSeries.Mode.X_AND_Y); + + assertEquals(0d, scaled1.getX(0).doubleValue()); + assertEquals(0d, scaled1.getY(0).doubleValue()); + + assertEquals(0.5d, scaled1.getX(1).doubleValue()); + assertEquals(2.5d, scaled1.getY(1).doubleValue()); + + assertEquals(1d, scaled1.getX(2).doubleValue()); + assertEquals(5d, scaled1.getY(2).doubleValue()); + } +} 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 f0e0c76e..4b9f156a 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java @@ -25,8 +25,10 @@ import org.junit.*; import org.mockito.*; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyFloat; @@ -179,12 +181,14 @@ public void testDrawGrid_centeredOrigin() throws Exception { } @Test - public void testDrawCursors_ifCursorPaintIsSet() throws Exception { + public void testDrawCursors_ifCursorPaintAndPositionAreSet() throws Exception { final Paint domainCursorPaint = new Paint(); graphWidget.setDomainCursorPaint(domainCursorPaint); final Paint rangeCursorPaint = new Paint(); graphWidget.setRangeCursorPaint(rangeCursorPaint); + graphWidget.setRangeCursorPosition(0f); + graphWidget.setDomainCursorPosition(0f); graphWidget.drawCursors(canvas); // expect one line to be drawn for each cursor: @@ -193,7 +197,7 @@ public void testDrawCursors_ifCursorPaintIsSet() throws Exception { } @Test - public void testDrawCursors_ifCursorPaintIsNotSet() throws Exception { + public void testDrawCursors_ifCursorPaintAndPositionAreNotSet() throws Exception { graphWidget.setDomainCursorPaint(null); graphWidget.setRangeCursorPaint(null); @@ -205,6 +209,8 @@ public void testDrawCursors_ifCursorPaintIsNotSet() throws Exception { @Test public void testDrawCursorLabel() throws Exception { + graphWidget.setDomainCursorPosition(0f); + graphWidget.setRangeCursorPosition(0f); XYGraphWidget.CursorLabelFormatter clf = mock(XYGraphWidget.CursorLabelFormatter.class); when(clf.getTextPaint()).thenReturn(new Paint()); when(clf.getLabelText(any(Number.class), any(Number.class))).thenReturn("bla"); @@ -238,4 +244,74 @@ public void testSetLineLabelEdges_bitfield() throws Exception { assertFalse(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.BOTTOM)); assertFalse(graphWidget.isLineLabelEnabled(XYGraphWidget.Edge.LEFT)); } + + @Test + public void testScreenToSeries() throws Exception { + when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); + + XYCoords coords = graphWidget.screenToSeries(new PointF(0, 0)); + assertEquals(-100, coords.x.intValue()); + assertEquals(100, coords.y.intValue()); + + coords = graphWidget.screenToSeries(new PointF(100, 100)); + assertEquals(100, coords.x.intValue()); + assertEquals(-100, coords.y.intValue()); + + coords = graphWidget.screenToSeries(new PointF(50, 50)); + assertEquals(0, coords.x.intValue()); + assertEquals(0, coords.y.intValue()); + } + + @Test + public void testSeriesToScreen() throws Exception { + when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); + + PointF point = graphWidget.seriesToScreen(new XYCoords(-100, 100)); + assertEquals(0f, point.x); + assertEquals(0f, point.y); + + point = graphWidget.seriesToScreen(new XYCoords(100, -100)); + assertEquals(100f, point.x); + assertEquals(100f, point.y); + + point = graphWidget.seriesToScreen(new XYCoords(0, 0)); + assertEquals(50f, point.x); + assertEquals(50f, point.y); + } + + @Test + public void testScreenToSeriesX() throws Exception { + when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); + + assertEquals(-100, graphWidget.screenToSeriesX(new PointF(0, 0)).intValue()); + assertEquals(100, graphWidget.screenToSeriesX(new PointF(100, 100)).intValue()); + assertEquals(0, graphWidget.screenToSeriesX(new PointF(50, 50)).intValue()); + } + + @Test + public void testScreenToSeriesY() throws Exception { + when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); + + assertEquals(100, graphWidget.screenToSeriesY(new PointF(0, 0)).intValue()); + assertEquals(-100, graphWidget.screenToSeriesY(new PointF(100, 100)).intValue()); + assertEquals(0, graphWidget.screenToSeriesY(new PointF(50, 50)).intValue()); + } + + @Test + public void testSeriesToScreenX() throws Exception { + when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); + + assertEquals(0f, graphWidget.seriesToScreenX(-100)); + assertEquals(100f, graphWidget.seriesToScreenX(100)); + assertEquals(50f, graphWidget.seriesToScreenX(0)); + } + + @Test + public void testSeriesToScreenY() throws Exception { + when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); + + assertEquals(0f, graphWidget.seriesToScreenY(100)); + assertEquals(100f, graphWidget.seriesToScreenY(-100)); + assertEquals(50f, graphWidget.seriesToScreenY(0)); + } } 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 7e443b29..553314a4 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java @@ -302,7 +302,6 @@ public void testSetDomainRightMinMax() throws Exception { assertEquals(9, plot.getBounds().getMaxX().doubleValue(), 0); plot.getOuterLimits().setMaxX(10); - //plot.setDomainRightMax(10); plot.calculateMinMaxVals(); // same values. @@ -316,7 +315,6 @@ public void testSetDomainRightMinMax() throws Exception { assertEquals(0, plot.getBounds().getMinX().doubleValue(), 0); assertEquals(10, plot.getBounds().getMaxX().doubleValue(), 0); - //plot.setDomainRightMax(null); plot.getOuterLimits().setMaxX(null); plot.calculateMinMaxVals(); @@ -325,7 +323,6 @@ public void testSetDomainRightMinMax() throws Exception { assertEquals(11, plot.getBounds().getMaxX().doubleValue(), 0); // now the RightMin - //plot.setDomainRightMin(10); plot.getInnerLimits().setMaxX(10); plot.calculateMinMaxVals(); @@ -342,7 +339,6 @@ public void testSetDomainRightMinMax() throws Exception { assertEquals(10, plot.getBounds().getMaxX().doubleValue(), 0); // now off again - //plot.setDomainRightMin(null); plot.getInnerLimits().setMaxX(null); plot.calculateMinMaxVals(); @@ -362,8 +358,6 @@ public void testSetRangeTopBottomMinMax() throws Exception { plot.getOuterLimits().setMaxY(110); plot.getOuterLimits().setMinY(-50); - //plot.setRangeTopMax(110); - //plot.setRangeBottomMin(-50); plot.calculateMinMaxVals(); // same values. @@ -379,8 +373,6 @@ public void testSetRangeTopBottomMinMax() throws Exception { plot.getOuterLimits().setMaxY(null); plot.getOuterLimits().setMinY(null); - //plot.setRangeTopMax(null); - //plot.setRangeBottomMin(null); plot.calculateMinMaxVals(); // back to full @@ -390,8 +382,6 @@ public void testSetRangeTopBottomMinMax() throws Exception { // now the Min plot.getInnerLimits().setMaxY(150); plot.getInnerLimits().setMinY(-60); - //plot.setRangeTopMin(150); - //plot.setRangeBottomMax(-60); plot.calculateMinMaxVals(); // still to full @@ -409,8 +399,6 @@ public void testSetRangeTopBottomMinMax() throws Exception { // now off again plot.getInnerLimits().setMaxY(null); plot.getInnerLimits().setMinY(null); - //plot.setRangeTopMin(null); - //plot.setRangeBottomMax(null); plot.calculateMinMaxVals(); // small values. diff --git a/build.gradle b/build.gradle index 0cd58cd4..d8cd3ff4 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 24 theTargetSdkVersion = 24 theMinSdkVersion = 5 - theVersionName = '1.3.1' + theVersionName = '1.4.0' theVersionCode = 0 } diff --git a/demoapp/src/main/AndroidManifest.xml b/demoapp/src/main/AndroidManifest.xml index f7ac2f6f..89f1428f 100644 --- a/demoapp/src/main/AndroidManifest.xml +++ b/demoapp/src/main/AndroidManifest.xml @@ -39,6 +39,12 @@ + + + + + diff --git a/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java new file mode 100644 index 00000000..7c2cdaee --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/AnimatedXYPlotActivity.java @@ -0,0 +1,155 @@ +/* + * 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.demos; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.graphics.Color; +import android.os.Bundle; +import android.view.animation.AccelerateDecelerateInterpolator; + +import com.androidplot.xy.BoundaryMode; +import com.androidplot.xy.CatmullRomInterpolator; +import com.androidplot.xy.LineAndPointFormatter; +import com.androidplot.xy.ScalingXYSeries; +import com.androidplot.xy.SimpleXYSeries; +import com.androidplot.xy.XYGraphWidget; +import com.androidplot.xy.XYPlot; +import com.androidplot.xy.XYSeries; + +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.util.Arrays; + +/** + * Demonstrates animating XYSeries data from a zero value up/down to the actual values set. Once + * the animation completes, labels for each point are made visible. + * + * IMPORTANT: This example makes use of {@link ValueAnimator} which is only available in + * SDK level 11 and later.. + */ +public class AnimatedXYPlotActivity extends Activity { + + private XYPlot plot; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.simple_xy_plot_example); + + // initialize our XYPlot reference: + plot = (XYPlot) findViewById(R.id.plot); + + // create a couple arrays of y-values to plot: + final Number[] domainLabels = {1, 2, 3, 6, 7, 8, 9, 10, 13, 14}; + Number[] series1Numbers = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64}; + Number[] series2Numbers = {5, 2, 10, 5, 20, 10, 40, 20, 80, 40}; + + // turn the above arrays into XYSeries': + // (Y_VALS_ONLY means use the element index as the x value) + XYSeries series1 = new SimpleXYSeries( + Arrays.asList(series1Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Series1"); + XYSeries series2 = new SimpleXYSeries( + Arrays.asList(series2Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Series2"); + + // create formatters to use for drawing a series using LineAndPointRenderer + // and configure them from xml: + final LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter); + + final LineAndPointFormatter series2Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter); + series2Format.getLinePaint().setColor(Color.RED); + + // just for fun, add some smoothing to the lines: + // see: http://androidplot.com/smooth-curves-and-androidplot/ + series1Format.setInterpolationParams( + new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal)); + + series2Format.setInterpolationParams( + new CatmullRomInterpolator.Params(10, CatmullRomInterpolator.Type.Centripetal)); + + // wrap each series in instances of ScalingXYSeries before adding to the plot + // so that we can animate the series values below: + final ScalingXYSeries scalingSeries1 = new ScalingXYSeries(series1, 0, ScalingXYSeries.Mode.Y_ONLY); + plot.addSeries(scalingSeries1, series1Format); + + final ScalingXYSeries scalingSeries2 = new ScalingXYSeries(series2, 0, ScalingXYSeries.Mode.Y_ONLY); + plot.addSeries(scalingSeries2, series2Format); + + plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setFormat(new Format() { + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + int i = Math.round(((Number) obj).floatValue()); + return toAppendTo.append(domainLabels[i]); + } + @Override + public Object parseObject(String source, ParsePosition pos) { + return null; + } + }); + + plot.setRangeBoundaries(0, 100, BoundaryMode.FIXED); + + // animate a scale value from a starting val of 0 to a final value of 1: + ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + + // use an animation pattern that begins and ends slowly: + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + float scale = valueAnimator.getAnimatedFraction(); + scalingSeries1.setScale(scale); + scalingSeries2.setScale(scale); + plot.redraw(); + } + }); + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + + } + + @Override + public void onAnimationEnd(Animator animator) { + // the animation is over, so show point labels: + series1Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); + series2Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); + plot.redraw(); + } + + @Override + public void onAnimationCancel(Animator animator) { + + } + + @Override + public void onAnimationRepeat(Animator animator) { + + } + }); + + // the animation will run for 1.5 seconds: + animator.setDuration(1500); + animator.start(); + } +} diff --git a/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java index e3400ec5..8d8720d1 100644 --- a/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/BarPlotExampleActivity.java @@ -96,7 +96,11 @@ public void onCreate(Bundle savedInstanceState) { plot = (XYPlot) findViewById(R.id.plot); formatter1 = new MyBarFormatter(Color.rgb(100, 150, 100), Color.LTGRAY); + formatter1.setMarginLeft(PixelUtils.dpToPix(1)); + formatter1.setMarginRight(PixelUtils.dpToPix(1)); formatter2 = new MyBarFormatter(Color.rgb(100, 100, 150), Color.LTGRAY); + formatter2.setMarginLeft(PixelUtils.dpToPix(1)); + formatter2.setMarginRight(PixelUtils.dpToPix(1)); selectionFormatter = new MyBarFormatter(Color.YELLOW, Color.WHITE); selectionWidget = new TextLabelWidget(plot.getLayoutManager(), NO_SELECTION_TXT, @@ -152,12 +156,12 @@ public boolean onTouch(View view, MotionEvent motionEvent) { }); spRenderStyle = (Spinner) findViewById(R.id.spRenderStyle); - ArrayAdapter adapter = new ArrayAdapter(this, - android.R.layout.simple_spinner_item, BarRenderer.Style + ArrayAdapter adapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, BarRenderer.BarOrientation .values()); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spRenderStyle.setAdapter(adapter); - spRenderStyle.setSelection(BarRenderer.Style.OVERLAID.ordinal()); + spRenderStyle.setSelection(BarRenderer.BarOrientation.OVERLAID.ordinal()); spRenderStyle.setOnItemSelectedListener(new OnItemSelectedListener() { public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { updatePlot(); @@ -169,15 +173,15 @@ public void onNothingSelected(AdapterView arg0) { }); spWidthStyle = (Spinner) findViewById(R.id.spWidthStyle); - ArrayAdapter adapter1 = new ArrayAdapter( - this, android.R.layout.simple_spinner_item, BarRenderer.BarWidthMode + ArrayAdapter adapter1 = new ArrayAdapter( + this, android.R.layout.simple_spinner_item, BarRenderer.BarGroupWidthMode .values()); adapter1.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spWidthStyle.setAdapter(adapter1); - spWidthStyle.setSelection(BarRenderer.BarWidthMode.FIXED_WIDTH.ordinal()); + spWidthStyle.setSelection(BarRenderer.BarGroupWidthMode.FIXED_WIDTH.ordinal()); spWidthStyle.setOnItemSelectedListener(new OnItemSelectedListener() { public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { - if (BarRenderer.BarWidthMode.FIXED_WIDTH.equals(spWidthStyle.getSelectedItem())) { + if (BarRenderer.BarGroupWidthMode.FIXED_WIDTH.equals(spWidthStyle.getSelectedItem())) { sbFixedWidth.setVisibility(View.VISIBLE); sbVariableWidth.setVisibility(View.INVISIBLE); } else { @@ -282,9 +286,7 @@ public Number parse(String string, ParsePosition position) { throw new UnsupportedOperationException("Not yet implemented."); } }); - updatePlot(); - } private void updatePlot() { @@ -326,13 +328,15 @@ private void updatePlot(SeriesSize seriesSize) { if (series2CheckBox.isChecked()) plot.addSeries(series2, formatter2); // Setup the BarRenderer with our selected options - MyBarRenderer renderer = ((MyBarRenderer) plot.getRenderer(MyBarRenderer.class)); - renderer.setStyle((BarRenderer.Style) spRenderStyle.getSelectedItem()); - renderer.setBarWidthMode((BarRenderer.BarWidthMode) spWidthStyle.getSelectedItem()); - renderer.setBarWidth(sbFixedWidth.getProgress()); - renderer.setBarGap(sbVariableWidth.getProgress()); - - if (BarRenderer.Style.STACKED.equals(spRenderStyle.getSelectedItem())) { + MyBarRenderer renderer = plot.getRenderer(MyBarRenderer.class); + renderer.setBarOrientation((BarRenderer.BarOrientation) spRenderStyle.getSelectedItem()); + final BarRenderer.BarGroupWidthMode barGroupWidthMode + = (BarRenderer.BarGroupWidthMode) spWidthStyle.getSelectedItem(); + renderer.setBarGroupWidth(barGroupWidthMode, + barGroupWidthMode == BarRenderer.BarGroupWidthMode.FIXED_WIDTH + ? sbFixedWidth.getProgress() : sbVariableWidth.getProgress()); + + if (BarRenderer.BarOrientation.STACKED.equals(spRenderStyle.getSelectedItem())) { plot.getInnerLimits().setMaxY(15); } else { plot.getInnerLimits().setMaxY(0); diff --git a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java b/demoapp/src/main/java/com/androidplot/demos/MainActivity.java index d091c0fb..86dadb68 100644 --- a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/MainActivity.java @@ -42,6 +42,14 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.main); + Button startAnimatedXYPlotExButton = (Button) findViewById(R.id.animatedXYPlotExButton); + startAnimatedXYPlotExButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(new Intent(MainActivity.this, AnimatedXYPlotActivity.class)); + } + }); + Button startScatterPlotExButton = (Button) findViewById(R.id.startScatterExButton); startScatterPlotExButton.setOnClickListener(new View.OnClickListener() { @Override diff --git a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java index 3ef6d04f..71e721d8 100644 --- a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java @@ -28,6 +28,7 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import com.androidplot.Plot; +import com.androidplot.util.PixelUtils; import com.androidplot.util.PlotStatistics; import com.androidplot.util.Redrawer; import com.androidplot.xy.*; @@ -163,7 +164,8 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { BarRenderer barRenderer = aprLevelsPlot.getRenderer(BarRenderer.class); if(barRenderer != null) { // make our bars a little thicker than the default so they can be seen better: - barRenderer.setBarWidth(25); + barRenderer.setBarGroupWidth( + BarRenderer.BarGroupWidthMode.FIXED_WIDTH, PixelUtils.dpToPix(18)); } // register for orientation sensor events: diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java index 216af0ea..850bc4d8 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java @@ -16,11 +16,14 @@ package com.androidplot.demos; +import android.animation.Animator; +import android.animation.ValueAnimator; import android.app.Activity; import android.graphics.*; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.SeekBar; import android.widget.TextView; import com.androidplot.pie.PieChart; @@ -69,10 +72,13 @@ public boolean onTouch(View view, MotionEvent motionEvent) { PointF click = new PointF(motionEvent.getX(), motionEvent.getY()); if(pie.getPie().containsPoint(click)) { Segment segment = pie.getRenderer(PieRenderer.class).getContainingSegment(click); - final boolean isSelected = getFormatter(segment).getOffset() != 0; + deselectAll(); - setSelected(segment, !isSelected); - pie.redraw(); + if(segment != null) { + final boolean isSelected = getFormatter(segment).getOffset() != 0; + setSelected(segment, !isSelected); + pie.redraw(); + } } return false; } @@ -151,7 +157,65 @@ public void onStopTrackingTouch(SeekBar seekBar) { pie.getBackgroundPaint().setColor(Color.TRANSPARENT); } + @Override + public void onStart() { + super.onStart(); + setupIntroAnimation(); + } + protected void updateDonutText() { donutSizeTextView.setText(donutSizeSeekBar.getProgress() + "%"); } + + protected void setupIntroAnimation() { + + final PieRenderer renderer = pie.getRenderer(PieRenderer.class); + // start with a zero degrees pie: + + renderer.setExtentDegs(0); + // animate a scale value from a starting val of 0 to a final value of 1: + ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + + // use an animation pattern that begins and ends slowly: + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + float scale = valueAnimator.getAnimatedFraction(); +// scalingSeries1.setScale(scale); +// scalingSeries2.setScale(scale); + renderer.setExtentDegs(360 * scale); + pie.redraw(); + } + }); +// animator.addListener(new Animator.AnimatorListener() { +// @Override +// public void onAnimationStart(Animator animator) { +// +// } +// +// @Override +// public void onAnimationEnd(Animator animator) { +// // the animation is over, so show point labels: +// series1Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); +// series2Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); +// plot.redraw(); +// } +// +// @Override +// public void onAnimationCancel(Animator animator) { +// +// } +// +// @Override +// public void onAnimationRepeat(Animator animator) { +// +// } +// }); + + // the animation will run for 1.5 seconds: + animator.setDuration(1500); + animator.start(); + } } diff --git a/demoapp/src/main/res/layout/main.xml b/demoapp/src/main/res/layout/main.xml index 82ca3831..65a43ef9 100644 --- a/demoapp/src/main/res/layout/main.xml +++ b/demoapp/src/main/res/layout/main.xml @@ -40,6 +40,11 @@ style="@style/toc_button" android:text="A Simple XY Plot"/> +