From eec774c7def73b16c26d97c2f0a256d918205b6d Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Mon, 12 Sep 2016 09:55:48 -0500 Subject: [PATCH 001/104] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4e6e90f1..910121fe 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ compatible with all versions of Android from 1.6 onward and is **used by over # Usage +NOTE: When viewing this page on Github you may experience issues with some of the links below, due to a temporary issue on Github's side. For now, [view this page on Bitbucket](https://bitbucket.org/androidplot/androidplot) instead. + * :star: **[Quickstart](docs/quickstart.md)** * [Full Documentation](docs/index.md) From 66e70addd1d303eed0286bd43c1d94ef4348800a Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Mon, 12 Sep 2016 09:57:58 -0500 Subject: [PATCH 002/104] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 910121fe..58bee1a9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ compatible with all versions of Android from 1.6 onward and is **used by over # Usage -NOTE: When viewing this page on Github you may experience issues with some of the links below, due to a temporary issue on Github's side. For now, [view this page on Bitbucket](https://bitbucket.org/androidplot/androidplot) instead. +**NOTE: When viewing this page on Github you may experience issues with some of the links below, due to a temporary issue on Github's side. For now, [view this page on Bitbucket](https://bitbucket.org/androidplot/androidplot) instead.** * :star: **[Quickstart](docs/quickstart.md)** * [Full Documentation](docs/index.md) From 36933d2639b5f9ff49f4b7aaca8a9a0d4f87fd40 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Tue, 13 Sep 2016 08:55:15 -0500 Subject: [PATCH 003/104] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 58bee1a9..4e6e90f1 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ compatible with all versions of Android from 1.6 onward and is **used by over # Usage -**NOTE: When viewing this page on Github you may experience issues with some of the links below, due to a temporary issue on Github's side. For now, [view this page on Bitbucket](https://bitbucket.org/androidplot/androidplot) instead.** - * :star: **[Quickstart](docs/quickstart.md)** * [Full Documentation](docs/index.md) From 898c3345b14ac58d902bde3596490c46090bbcdb Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Thu, 15 Sep 2016 10:21:31 -0500 Subject: [PATCH 004/104] 1.2.1 changes: (#7) * Lots of new documentation * New functionality for pie charts * Added FastLineAndPointRenderer * Project update to SDK 24 * Removed sdkmanager * Replaced jmockit with mockito * Added circleci config --- androidplot-core/build.gradle | 11 +- .../src/main/java/com/androidplot/Plot.java | 4 + .../java/com/androidplot/pie/PieChart.java | 8 +- .../java/com/androidplot/pie/PieRenderer.java | 65 ++++--- .../com/androidplot/pie/SegmentFormatter.java | 48 ++++- .../xy/FastLineAndPointRenderer.java | 168 ++++++++++++++++++ .../com/androidplot/xy/SimpleXYSeries.java | 21 +++ .../main/java/com/androidplot/xy/XYPlot.java | 19 -- .../test/java/com/androidplot/PlotTest.java | 5 +- .../com/androidplot/test/AndroidplotTest.java | 2 +- .../com/androidplot/test/MyTestRunner.java | 17 ++ .../xy/FastLineAndPointRendererTest.java | 106 +++++++++++ .../androidplot/xy/XYLegendWidgetTest.java | 26 ++- build.gradle | 19 +- circle.yml | 44 +++++ demoapp/build.gradle | 3 +- demoapp/src/main/AndroidManifest.xml | 4 +- .../com/androidplot/demos/ECGExample.java | 3 - .../OrientationSensorExampleActivity.java | 8 +- .../demos/SimplePieChartActivity.java | 63 +++++-- demoapp/src/main/res/values/attrs.xml | 22 --- docs/dynamicdata.md | 84 +++++++++ docs/index.md | 2 +- docs/piechart.md | 50 +++++- docs/plot_composition.md | 5 +- docs/quickstart.md | 17 +- docs/release_notes.md | 20 +++ docs/xyplot.md | 78 ++++++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 29 files changed, 782 insertions(+), 142 deletions(-) create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java create mode 100644 androidplot-core/src/test/java/com/androidplot/test/MyTestRunner.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java create mode 100644 circle.yml delete mode 100644 demoapp/src/main/res/values/attrs.xml create mode 100644 docs/dynamicdata.md diff --git a/androidplot-core/build.gradle b/androidplot-core/build.gradle index 1d02906f..b365641f 100644 --- a/androidplot-core/build.gradle +++ b/androidplot-core/build.gradle @@ -14,7 +14,6 @@ * limitations under the License. */ -apply plugin: 'android-sdk-manager' apply plugin: 'com.android.library' apply plugin: 'com.github.dcendents.android-maven' apply plugin: 'com.jfrog.bintray' @@ -46,9 +45,13 @@ def siteUrl = 'http://androidplot.com' def gitUrl = 'https://bitbucket.org/androidplot/androidplot.git' dependencies { - testCompile group: 'org.jmockit', name: 'jmockit', version: '1.20' - testCompile group: 'junit', name: 'junit', version: '4.8.1' - testCompile "org.robolectric:robolectric:3.0" + testCompile "org.mockito:mockito-core:1.10.19" + testCompile group: 'junit', name: 'junit', version: '4.9' + testCompile "org.robolectric:robolectric:3.1" + + // temp fix for: + // https://github.com/robolectric/robolectric/issues/1932 + testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1' } task javadoc(type: Javadoc) { diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index a5f4ff86..fd518be3 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -548,6 +548,10 @@ public synchronized boolean removeListener(PlotListener listener) { return listeners.remove(listener); } + protected ArrayList getListeners() { + return listeners; + } + protected void notifyListenersBeforeDraw(Canvas canvas) { for (PlotListener listener : listeners) { listener.onBeforeDraw(this, canvas); diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java index 7ec48a31..3469bf6d 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java @@ -29,6 +29,9 @@ import com.androidplot.ui.HorizontalPositioning; import com.androidplot.ui.VerticalPositioning; +/** + * Basic representation of a Pie Chart that displays a title and pie widget. + */ public class PieChart extends Plot { private static final int DEFAULT_PIE_WIDGET_H_DP = 18; @@ -37,6 +40,8 @@ public class PieChart extends Plot { private static final int DEFAULT_PIE_WIDGET_Y_OFFSET_DP = 0; private static final int DEFAULT_PIE_WIDGET_X_OFFSET_DP = 0; + private static final int DEFAULT_PADDING_DP = 5; + public void setPie(PieWidget pie) { this.pie = pie; } @@ -74,7 +79,8 @@ protected void onPreInit() { VerticalPositioning.ABSOLUTE_FROM_CENTER, Anchor.CENTER); - pie.setPadding(10, 10, 10, 10); + final float padding = PixelUtils.dpToPix(DEFAULT_PADDING_DP); + pie.setPadding(padding, padding, padding, padding); } @Override 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 9da04fb5..6412628a 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java @@ -25,6 +25,9 @@ import java.util.List; +/** + * Basic renderer for drawing pie charts. + */ public class PieRenderer extends SeriesRenderer { // starting angle to use when drawing the first radial line of the first segment. @@ -79,9 +82,17 @@ public void onRender(Canvas canvas, RectF plotArea, Segment series, SegmentForma protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f, 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); + + PointF translated = calculateLineEnd( + bounds.centerX(), bounds.centerY(), f.getOffset(), halfSweepEndAngle); - float cx = bounds.centerX(); - float cy = bounds.centerY(); + final float cx = translated.x; + final float cy = translated.y; float donutSizePx; switch(donutMode) { @@ -95,41 +106,55 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm throw new UnsupportedOperationException("Not yet implemented."); } + final float outerRad = rad - f.getOuterInset(); + final float innerRad = donutSizePx == 0 ? 0 : donutSizePx + f.getInnerInset(); + //final float outerRad = rad; + //final float innerRad = donutSizePx; + // do we have a pie chart of less than 100% if(Math.abs(sweep - 360f) > Float.MIN_VALUE) { // vertices of the first radial: - PointF r1Outer = calculateLineEnd(cx, cy, rad, startAngle); - PointF r1Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle); + PointF r1Outer = calculateLineEnd(cx, cy, outerRad, startAngle); + PointF r1Inner = calculateLineEnd(cx, cy, innerRad, startAngle); // vertices of the second radial: - PointF r2Outer = calculateLineEnd(cx, cy, rad, startAngle + sweep); - PointF r2Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle + sweep); + PointF r2Outer = calculateLineEnd(cx, cy, outerRad, startAngle + sweep); + PointF r2Inner = calculateLineEnd(cx, cy, innerRad, startAngle + sweep); Path clip = new Path(); + // outer arc: // leave plenty of room on the outside for stroked borders; // necessary because the clipping border is ugly // and cannot be easily anti aliased. Really we only care about masking off the // radial edges. - clip.arcTo(new RectF(bounds.left - rad, - bounds.top - rad, - bounds.right + rad, - bounds.bottom + rad), + clip.arcTo(new RectF(bounds.left - outerRad, + bounds.top - outerRad, + bounds.right + outerRad, + bounds.bottom + outerRad), startAngle, sweep); clip.lineTo(cx, cy); clip.close(); canvas.clipPath(clip); Path p = new Path(); - p.arcTo(bounds, startAngle, sweep); + + // outer arc: + p.arcTo(new RectF( + 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 - donutSizePx, - cy - donutSizePx, - cx + donutSizePx, - cy + donutSizePx), + cx - innerRad, + cy - innerRad, + cx + innerRad, + cy + innerRad), startAngle + sweep, -sweep); p.close(); @@ -144,23 +169,23 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm else { canvas.save(Canvas.CLIP_SAVE_FLAG); Path chart = new Path(); - chart.addCircle(cx, cy, rad, Path.Direction.CW); + chart.addCircle(cx, cy, outerRad, Path.Direction.CW); Path inside = new Path(); - inside.addCircle(cx, cy, donutSizePx, Path.Direction.CW); + inside.addCircle(cx, cy, innerRad, Path.Direction.CW); canvas.clipPath(inside, Region.Op.DIFFERENCE); canvas.drawPath(chart, f.getFillPaint()); canvas.restore(); } // draw inner line: - canvas.drawCircle(cx, cy, donutSizePx, f.getInnerEdgePaint()); + canvas.drawCircle(cx, cy, innerRad, f.getInnerEdgePaint()); // draw outer line: - canvas.drawCircle(cx, cy, rad, f.getOuterEdgePaint()); + canvas.drawCircle(cx, cy, outerRad, f.getOuterEdgePaint()); canvas.restore(); PointF labelOrigin = calculateLineEnd(cx, cy, - (rad-((rad- donutSizePx)/2)), startAngle + (sweep/2)); + (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 diff --git a/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java b/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java index 390de996..be81b9fb 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java @@ -38,6 +38,11 @@ public class SegmentFormatter extends Formatter { private Paint labelPaint; private Paint labelMarkerPaint; + private float offset; + private float radialInset; + private float innerInset; + private float outerInset; + { setFillPaint(new Paint()); // outer edge: @@ -64,7 +69,6 @@ public class SegmentFormatter extends Formatter { getLabelPaint().setTextSize(DEFAULT_LABEL_FONT_SIZE); getLabelPaint().setAntiAlias(true); getLabelPaint().setTextAlign(Paint.Align.CENTER); - //getLabelPaint().setShadowLayer(5, 4, 4, Color.BLACK); // label marker paint: setLabelMarkerPaint(new Paint()); @@ -176,4 +180,46 @@ public Paint getLabelMarkerPaint() { public void setLabelMarkerPaint(Paint labelMarkerPaint) { this.labelMarkerPaint = labelMarkerPaint; } + + public float getOffset() { + return offset; + } + + /** + * Set an offset relative to the center of the pie chart at which this segment should be drawn; + * generally used to highlight specific segments. + * @param offset + */ + public void setOffset(float offset) { + this.offset = offset; + } + + public float getRadialInset() { + return radialInset; + } + + /** + * Set an inset in degrees for the radial edges of this segment. + * generally used to highlight specific segments. + * @param radialInset + */ + public void setRadialInset(float radialInset) { + this.radialInset = radialInset; + } + + public float getInnerInset() { + return innerInset; + } + + public void setInnerInset(float innerInset) { + this.innerInset = innerInset; + } + + public float getOuterInset() { + return outerInset; + } + + public void setOuterInset(float outerInset) { + this.outerInset = outerInset; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java new file mode 100644 index 00000000..29dcd897 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java @@ -0,0 +1,168 @@ +/* + * Copyright 2016 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot.xy; + +import android.graphics.*; +import com.androidplot.exception.PlotRenderException; +import com.androidplot.ui.RenderStack; +import com.androidplot.ui.SeriesRenderer; +import com.androidplot.util.ValPixConverter; + +import java.util.*; + +/** + * A faster implementation of of {@link LineAndPointRenderer}. For performance reasons, has these constraints: + * - Interpolation is not supported + * - Does not draw fill + * @since 1.2.0 + */ +public class FastLineAndPointRenderer extends XYSeriesRenderer { + + private float[] points; + List segmentOffsets = new ArrayList<>(); + List segmentLengths = new ArrayList<>(); + public FastLineAndPointRenderer(XYPlot plot) { + super(plot); + } + + @Override + protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) throws PlotRenderException { + + segmentOffsets.clear(); + segmentLengths.clear(); + + final int numPoints = series.size() * 2; + if(points == null || points.length != numPoints) { + // only allocate when necessary: + 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) { + Number y = series.getY(i); + Number x = series.getX(i); + + PointF thisPoint; + if (y != null && x != null) { + if(isLastPointNull) { + segmentOffsets.add(j); + segmentLen = 0; + isLastPointNull = false; + } + + thisPoint = ValPixConverter.valToPix( + x, y, + plotArea, + minX, maxX, + minY, maxY); + points[j] = thisPoint.x; + points[j+1] = thisPoint.y; + segmentLen+=2; + + // if this is the last point, account for it in segment lengths: + if(i == series.size()-1) { + segmentLengths.add(segmentLen); + } + } else if(!isLastPointNull) { + segmentLengths.add(segmentLen); + isLastPointNull = true; + } + } + + // draw segments + if(formatter.linePaint != null || formatter.vertexPaint != null) { + for (int i = 0; i < segmentOffsets.size(); i++) { + final int len = segmentLengths.get(i); + final int offset = segmentOffsets.get(i); + drawSegment(canvas, points, offset, len, formatter); + +// if(formatter.vertexPaint != null) { +// // draw vertices: +// canvas.drawPoints(points, offset, len, formatter.vertexPaint); +// } + } + } + } + + protected void drawSegment(Canvas canvas, float[] points, int offset, int len, Formatter formatter) { + if(formatter.linePaint != null) { + // draw lines: + if (len >= 4) { + // optimization to avoid using 2x storage space to represent the full path: + if ((len & 2) != 0) { + canvas.drawLines(points, offset, len - 2, formatter.linePaint); + canvas.drawLines(points, offset + 2, len - 2, formatter.linePaint); + } else { + canvas.drawLines(points, offset, len, formatter.linePaint); + canvas.drawLines(points, offset + 2, len - 4, formatter.linePaint); + } + } + } + + if(formatter.vertexPaint != null) { + // draw vertices: + canvas.drawPoints(points, offset, len, formatter.vertexPaint); + } + } + + @Override + protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) { + if(formatter.getLinePaint() != null) { + canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint()); + } + } + + /** + * Formatter designed to work in tandem with {@link AdvancedLineAndPointRenderer}. + * @since 0.9.9 + */ + public static class Formatter extends LineAndPointFormatter { + + public Formatter(Integer lineColor, Integer vertexColor, Integer fillColor, PointLabelFormatter plf) { + super(lineColor, vertexColor, fillColor, plf); + } + + public Formatter(Integer lineColor, Integer vertexColor, + Integer fillColor, PointLabelFormatter plf, FillDirection fillDir) { + super(lineColor, vertexColor, fillColor, plf, fillDir); + } + + @Override + protected void initLinePaint(Integer lineColor) { + super.initLinePaint(lineColor); + + // disable anti-aliasing by default: + getLinePaint().setAntiAlias(false); + } + + @Override + public Class getRendererClass() { + return FastLineAndPointRenderer.class; + } + + @Override + public SeriesRenderer getRendererInstance(XYPlot plot) { + return new FastLineAndPointRenderer(plot); + } + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java index 0dc9c88e..1763bc20 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/SimpleXYSeries.java @@ -284,4 +284,25 @@ public Number getX(int index) { public Number getY(int index) { return yVals.get(index); } + + public LinkedList getxVals() { + return xVals; + } + + public LinkedList getyVals() { + return yVals; + } + + /** + * Remove all values from the series + */ + public void clear() { + lock.writeLock().lock(); + try { + xVals.clear(); + yVals.clear(); + } finally { + lock.writeLock().unlock(); + } + } } 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 4ca4467d..a9b24f36 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -64,9 +64,6 @@ public class XYPlot extends Plot private static final int DEFAULT_RANGE_LABEL_WIDGET_Y_OFFSET_DP = 0; private static final int DEFAULT_RANGE_LABEL_WIDGET_X_OFFSET_DP = 0; - private static final int DEFAULT_DOMAIN_LINE_EXTENSION_DP = 2; - private static final int DEFAULT_RANGE_LINE_EXTENSION_DP = 2; - private static final int DEFAULT_PLOT_LEFT_MARGIN_DP = 1; private static final int DEFAULT_PLOT_RIGHT_MARGIN_DP = 1; private static final int DEFAULT_PLOT_TOP_MARGIN_DP = 1; @@ -999,22 +996,6 @@ public Number getCalculatedMaxY() { return calculatedMaxY != null ? calculatedMaxY : getDefaultBounds().getMaxY(); } - public boolean isDrawDomainOriginEnabled() { - return drawDomainOriginEnabled; - } - - public void setDrawDomainOriginEnabled(boolean drawDomainOriginEnabled) { - this.drawDomainOriginEnabled = drawDomainOriginEnabled; - } - - public boolean isDrawRangeOriginEnabled() { - return drawRangeOriginEnabled; - } - - public void setDrawRangeOriginEnabled(boolean drawRangeOriginEnabled) { - this.drawRangeOriginEnabled = drawRangeOriginEnabled; - } - /** * Appends the specified marker to the end of plot's yValueMarkers list. * diff --git a/androidplot-core/src/test/java/com/androidplot/PlotTest.java b/androidplot-core/src/test/java/com/androidplot/PlotTest.java index 28cfac45..bf492164 100644 --- a/androidplot-core/src/test/java/com/androidplot/PlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/PlotTest.java @@ -24,7 +24,6 @@ import com.androidplot.ui.SeriesRenderer; import com.androidplot.ui.Formatter; import com.androidplot.util.Configurator; -import mockit.*; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -312,7 +311,7 @@ public void testGetRendererList() throws Exception { @Test public void testAddListener() throws Exception { Plot plot = new MockPlot("MockPlot"); - ArrayList listeners = Deencapsulation.getField(plot, "listeners"); + ArrayList listeners = plot.getListeners(); assertEquals(0, listeners.size()); @@ -338,7 +337,7 @@ public void testAddListener() throws Exception { @Test public void testRemoveListener() throws Exception { Plot plot = new MockPlot("MockPlot"); - ArrayList listeners = Deencapsulation.getField(plot, "listeners"); + ArrayList listeners = plot.getListeners(); assertEquals(0, listeners.size()); diff --git a/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java b/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java index 16a356d8..e2eb9ac6 100644 --- a/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java @@ -25,7 +25,7 @@ /** * Base class for all Androidplot tests that make use of Android platform classes. */ -@RunWith(RobolectricTestRunner.class) +@RunWith(MyTestRunner.class) public abstract class AndroidplotTest { /** diff --git a/androidplot-core/src/test/java/com/androidplot/test/MyTestRunner.java b/androidplot-core/src/test/java/com/androidplot/test/MyTestRunner.java new file mode 100644 index 00000000..f268087a --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/test/MyTestRunner.java @@ -0,0 +1,17 @@ +package com.androidplot.test; + +import org.junit.runners.model.*; +import org.robolectric.*; + +public class MyTestRunner extends RobolectricTestRunner { + + /** + * Constructs a new instance of the test runner. + * + * @throws InitializationError if the test class is malformed + */ + public MyTestRunner(Class testClass) throws InitializationError + { + super(testClass); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java new file mode 100644 index 00000000..f510171c --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/FastLineAndPointRendererTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot.xy; + +import android.graphics.*; + +import com.androidplot.test.*; + +import org.junit.*; +import org.junit.runner.*; +import org.mockito.*; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@RunWith(MyTestRunner.class) +public class FastLineAndPointRendererTest extends AndroidplotTest { + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testOnRender() throws Exception { + RectF gridRect = new RectF(5, 5, 105, 105); + XYPlot plot = new XYPlot(getContext(), "Test"); + + FastLineAndPointRenderer.Formatter formatter = + new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null, null); + + + // create a series composed of 3 "segments"; series portions separated by null values: + XYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, null, 3, 4, 5, 6, 7, null, null, 8); + + FastLineAndPointRenderer renderer = Mockito.spy(new FastLineAndPointRenderer(plot)); + Canvas canvas = mock(Canvas.class); + + renderer.onRender(canvas, gridRect, series, formatter, null); + + // first segment + verify(renderer, times(1)).drawSegment( + eq(canvas), + any(float[].class), + eq(0), + eq(4), + eq(formatter)); + + verify(canvas, times(1)).drawPoints( + any(float[].class), + eq(0), + eq(4), + eq(formatter.getVertexPaint())); + + // second segment + verify(renderer, times(1)).drawSegment( + eq(canvas), + any(float[].class), + eq(6), + eq(10), + eq(formatter)); + + verify(canvas, times(1)).drawPoints( + any(float[].class), + eq(6), + eq(10), + eq(formatter.getVertexPaint())); + + // third segment + verify(renderer, times(1)).drawSegment( + eq(canvas), + any(float[].class), + eq(20), + eq(2), + eq(formatter)); + + verify(canvas, times(1)).drawPoints( + any(float[].class), + eq(20), + eq(2), + eq(formatter.getVertexPaint())); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java index e59205c7..bd98c386 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java @@ -19,7 +19,6 @@ import android.graphics.*; import com.androidplot.Plot; import com.androidplot.test.AndroidplotTest; -import mockit.*; import org.junit.After; import org.junit.Test; import org.robolectric.RuntimeEnvironment; @@ -28,13 +27,28 @@ public class XYLegendWidgetTest extends AndroidplotTest { + static class MockXYPlot extends XYPlot { + + public MockXYPlot() { + super(RuntimeEnvironment.application, "Test", + Plot.RenderMode.USE_MAIN_THREAD); + } + + public void exposedOnSizeChanged(int w, int h, int oldw, int oldh) { + this.onSizeChanged(w, h, oldw, oldh); + } + + public void exposedOnDraw(Canvas canvas) { + this.onDraw(canvas); + } + } + @After public void tearDown() throws Exception {} @Test public void testDoOnDraw() throws Exception { - XYPlot plot = new XYPlot(RuntimeEnvironment.application, "Test", - Plot.RenderMode.USE_MAIN_THREAD); + MockXYPlot plot = new MockXYPlot(); SimpleXYSeries s1 = new SimpleXYSeries((Arrays.asList(1, 2, 3)), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1"); @@ -44,11 +58,11 @@ public void testDoOnDraw() throws Exception { assertEquals(1, plot.getSeriesRegistry().size()); - Deencapsulation.invoke(plot, "onSizeChanged", 100, 100, 100, 100); + plot.exposedOnSizeChanged(100, 100, 100, 100); plot.redraw(); // have to manually invoke this because the invalidate() // invoked by redraw() is a stub and will not result in onDraw being called. - Deencapsulation.invoke(plot, "onDraw", new Canvas()); + plot.exposedOnDraw(new Canvas()); plot.removeSeries(s1); assertEquals(0, plot.getSeriesRegistry().size()); @@ -57,7 +71,7 @@ public void testDoOnDraw() throws Exception { // throws NullPointerException before fix // for ANDROIDPLOT-166 was applied. - Deencapsulation.invoke(plot, "onDraw", new Canvas()); + plot.exposedOnDraw(new Canvas()); } } diff --git a/build.gradle b/build.gradle index 2c262f42..4b925745 100644 --- a/build.gradle +++ b/build.gradle @@ -22,12 +22,12 @@ repositories { } ext { - theBuildToolsVersion = '21.1.2' - theCompileSdkVersion = 23 - theTargetSdkVersion = 23 + theBuildToolsVersion = '24.0.2' + theCompileSdkVersion = 24 + theTargetSdkVersion = 24 theMinSdkVersion = 5 - theVersionName = '1.1.0' - theVersionCode = 17 + theVersionName = '1.2.1' + theVersionCode = 19 } buildscript { @@ -37,10 +37,9 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' - classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.+' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' + classpath 'com.android.tools.build:gradle:2.2.0-beta3' + classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' } } @@ -59,5 +58,5 @@ allprojects { }*/ task wrapper(type: Wrapper) { - gradleVersion = '2.9' + gradleVersion = '2.14.1' } \ No newline at end of file diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000..fea51218 --- /dev/null +++ b/circle.yml @@ -0,0 +1,44 @@ + +dependencies: + + pre: + - echo y | android update sdk --no-ui --all --filter "android-24" + - echo y | android update sdk --no-ui --all --filter "build-tools-24.0.2" + +test: + + override: + - (./gradlew test assembleRelease javadoc): + timeout: 360 + + post: + + # core lib: + - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/outputs/aar/ $CIRCLE_ARTIFACTS + + # demo app .apk: + - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/demoapp/build/outputs/apk/ $CIRCLE_ARTIFACTS + + # javadoc: + - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/docs/javadoc/ $CIRCLE_ARTIFACTS + + + # junit xml report: + - mkdir -p $CIRCLE_TEST_REPORTS/junit-xml/ + - find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit-xml/ \; + + # junit html report: + # TODO: recursively copy subdirs etc + - mkdir -p $CIRCLE_TEST_REPORTS/junit-html/ + - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/tests/release/* $CIRCLE_TEST_REPORTS/junit-html/ + + # lint report: + - mkdir -p $CIRCLE_TEST_REPORTS/lint/ + - find . -type f -regex ".*/build/outputs/.*html" -exec cp {} $CIRCLE_TEST_REPORTS/lint/ \; + +deployment: + master: + branch: master + commands: + - (./gradlew bintrayUpload): + timeout: 360 diff --git a/demoapp/build.gradle b/demoapp/build.gradle index 8eed462d..34ccb014 100644 --- a/demoapp/build.gradle +++ b/demoapp/build.gradle @@ -14,8 +14,7 @@ * limitations under the License. */ -apply plugin: 'android-sdk-manager' -apply plugin: 'android' +apply plugin: 'com.android.application' dependencies { compile project(':androidplot-core') diff --git a/demoapp/src/main/AndroidManifest.xml b/demoapp/src/main/AndroidManifest.xml index 8f263127..d24f8094 100644 --- a/demoapp/src/main/AndroidManifest.xml +++ b/demoapp/src/main/AndroidManifest.xml @@ -20,10 +20,8 @@ - - - diff --git a/demoapp/src/main/java/com/androidplot/demos/ECGExample.java b/demoapp/src/main/java/com/androidplot/demos/ECGExample.java index b9559d32..73ce4f15 100644 --- a/demoapp/src/main/java/com/androidplot/demos/ECGExample.java +++ b/demoapp/src/main/java/com/androidplot/demos/ECGExample.java @@ -70,9 +70,6 @@ public void onUpdate(int latestIndex) { // reduce the number of range labels plot.setLinesPerRangeLabel(3); - // rotate domain labels 45 degrees to make them more compact horizontally: - //plot.getGraphWidget().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).setRotation(-45); - // start generating ecg data in the background: ecgSeries.start(); diff --git a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java index c448986a..91716b94 100644 --- a/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java @@ -18,7 +18,7 @@ import android.app.Activity; import android.content.Context; -import android.graphics.Color; +import android.graphics.*; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; @@ -110,13 +110,13 @@ public void onCreate(Bundle savedInstanceState) { aprHistoryPlot.setRangeBoundaries(-180, 359, BoundaryMode.FIXED); aprHistoryPlot.setDomainBoundaries(0, HISTORY_SIZE, BoundaryMode.FIXED); aprHistoryPlot.addSeries(azimuthHistorySeries, - new LineAndPointFormatter( + new FastLineAndPointRenderer.Formatter( Color.rgb(100, 100, 200), null, null, null)); aprHistoryPlot.addSeries(pitchHistorySeries, - new LineAndPointFormatter( + new FastLineAndPointRenderer.Formatter( Color.rgb(100, 200, 100), null, null, null)); aprHistoryPlot.addSeries(rollHistorySeries, - new LineAndPointFormatter( + new FastLineAndPointRenderer.Formatter( Color.rgb(200, 100, 100), null, null, null)); aprHistoryPlot.setDomainStepMode(StepMode.INCREMENT_BY_VAL); aprHistoryPlot.setDomainStepValue(HISTORY_SIZE/10); diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java index d5e6b0cb..cdf0e5c2 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java @@ -27,6 +27,9 @@ import com.androidplot.pie.PieRenderer; import com.androidplot.pie.Segment; import com.androidplot.pie.SegmentFormatter; +import com.androidplot.util.*; + +import java.util.*; /** * The simplest possible example of using AndroidPlot to plot some data. @@ -34,10 +37,12 @@ public class SimplePieChartActivity extends Activity { + public static final int SELECTED_SEGMENT_OFFSET = 50; + private TextView donutSizeTextView; private SeekBar donutSizeSeekBar; - private PieChart pie; + public PieChart pie; private Segment s1; private Segment s2; @@ -54,6 +59,9 @@ public void onCreate(Bundle savedInstanceState) // initialize our XYPlot reference: pie = (PieChart) findViewById(R.id.mySimplePieChart); + final float padding = PixelUtils.dpToPix(30); + pie.getPie().setPadding(padding, padding, padding, padding); + // detect segment clicks: pie.setOnTouchListener(new View.OnTouchListener() { @Override @@ -61,19 +69,36 @@ 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); - if(segment != null) { - // handle the segment click...for now, just print - // the clicked segment's title to the console: - System.out.println("Clicked Segment: " + segment.getTitle()); - } + final boolean isSelected = getFormatter(segment).getOffset() != 0; + deselectAll(); + setSelected(segment, !isSelected); + pie.redraw(); } return false; } - }); + private SegmentFormatter getFormatter(Segment segment) { + return pie.getFormatter(segment, PieRenderer.class); + } - donutSizeSeekBar = (SeekBar) findViewById(R.id.donutSizeSeekBar); + private void deselectAll() { + List segments = pie.getSeriesRegistry().getSeriesList(); + for(Segment segment : segments) { + setSelected(segment, false); + } + } + private void setSelected(Segment segment, boolean isSelected) { + SegmentFormatter f = getFormatter(segment); + if(isSelected) { + f.setOffset(SELECTED_SEGMENT_OFFSET); + } else { + f.setOffset(0); + } + } + }); + + donutSizeSeekBar = (SeekBar) findViewById(R.id.donutSizeSeekBar); donutSizeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) {} @@ -93,38 +118,38 @@ public void onStopTrackingTouch(SeekBar seekBar) { donutSizeTextView = (TextView) findViewById(R.id.donutSizeTextView); updateDonutText(); - s1 = new Segment("s1", 10); + s1 = new Segment("s1", 3); s2 = new Segment("s2", 1); - s3 = new Segment("s3", 10); - s4 = new Segment("s4", 10); + s3 = new Segment("s3", 7); + s4 = new Segment("s4", 9); EmbossMaskFilter emf = new EmbossMaskFilter( new float[]{1, 1, 1}, 0.4f, 10, 8.2f); SegmentFormatter sf1 = new SegmentFormatter(); sf1.configure(getApplicationContext(), R.xml.pie_segment_formatter1); - + sf1.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf1.getFillPaint().setMaskFilter(emf); SegmentFormatter sf2 = new SegmentFormatter(); sf2.configure(getApplicationContext(), R.xml.pie_segment_formatter2); - + sf2.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf2.getFillPaint().setMaskFilter(emf); SegmentFormatter sf3 = new SegmentFormatter(); sf3.configure(getApplicationContext(), R.xml.pie_segment_formatter3); - + sf3.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf3.getFillPaint().setMaskFilter(emf); SegmentFormatter sf4 = new SegmentFormatter(); sf4.configure(getApplicationContext(), R.xml.pie_segment_formatter4); - + sf4.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf4.getFillPaint().setMaskFilter(emf); - pie.addSeries(s1, sf1); - pie.addSeries(s2, sf2); - pie.addSeries(s3, sf3); - pie.addSeries(s4, sf4); + pie.addSegment(s1, sf1); + pie.addSegment(s2, sf2); + pie.addSegment(s3, sf3); + pie.addSegment(s4, sf4); pie.getBorderPaint().setColor(Color.TRANSPARENT); pie.getBackgroundPaint().setColor(Color.TRANSPARENT); diff --git a/demoapp/src/main/res/values/attrs.xml b/demoapp/src/main/res/values/attrs.xml deleted file mode 100644 index 6eb58380..00000000 --- a/demoapp/src/main/res/values/attrs.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/docs/dynamicdata.md b/docs/dynamicdata.md new file mode 100644 index 00000000..bce02b5a --- /dev/null +++ b/docs/dynamicdata.md @@ -0,0 +1,84 @@ +# Plotting Dynamic Data +One of Androidplot's core focuses is on plotting dynamically changing data in real-time. Special care has +been put into the design of the library to provide solutions for various scenarios and synchronization concerns. + +# Background Rendering +By default, Androidplot does all of it's rendering on the UI thread. While this is fine for most applications, +it can cause performance issues when rendering larger datasets or when continuously redrawing dynamic data. +To resolve this issue, Androidplot provides a background rendering mode that can be enabled programmatically in +your Java source or via XML: + +**Programmatically:** +```java +plot.setRenderMode(Plot.RenderMode.USE_BACKGROUND_THREAD); +``` + +**XML:** +```xml +ap:renderMode="use_background_thread" +``` + +In general, if your plot is continuously redrawing the plot, you should use background rendering. + +#Rendering Dynamic Data +There are two general approaches to dynamically rendering data: event driven and render loops. Each has +pros and cons and often times, the application might force you to use one approach over the other, however +90% of the time using a render loop is the better approach. + +In both cases the redraw is triggered by invoking `Plot.redraw().` + +## Render Loops +A render loop is basically a continuous loop of calls to a render routine. This loop might +include logic for updating the data being plotted, or it might simply focus on maintaining a stable refresh rate. +Androidplot provides a convenience utility, Redrawer which provides a basic implementation of a render loop +running on a fixed frequency. Check out the [ECG demo source](../demoapp/src/main/java/com/androidplot/demos/ECGExample.java) for a working example. + +## Event Driven Redraws +Sometimes it's more efficient to only redraw the plot as a result of an event such as a GPS update, button click, etc. +Event driven updates are as simple as invoking `Plot.redraw()` from the callback handling the event of interest, +after the data being plotted has been updated. While it's possible for the event triggering the redraw +to fire at a faster rate than the plot is capable of redrawing, no special care needs to be taken as Androidplot +ignores subsequent invocations of redraw() when a previous invocation is already active. + +# Synchronization +A major challenge of plotting dynamic data is the need to render an accurate representation of the data +as it existed at a specific point in time. This is not a trivial problem and the right solution depends +greatly on the specific details of the project. Presented here are a list of items to keep in mind +along with general best practices and approaches to solving more common problems. + +The most common scenario is when your data model is being dynamically updated by a background thread. +Regardless of whether or not you use background rendering here, invoking plot.redraw() is asynchronous +and your background thread can easily loop around and start altering values while your plot is half way through +rendering the previous set of values. The result of a race condition is indeterminate; if the size of your data +changed the app could crash from an IndexOutOfBoundsException, or a bogus representation of your data might +be rendered to the display. + +To prevent the race condition, changes to the data model must be synchronized with Androidplot's +rendering loop. A PlotListener can be used to detect when a Plot begins and ends a redraw and a read-write +can be used to protect the model (typically any series data being drawn): + +```java +plot.addListener(new PlotListener() { + @Override + public void onBeforeDraw(Plot source, Canvas canvas) { + // write-lock each active series for writes + } + + @Override + public void onAfterDraw(Plot source, Canvas canvas) { + // unlock any locked series + } +}); +``` + +In-depth usage of thread synchronization and read-write locks is beyond scope for this doc but you can +check out the source code of the SimpleXYSeries class for a functional example of ReentrantReadWriteLock +being used to synchronize an XYSeries implementation. + +# Examples +Androidplot provides a few full-source examples of how to dynamically plot data as demonstrated +in the [demo app](https://play.google.com/store/apps/details?id=com.androidplot.demos&hl=en). + +* [Plotting Realtime Orientation Sensor Data](../demoapp/src/main/java/com/androidplot/demos/OrientationSensorExampleActivity.java) +* [An ECG Example](../demoapp/src/main/java/com/androidplot/demos/ECGExample.java) +* [A simple dynamic XYPlot](../demoapp/src/main/java/com/androidplot/demos/DynamicXYPlotActivity.java) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 9cdd5448..077f592e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ specific plot types explaining styling and other advanced topics. * [Bar Charts](barchart.md) * [Candlestick Charts](candlestick.md) * [Pie Charts](piechart.md) -* Dynamic Plots (TODO) +* [Dynamic Plots](dynamicdata.md) # Examples Source code examples of the various plot types. diff --git a/docs/piechart.md b/docs/piechart.md index 70044ab3..2c0eaf3f 100644 --- a/docs/piechart.md +++ b/docs/piechart.md @@ -1,2 +1,50 @@ # Pie Charts -See the DemoApp's [pie chart example source](../demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java). \ No newline at end of file +Like all other primary chart and graph classes in Androidplot, PieChart is simply a composition +of widgets along with convenience methods. The real work is actually done by PieWidget. + +# Basic Usage +Pie charts are composed of one or more values called Segments which combines a vaue and a label: + +```java +Segment segment = new Segment("my segment", 10); +``` + +Displaying a Segment to a pie chart also requires a SegmentFormatter. SegmentFormatter is what defines +the visual characteristics of the associated segment; color, border thickness, +segment offset, label text style (if any) etc. The below code instantiates a new SegmentFormatter +with a segment color of red: + +```java +SegmentFormatter formatter = new SegmentFormatter(Color.RED); +``` + +Finally, the Segment must be added to the pie chart along with it's SegmentFormatter: + +```java +pie.addSegment(segment, formatter); +``` + +# SegmentFormatter +As mentioned above, SegmentFormatter defines how a Segment is visually represented in a PieChart. +In addition to basic color / border line styling params, there are several parameters available +to visually distinguish a segment: + +## Offset +Value in pixels used to "shift" the position of the segment relative to the center of the PieChart. +The visual effect is equivalent to cutting up a pie and dragging one piece outward, away from the rest of the pie. + +## RadialInset +Value in degrees used to shrink the radial edges of the segment inward. The visual effect is equivalent +to shaving the edges off of a slice of pie, resulting in a narrower slice of pie. + +## InnerInset +Value in pixels used to shrink the inner section of the pie. The visual effect is equivalent to +cutting the tip off of a slice of pie. + +## OuterInset +Value in pixels used to shrink the outer edge of the pie. The visual effect is equivalent to evenly +shaving the crust off of a slice of pie. + +See the DemoApp's +[pie chart example source](../demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java) for a comprehensive usage example. + diff --git a/docs/plot_composition.md b/docs/plot_composition.md index a7643b7c..20b80952 100644 --- a/docs/plot_composition.md +++ b/docs/plot_composition.md @@ -18,10 +18,9 @@ behavior encapsulated by that Plot. In addition to moving and scaling these Wid also extend them and replace the Plot's default instance with the derived implenentation in order to get custom behavior. -Internally, an instance of LayoutManager is used by Plot to manage it's Widgets. - # The LayoutManager -The LayoutManager provides the logic for visually positioning and scaling Widgets. +The LayoutManager provides the logic for visually positioning and scaling Widgets; all Plot implementations +contain an instance of LayoutManager that can be retrieved via `Plot.getLayoutManager()`. ## Z-Indexing Z-indexing is a 2D drawing concept which associates each drawable entity with a value that determines diff --git a/docs/quickstart.md b/docs/quickstart.md index 77755024..40d58bce 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -5,7 +5,7 @@ This tutorial will walk you through Adding Androidplot as a dependency and displaying an XYPlot with two data series. You can view this sample on your own device or emulator by installing the [Demo App](https://play.google.com/store/apps/details?id=com.androidplot.demos) on your device and -clicking on the Simple XY Plot Example. Full source [available here](https://bitbucket.org/androidplot/androidplot/src/26bbcfccb8ad0ba05f45f2d59a7e7ae40957b579/demoapp/?at=master). +clicking on the Simple XY Plot Example. Full source [available here](../demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java). You can also watch this tutorial in video form on [Youtube](https://youtu.be/wEFkzQY_wWI). @@ -24,8 +24,8 @@ want add this to your proguard-rules.pro file: `-keep class com.androidplot.** { *; }` # Create your Activity XML Layout -Once you’ve got the project skeleton created, it’s time to edit **res/layout/simple_xy_plot_example.xml** -and add an XYPlot view. By default this file is usually named main.xml. If it is, go ahead and rename it. +Once you’ve got an Android project skeleton created, create **res/layout/simple_xy_plot_example.xml** +and add an XYPlot view: ```xml ``` This example uses a default style to decorate the plot. The full list of styleable attributes is -[available here](../androiplot-core/src/main/res/attrs.xml). While new attributes are added regularly, +[available here](../androidplot-core/src/main/res/attrs.xml). While new attributes are added regularly, not all configurable properties are yet available. If something you need is missing, use the [Configurator.](http://androidplot.com/docs/xml-styling-with-configurator/) # Create an Activity -Let’s look at an example Activity that displays the XYPlot graphic shown above. The basic steps to follow are: +Now let's create an Activity to display the XYPlot we just defined in `simple_xy_plot_example.xml`. +The basic steps are: 1. Create an instance of Series and populate it with data to be displayed. -2. Register the series with the Plot along with a Formatter to describing how the data should be styled. +2. Register one or more series with the plot instance along with a Formatter to describing how the series should look when drawn. 3. Draw the Plot -In our case, since we are working with XY data, we’ll be using an XYPlot, a SimpleXYSeries (which is an -implementation of the XYSeries interface) and a LineAndPointFormatter. +Since we're working with XY data, we’ll use XYPlot, SimpleXYSeries (which is an +implementation of the XYSeries interface) and LineAndPointFormatter: ```java package com.androidplot.demos; diff --git a/docs/release_notes.md b/docs/release_notes.md index 5789b1d7..52ec9c55 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -1,4 +1,23 @@ +# 1.2.0 + +### Pie Chart Enhancements +Pie chart has been updated with new methods and format attributes to improve segment +selection and highlighting functionality: + +* Added `offset`, `radialInset`, `innerInset` and `outerInset` properties to SegmentFormatter. +See the [pie chart documentation](piechart.md) for usage details. +* Updated PieRenderer to support the new SegmentFormatter properties. +* SimplePieChartActivity has been updated to provide an interactive demo of some of these new features. + +### Misc + +* Added `Plot.getListeners()` method. +* Added FastLineAndPointRenderer, updated OrientationSensorExampleActivity to use it. +* Updated to target SDK 24, removed sdkmanager dependency, and other misc. updates to project deps etc. +* Lots of additions and updates to documentation + # 1.1.0 + * Added drawGridOnTop param to XYGraphWidget; when set to true, grid lines will be drawn on top of rendered series data. (default is false) * Added PanZoom class providing one-line configuration of pan/zoom behavior for instances of XYPlot. See TouchZoomExampleActivity for a usage example. * Removed InteractiveXYPlot as PanZoom makes it obsolete. @@ -6,6 +25,7 @@ # 1.0.0 This is a factor of several core elements of the Androidplot lib. The general theme was to make class and method names more intuitive and to make xml styling more powerful. + * Major refactor of XYGraphWidget * Tick renamed to Line * Extensible Label Formatters diff --git a/docs/xyplot.md b/docs/xyplot.md index 01d41476..9323d1e1 100644 --- a/docs/xyplot.md +++ b/docs/xyplot.md @@ -8,21 +8,22 @@ of [XYLegendWidget](#legend) which by default will automatically display legend # XYSeries XYSeries is the interface Androidplot uses to retrieve your numeric data. You may either create your own implementation of XYSeries or use SimpleXYSeries if you don't have tight performance requirements or -if your numeric data is easily accessed as an array or list of values. As a convenience, Androidplot provides -an all-purpose implementation of the XYSeries interface called SimpleXYSeries. +if your numeric data is easily accessed as an array or list of values. ## SimpleXYSeries -SimpleXYSeries is used to wrap your raw data with an implementation of the XYSeries interface. You can supply -your data in several ways: +As a convenience, Androidplot provides an all-purpose implementation of the XYSeries interface called +SimpleXYSeries. SimpleXYSeries is used to wrap your raw data with an implementation of the XYSeries interface. -As a list of y-vals; the x-val is implicitly set to array index of each supplied y-val: +You can supply your data in several ways: + +As a list of y-vals (x = i for each supplied y-val): ```java Number[] yVals = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64}; XYSeries series1 = new SimpleXYSeries( Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "my series"); ``` -An interleaved list of x/y value pairs: +An interleaved list of x/y value pairs (x[0] = 1, y[0] = 4, x[1] = 2, y[1] = 8, ...): ```java Number[] yVals = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64}; XYSeries series1 = new SimpleXYSeries( @@ -36,9 +37,9 @@ Number[] yVals = {5, 2, 10, 5, 20, 10, 40, 20, 80, 40}; XYSeries series = new SimpleXYSeries(xVals, yVals, "my series"); ``` -NOTE: SimpleXYSeries is designed to be easy to use for a broad number of applications,; it is not -designed for speed; if you are dynamically displaying data that needs to be refreshed more than several -times a second, you'll probably want to write an implementation of XYSeries designed for your app's +Keep in mind that SimpleXYSeries is designed to be easy to use for a broad number of applications; it's not +optimized for any specific scenario; if you are dynamically displaying data that needs to be refreshed more than several +times a second, consider building your own implementation of XYSeries designed for your app's specific needs. # The Graph @@ -101,6 +102,21 @@ Androidplot supports labeling domain values on either or both the top and bottom and range values on either or both the left and right graph edges. Most default styles show labels only on the left and bottom edges. +### Line Label Insets +Insets are used to control where line labels are drawn in relation to the graph space. The Insets instance +can be obtained via `XYPlot.getGraph().getLineLabelInsets()`. For example, to +move the range labels on the left of the graph further to the left by 5dp: + +**Programmatically:** +```java +plot.getGraph().getLineLabelInsets().setLeft(PixelUtils.dpToPix(-5)); +``` + +**XML:** +```xml +ap:lineLabelInsetLeft="-5dp" +``` + ### Dual Axis Labels Sometimes it is desirable to display additional labels for a single axis, each using it's own scale. @@ -173,6 +189,26 @@ your own by either extending XYSeriesRenderer or one of the above implementation to create a matching implementation of XYSeriesFormatter that returns your XYSeriesRenderer's class from it's `getRendererClass()` method. +## LineAndPointRenderer +TODO + +Androidplot includes two additional variations of LineAndPointRenderer: + +* **FastLineAndPointRenderer** - intended for use in apps displaying large amounts of dynamic data where +fast refresh rates are important. +* **AdvancedLineAndPointRenderer** - provides capabilities for dynamically coloring individual line +segments, etc. See the ECG source in the demo app for a full source example. +within a + +### Labeling Points +TODO + +## BarRenderer +See the [barcharts documentation](barchart.md). + +## CandlestickRenderer +See the [candlestick documentation](candlestick.md) + # Drawing Smooth Lines Smooth lines can be created by applying the [Catmull-Rom interpolator](http://androidplot.com/smooth-curves-and-androidplot/) to your series' Format. @@ -208,4 +244,26 @@ a TableOrder of COLUMN_MAJOR: ```java plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300), PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR)); -``` \ No newline at end of file +``` + +# Optimization Tips +Here are a few suggestions to improve performance when plotting dynamic data: + +* Create your own implementation of XYSeries to work with your data in it's rawest form. +* Use FastLineAndPointRenderer instead of LineAndPointRenderer: + +```java +plot.addSeries(azimuthHistorySeries, + new FastLineAndPointRenderer.Formatter( + Color.rgb(100, 100, 200), null, null, null)); +``` + +* Consider averaging or subsampling very large datasets before rendering. If you have the time and +inclination, [the LTTB algorithm](http://skemman.is/stream/get/1946/15343/37285/3/SS_MSthesis.pdf) is particularly well suited for downsampling XY Series data. +* If possible, avoid rendering vertices (points). +* Disable anti-aliasing on your Formatter's paint values: + +```java +LineAndPointFormatter format = new LineAndPointFormatter(...); +format.getLinePaint().setAntiAlias(false); +``` diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1ec706de..f7eef2f8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip From a731d75e855d22d9e1a6f057d7f4d8d4557db4e2 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Thu, 15 Sep 2016 10:33:22 -0500 Subject: [PATCH 005/104] updated documentation for 1.2.1 --- docs/quickstart.md | 2 +- docs/release_notes.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 40d58bce..1e172c07 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -14,7 +14,7 @@ To use the library in your gradle project add the following to your build.gradle ```groovy dependencies { - compile "com.androidplot:androidplot-core:1.1.0" + compile "com.androidplot:androidplot-core:1.2.1" } ``` diff --git a/docs/release_notes.md b/docs/release_notes.md index 52ec9c55..5ca13a24 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -1,4 +1,4 @@ -# 1.2.0 +# 1.2.1 ### Pie Chart Enhancements Pie chart has been updated with new methods and format attributes to improve segment @@ -15,6 +15,7 @@ See the [pie chart documentation](piechart.md) for usage details. * Added FastLineAndPointRenderer, updated OrientationSensorExampleActivity to use it. * Updated to target SDK 24, removed sdkmanager dependency, and other misc. updates to project deps etc. * Lots of additions and updates to documentation +* Added CircleCI support # 1.1.0 From 10e4ad2f728e953d08ef14588da49a5dffb20c73 Mon Sep 17 00:00:00 2001 From: daberni Date: Thu, 15 Sep 2016 17:34:13 +0200 Subject: [PATCH 006/104] fixed styleable bug (#5) --- .../src/main/java/com/androidplot/Plot.java | 87 ++++++++----------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index fd518be3..735b4d05 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -454,73 +454,54 @@ private void loadAttrs(AttributeSet attrs, int defStyle) { Field styleableFieldInR = null; TypedArray typedAttrs = null; - - final String appPkg = getContext().getPackageName(); - Class styleableClass = null; + + Class styleableClass = R.styleable.class; + String styleableName = getClass().getName().substring(BASE_PACKAGE.length()); + styleableName = styleableName.replace('.', '_'); try { /** - * Need to retrieve R$styleable.class dynamically to avoid exceptions - * in environments where this class does not exist, such as .jar users - * that have not merged the library's attrs.xml with their own. + * Use reflection to safely check for the existence of styleable defs for Plot + * and it's derivatives. This safety check is necessary to avoid runtime exceptions + * in apps that don't include Androidplot as a .aar and won't have access to + * the resources defined in the core library. */ - styleableClass = Class.forName(appPkg + ".R$styleable"); - - } catch (ClassNotFoundException e) { - // when running as a preview in IntelliJ or AndroidStudio it seems that the package of R is - // something else; this fixes that issue: - if(isInEditMode()) { - styleableClass = R.styleable.class; - } + styleableFieldInR = styleableClass.getField(styleableName); + } catch (NoSuchFieldException e) { + Log.d(TAG, "Styleable definition not found for: " + styleableName); } - - if(styleableClass != null) { - String styleableName = getClass().getName().substring(BASE_PACKAGE.length()); - styleableName = styleableName.replace('.', '_'); - try { - /** - * Use reflection to safely check for the existence of styleable defs for Plot - * and it's derivatives. This safety check is necessary to avoid runtime exceptions - * in apps that don't include Androidplot as a .aar and won't have access to - * the resources defined in the core library. - */ - styleableFieldInR = styleableClass.getField(styleableName); - } catch (NoSuchFieldException e) { - Log.d(TAG, "Styleable definition not found for: " + styleableName); - } - if (styleableFieldInR != null) { - try { - int[] resIds = (int[]) styleableFieldInR.get(null); - typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0); - } catch (IllegalAccessException e) { - // nothing to do - } finally { - if (typedAttrs != null) { - // apply derived class' attrs: - processAttrs(typedAttrs); - typedAttrs.recycle(); - } - } - } - + if (styleableFieldInR != null) { try { - styleableFieldInR = styleableClass.getField(Plot.class.getSimpleName()); - if (styleableFieldInR != null) { - int[] resIds = (int[]) styleableFieldInR.get(null); - typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0); - } + int[] resIds = (int[]) styleableFieldInR.get(null); + typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0); } catch (IllegalAccessException e) { // nothing to do - } catch (NoSuchFieldException e) { - Log.d(TAG, "Styleable definition not found for: " + Plot.class.getSimpleName()); } finally { if (typedAttrs != null) { - // apply base attrs: - processBaseAttrs(typedAttrs); + // apply derived class' attrs: + processAttrs(typedAttrs); typedAttrs.recycle(); } } } + try { + styleableFieldInR = styleableClass.getField(Plot.class.getSimpleName()); + if (styleableFieldInR != null) { + int[] resIds = (int[]) styleableFieldInR.get(null); + typedAttrs = getContext().obtainStyledAttributes(attrs, resIds, defStyle, 0); + } + } catch (IllegalAccessException e) { + // nothing to do + } catch (NoSuchFieldException e) { + Log.d(TAG, "Styleable definition not found for: " + Plot.class.getSimpleName()); + } finally { + if (typedAttrs != null) { + // apply base attrs: + processBaseAttrs(typedAttrs); + typedAttrs.recycle(); + } + } + // apply "configurator" attrs: (overrides any previously applied styleable attrs) // filter out androidplot prefixed attrs: HashMap attrHash = new HashMap(); From 091223cf54ed623b86edd0a6712eb11d0a0a8758 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Fri, 30 Sep 2016 23:44:41 -0500 Subject: [PATCH 007/104] 1.2.2 updates: (#8) * more unit test coverage * more help documentation * added jacoco coverage reports * replaced Configurator with Fig lib * updated various formatters to support direct construction from xml configs * fixed a bug with rendering regions in LineAndPointRenderer * phased out ValPixConverter in favor of Region/RectRegion transform ops * moved PointLabeler and PointLabelFormatter into XYSeriesFormatter for broader point label support. * dded basic implementation of bubble charts. * added CircleCI support --- README.md | 18 +- androidplot-core/build.gradle | 14 +- .../src/main/java/com/androidplot/Bounds.java | 78 --- .../main/java/com/androidplot/LineRegion.java | 101 --- .../src/main/java/com/androidplot/Plot.java | 8 +- .../src/main/java/com/androidplot/Region.java | 193 ++++++ .../java/com/androidplot/pie/PieRenderer.java | 4 +- .../com/androidplot/pie/SegmentFormatter.java | 12 +- .../java/com/androidplot/ui/BoxModelable.java | 8 +- .../java/com/androidplot/ui/Formatter.java | 13 +- .../main/java/com/androidplot/ui/Size.java | 10 - .../java/com/androidplot/util/AttrUtils.java | 18 +- .../com/androidplot/util/Configurator.java | 346 ----------- .../java/com/androidplot/util/FontUtils.java | 17 +- .../java/com/androidplot/util/PixelUtils.java | 44 -- .../com/androidplot/util/SeriesUtils.java | 114 ++-- .../com/androidplot/util/ValPixConverter.java | 99 --- .../xy/AdvancedLineAndPointRenderer.java | 26 +- .../java/com/androidplot/xy/BarFormatter.java | 19 +- .../java/com/androidplot/xy/BarRenderer.java | 92 +-- .../com/androidplot/xy/BubbleFormatter.java | 88 +++ .../com/androidplot/xy/BubbleRenderer.java | 152 +++++ .../java/com/androidplot/xy/BubbleSeries.java | 62 ++ .../androidplot/xy/CandlestickFormatter.java | 12 +- .../androidplot/xy/CandlestickRenderer.java | 96 ++- .../xy/FastLineAndPointRenderer.java | 22 +- .../com/androidplot/xy/GroupRenderer.java | 5 +- .../androidplot/xy/LineAndPointFormatter.java | 93 +-- .../androidplot/xy/LineAndPointRenderer.java | 79 +-- .../main/java/com/androidplot/xy/PanZoom.java | 52 +- .../androidplot/xy/PointLabelFormatter.java | 9 +- .../java/com/androidplot/xy/PointLabeler.java | 4 +- .../java/com/androidplot/xy/RectRegion.java | 255 ++++++-- .../com/androidplot/xy/StepFormatter.java | 2 - .../java/com/androidplot/xy/XYBounds.java | 98 --- .../com/androidplot/xy/XYConstraints.java | 6 +- .../java/com/androidplot/xy/XYCoords.java | 2 + .../com/androidplot/xy/XYGraphWidget.java | 588 +++++++++--------- .../main/java/com/androidplot/xy/XYPlot.java | 194 +++--- .../com/androidplot/xy/XYRegionFormatter.java | 5 +- .../com/androidplot/xy/XYSeriesFormatter.java | 43 ++ .../com/androidplot/xy/XYStepCalculator.java | 34 +- .../java/com/androidplot/xy/package-info.java | 4 + .../android/net/http/AndroidHttpClient.java | 24 - .../java/com/androidplot/LineRegionTest.java | 85 --- .../test/java/com/androidplot/PlotTest.java | 6 +- .../test/java/com/androidplot/RegionTest.java | 191 ++++-- .../com/androidplot/pie/TestPieRenderer.java | 96 +++ .../com/androidplot/test/AndroidplotTest.java | 19 +- .../androidplot/util/ConfiguratorTest.java | 91 --- .../com/androidplot/util/LayerHashTest.java | 97 +++ .../com/androidplot/util/SeriesUtilsTest.java | 10 +- .../androidplot/util/ValPixConverterTest.java | 125 ---- .../xy/LineAndPointRendererTest.java | 135 ++++ .../java/com/androidplot/xy/PanZoomTest.java | 92 +++ .../com/androidplot/xy/RectRegionTest.java | 39 ++ .../androidplot/xy/StepCalculatorTest.java | 31 +- .../com/androidplot/xy/XYGraphWidgetTest.java | 127 ++++ .../java/com/androidplot/xy/XYPlotTest.java | 173 +++--- .../androidplot/xy/XYSeriesRendererTest.java | 24 +- build.gradle | 23 +- circle.yml | 14 + demoapp/build.gradle | 39 ++ demoapp/src/main/AndroidManifest.xml | 1 + .../demos/BarPlotExampleActivity.java | 225 ++++--- .../demos/BubbleChartActivity.java | 71 +++ .../demos/CandlestickChartActivity.java | 62 +- .../demos/FXPlotExampleActivity.java | 5 +- .../com/androidplot/demos/MainActivity.java | 9 + .../demos/ScatterPlotActivity.java | 13 +- .../demos/SimplePieChartActivity.java | 12 +- .../demos/SimpleXYPlotActivity.java | 24 +- .../demos/TouchZoomExampleActivity.java | 7 +- .../demos/XYRegionExampleActivity.java | 121 +--- .../main/res/layout/bubble_chart_example.xml | 33 + demoapp/src/main/res/layout/main.xml | 6 + .../src/main/res/layout/xyregion_example.xml | 17 +- .../src/main/res/xml/bubble_formatter1.xml | 20 + .../src/main/res/xml/bubble_formatter2.xml | 20 + .../src/main/res/xml/bubble_formatter3.xml | 20 + .../main/res/xml/candlestick_formatter.xml | 23 + docs/barchart.md | 59 +- docs/bubblechart.md | 52 ++ docs/candlestick.md | 4 +- docs/grouprenderer.md | 18 + docs/images/aplogo.png | Bin 0 -> 8409 bytes docs/images/aplogo_small.png | Bin 0 -> 18442 bytes docs/images/bubble_chart.png | Bin 0 -> 60688 bytes docs/images/screens/about_screens.md | 7 + docs/images/screens/bar_horiz.png | Bin 0 -> 211793 bytes docs/images/screens/bar_vert.png | Bin 0 -> 211027 bytes docs/images/screens/bubble_horiz.png | Bin 0 -> 196882 bytes docs/images/screens/bubble_vert.png | Bin 0 -> 201180 bytes docs/images/screens/candlestick_horiz.png | Bin 0 -> 195150 bytes docs/images/screens/candlestick_vert.png | Bin 0 -> 199513 bytes docs/images/screens/fx_horiz.png | Bin 0 -> 268087 bytes docs/images/screens/fx_vert.png | Bin 0 -> 294715 bytes docs/images/screens/pie_horiz.png | Bin 0 -> 194711 bytes docs/images/screens/pie_vert.png | Bin 0 -> 204775 bytes docs/images/screens/scatter_horiz.png | Bin 0 -> 233430 bytes docs/images/screens/scatter_vert.png | Bin 0 -> 239611 bytes docs/images/screens/step_horiz.png | Bin 0 -> 192945 bytes docs/images/screens/step_vert.png | Bin 0 -> 197924 bytes docs/index.md | 15 +- docs/piechart.md | 2 + docs/quickstart.md | 58 +- docs/release_notes.md | 14 + misc/download_google_publisher_json.sh | 9 + misc/download_keystore.sh | 9 + 109 files changed, 3101 insertions(+), 2415 deletions(-) delete mode 100644 androidplot-core/src/main/java/com/androidplot/Bounds.java delete mode 100644 androidplot-core/src/main/java/com/androidplot/LineRegion.java create mode 100644 androidplot-core/src/main/java/com/androidplot/Region.java delete mode 100644 androidplot-core/src/main/java/com/androidplot/util/Configurator.java delete mode 100644 androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java delete mode 100644 androidplot-core/src/main/java/com/androidplot/xy/XYBounds.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/package-info.java delete mode 100644 androidplot-core/src/test/java/android/net/http/AndroidHttpClient.java delete mode 100644 androidplot-core/src/test/java/com/androidplot/LineRegionTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/pie/TestPieRenderer.java delete mode 100644 androidplot-core/src/test/java/com/androidplot/util/ConfiguratorTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java delete mode 100644 androidplot-core/src/test/java/com/androidplot/util/ValPixConverterTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java create mode 100644 androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java create mode 100644 demoapp/src/main/java/com/androidplot/demos/BubbleChartActivity.java create mode 100644 demoapp/src/main/res/layout/bubble_chart_example.xml create mode 100644 demoapp/src/main/res/xml/bubble_formatter1.xml create mode 100644 demoapp/src/main/res/xml/bubble_formatter2.xml create mode 100644 demoapp/src/main/res/xml/bubble_formatter3.xml create mode 100644 demoapp/src/main/res/xml/candlestick_formatter.xml create mode 100644 docs/bubblechart.md create mode 100644 docs/grouprenderer.md create mode 100644 docs/images/aplogo.png create mode 100644 docs/images/aplogo_small.png create mode 100644 docs/images/bubble_chart.png create mode 100644 docs/images/screens/about_screens.md create mode 100644 docs/images/screens/bar_horiz.png create mode 100644 docs/images/screens/bar_vert.png create mode 100644 docs/images/screens/bubble_horiz.png create mode 100644 docs/images/screens/bubble_vert.png create mode 100644 docs/images/screens/candlestick_horiz.png create mode 100644 docs/images/screens/candlestick_vert.png create mode 100644 docs/images/screens/fx_horiz.png create mode 100644 docs/images/screens/fx_vert.png create mode 100644 docs/images/screens/pie_horiz.png create mode 100644 docs/images/screens/pie_vert.png create mode 100644 docs/images/screens/scatter_horiz.png create mode 100644 docs/images/screens/scatter_vert.png create mode 100644 docs/images/screens/step_horiz.png create mode 100644 docs/images/screens/step_vert.png create mode 100755 misc/download_google_publisher_json.sh create mode 100755 misc/download_keystore.sh diff --git a/README.md b/README.md index 4e6e90f1..7736e7d4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ -# Androidplot - -![image](docs/images/preview.png) +# ![image](docs/images/aplogo_small.png) Androidplot [![CircleCI](https://circleci.com/gh/halfhp/androidplot.svg?style=shield)](https://circleci.com/gh/halfhp/androidplot) [![codecov](https://codecov.io/gh/halfhp/androidplot/branch/master/graph/badge.svg)](https://codecov.io/gh/halfhp/androidplot) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/androidplot.svg?style=social&label=Follow%20Us)](https://twitter.com/androidplot) A library for creating dynamic and static charts in Android apps. It’s designed from the ground up for Android, compatible with all versions of Android from 1.6 onward and is **used by over [1,000 apps](http://www.appbrain.com/stats/libraries/details/androidplot/androidplot) on Google Play**. + + + + + + + + **Features:** * Line Charts @@ -14,13 +20,13 @@ compatible with all versions of Android from 1.6 onward and is **used by over * Pie Charts * Step Charts * Candlestick Charts +* Bubble Charts * Dynamic plots * Pan & Zoom - # Usage -* :star: **[Quickstart](docs/quickstart.md)** +* **[Quickstart](docs/quickstart.md)** :star: * [Full Documentation](docs/index.md) # Links @@ -30,7 +36,7 @@ compatible with all versions of Android from 1.6 onward and is **used by over * [Bitbucket Repo](https://bitbucket.org/androidplot/androidplot) * [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) -* :ant: [Bugs](https://github.com/halfhp/androidplot/issues) +* [Bugs](https://github.com/halfhp/androidplot/issues) :ant: * [Contributing Source Code](http://androidplot.com/docs/working-with-androidplot-source/) # Help diff --git a/androidplot-core/build.gradle b/androidplot-core/build.gradle index b365641f..2ead754f 100644 --- a/androidplot-core/build.gradle +++ b/androidplot-core/build.gradle @@ -17,6 +17,8 @@ apply plugin: 'com.android.library' apply plugin: 'com.github.dcendents.android-maven' apply plugin: 'com.jfrog.bintray' +apply plugin: 'com.vanniktech.android.junit.jacoco' +apply plugin: 'com.github.kt3k.coveralls' android { compileSdkVersion theCompileSdkVersion @@ -42,11 +44,13 @@ group = 'com.androidplot' version = theVersionName def siteUrl = 'http://androidplot.com' -def gitUrl = 'https://bitbucket.org/androidplot/androidplot.git' +def gitUrl = 'https://github.com/halfhp/androidplot.git' dependencies { + + compile 'com.halfhp.fig:figlib:1.0.3' testCompile "org.mockito:mockito-core:1.10.19" - testCompile group: 'junit', name: 'junit', version: '4.9' + testCompile group: 'junit', name: 'junit', version: '4.12' testCompile "org.robolectric:robolectric:3.1" // temp fix for: @@ -124,13 +128,17 @@ bintray { } repo = "androidplot" name = "com.androidplot:androidplot-core" - issueTrackerUrl = "https://androidplot.jira.com" + issueTrackerUrl = "https://github.com/halfhp/androidplot/issues" websiteUrl = siteUrl vcsUrl = gitUrl licenses = ["Apache-2.0"] } } +javadoc { + options.overview = "src/main/java/overview.html" +} + artifacts { archives javadocJar archives sourcesJar diff --git a/androidplot-core/src/main/java/com/androidplot/Bounds.java b/androidplot-core/src/main/java/com/androidplot/Bounds.java deleted file mode 100644 index f91c0003..00000000 --- a/androidplot-core/src/main/java/com/androidplot/Bounds.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2016 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; - -/** - * Defines simple min/max boundary. Differs from {@link LineRegion} in that it accepts null values. - * @since 0.9.7 - */ -public class Bounds { - - private Number min; - private Number max; - - public Bounds() { - this(null, null); - } - - public Bounds(Number min, Number max) { - this.min = min; - this.max = max; - } - - public Number getMin() { - return min; - } - - public void setMin(Number min) { - this.min = min; - } - - public Number getMax() { - return max; - } - - public void setMax(Number max) { - this.max = max; - } - - /** - * Compares the input bounds min/max against this instance's current min/max. - * If the input.min is less than this.min then this.min will be set to input.min. - * If the input.max is greater than this.max then this.max will be set to input.max - * @param input - */ - public void union(Bounds input) { - if(this.min == null || input.min != null && - input.min.doubleValue() < this.min.doubleValue()) { - this.min = input.min; - } - if(this.max == null || input.max != null && input.max.doubleValue() > - this.max.doubleValue()) { - this.max = input.max; - } - } - - /** - * Inverse of {@link #union(Bounds)}. - * @param input - */ - public void intersect(Bounds input) { - // TODO - throw new UnsupportedOperationException("Not yet implemented."); - } -} diff --git a/androidplot-core/src/main/java/com/androidplot/LineRegion.java b/androidplot-core/src/main/java/com/androidplot/LineRegion.java deleted file mode 100644 index b656785d..00000000 --- a/androidplot-core/src/main/java/com/androidplot/LineRegion.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package com.androidplot; - -/** - * A one dimensional region represented by a starting and ending value. - */ -public class LineRegion { - private Number minVal; - private Number maxVal; - - public LineRegion(Number v1, Number v2) { - if (v1 != null && v2 != null && v1.doubleValue() < v2.doubleValue()) { - this.setMinVal(v1); - this.setMaxVal(v2); - } else { - this.setMinVal(v2); - this.setMaxVal(v1); - } - } - - /** - * - * @param v1 - * @param v2 - * @return The distance between val1 and val2 or null if either parameters are null. - * @since 0.9.7 - */ - public static Number measure(Number v1, Number v2) { - return new LineRegion(v1, v2).length(); - } - - public Number length() { - return maxVal == null || minVal == null ? null : maxVal.doubleValue() - minVal.doubleValue(); - } - - /** - * Tests whether a value is within the given range - * @param value - * @return - */ - public boolean contains(Number value) { - return value.doubleValue() >= minVal.doubleValue() && value.doubleValue() <= maxVal.doubleValue(); - } - - public boolean intersects(LineRegion lineRegion) { - return intersects(lineRegion.getMinVal(), lineRegion.getMaxVal()); - } - - /** - * Tests whether this segment intersects another - * @param line2Min - * @param line2Max - * @return - */ - public boolean intersects(Number line2Min, Number line2Max) { - - // is this line completely within line2? - if(line2Min.doubleValue() <= this.minVal.doubleValue() && line2Max.doubleValue() >= this.maxVal.doubleValue()) { - return true; - // is line1 partially within line2 - } else return contains(line2Min) || contains(line2Max); - } - - public Number getMinVal() { - return minVal; - } - - public void setMinVal(Number minVal) { - if(minVal == null) { - throw new NullPointerException("Region values can never be null."); - } - this.minVal = minVal; - } - - public Number getMaxVal() { - return maxVal; - } - - public void setMaxVal(Number maxVal) { - if(maxVal == null) { - throw new NullPointerException("Region values can never be null."); - } - this.maxVal = maxVal; - } -} diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index 735b4d05..6716ac42 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -31,11 +31,11 @@ import com.androidplot.ui.widget.TextLabelWidget; import com.androidplot.ui.SeriesRenderer; import com.androidplot.util.AttrUtils; -import com.androidplot.util.Configurator; import com.androidplot.util.DisplayDimensions; import com.androidplot.util.PixelUtils; import com.androidplot.ui.HorizontalPositioning; import com.androidplot.ui.VerticalPositioning; +import com.halfhp.fig.*; import java.lang.reflect.Field; import java.util.*; @@ -513,7 +513,7 @@ private void loadAttrs(AttributeSet attrs, int defStyle) { attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i)); } } - Configurator.configure(getContext(), this, attrHash); + Fig.configure(getContext(), this, attrHash); } } @@ -691,8 +691,8 @@ public FormatterType getFormatter(SeriesType series, Class rendererClass) { - return getRenderers().get(rendererClass); + public T getRenderer(Class rendererClass) { + return (T) getRenderers().get(rendererClass); } public List getRendererList() { diff --git a/androidplot-core/src/main/java/com/androidplot/Region.java b/androidplot-core/src/main/java/com/androidplot/Region.java new file mode 100644 index 00000000..6ed18341 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/Region.java @@ -0,0 +1,193 @@ +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.androidplot; + +import android.graphics.*; + +/** + * A one dimensional region represented by a starting and ending value. + */ +public class Region { + private Number min; + private Number max; + private Region defaults = this; + + public Region() {} + + public static Region withDefaults(Region defaults) { + if(defaults == null || !defaults.isDefined()) { + throw new IllegalArgumentException("When specifying default min and max must both be non-null values"); + } + Region r = new Region(); + r.defaults = defaults; + return r; + } + + public Region(Number v1, Number v2) { + if (v1 != null && v2 != null && v1.doubleValue() < v2.doubleValue()) { + this.setMin(v1); + this.setMax(v2); + } else { + this.setMin(v2); + this.setMax(v1); + } + } + + /** + * + * @param v1 + * @param v2 + * @return The distance between val1 and val2 or null if either parameters are null. + * @since 0.9.7 + */ + public static Number measure(Number v1, Number v2) { + return new Region(v1, v2).length(); + } + + public Number length() { + return getMax() == null || getMin() == null ? + null : getMax().doubleValue() - getMin().doubleValue(); + } + + /** + * Tests whether a value is within the given range + * @param value + * @return + */ + public boolean contains(Number value) { + return value.doubleValue() >= getMin().doubleValue() && value.doubleValue() <= getMax().doubleValue(); + } + + public boolean intersects(Region region) { + return intersects(region.getMin(), region.getMax()); + } + + /** + * + * @return Middle value within this region + */ + public Number center() { + return getMax().doubleValue() - (length().doubleValue() / 2); + } + + /** + * Transform a value relative to this region into it's corresponding value relative to the + * specified region. + * @param value + * @param region2 + * @return + */ + public Number transform(double value, Region region2) { + return transform(value, region2, false); + } + + public Number transform(double value, Region region2, boolean flip) { + return transform(value, region2.min.doubleValue(), region2.max.doubleValue(), flip); + } + + public double transform(double value, double min, double max, boolean flip) { + double range = length().doubleValue(); + final double r2 = max - min; + + // TODO: refactor to use ratio here + final double scale = r2 / range; + if(!flip) { + return min + (scale * (value - this.getMin().doubleValue())); + } else { + return max - (scale * (value - this.getMin().doubleValue())); + } + } + + public Number ratio(Region r2) { + return ratio(r2.min.doubleValue(), r2.max.doubleValue()); + } + + public double ratio(double min, double max) { + return length().doubleValue() / (max - min); + } + + /** + * Compares the input bounds min/max against this instance's current min/max. + * If the input.min is less than this.min then this.min will be set to input.min. + * If the input.max is greater than this.max then this.max will be set to input.max + * @param input + */ + public void union(Region input) { + if(getMin() == null || input.min != null && + input.min.doubleValue() < getMin().doubleValue()) { + setMin(input.min); + } + if(getMax() == null || input.max != null && input.max.doubleValue() > + getMax().doubleValue()) { + setMax(input.max); + } + } + + /** + * Tests whether this segment intersects another + * @param line2Min + * @param line2Max + * @return + */ + public boolean intersects(Number line2Min, Number line2Max) { + + // is this line completely within line2? + if(line2Min.doubleValue() <= getMin().doubleValue() && line2Max.doubleValue() >= getMax().doubleValue()) { + return true; + // is line1 partially within line2 + } else return contains(line2Min) || contains(line2Max); + } + + public boolean isMinSet() { + return min != null; + } + + public Number getMin() { + return isMinSet() ? min : defaults.min; + } + + public void setMin(Number min) { + if(min == null && defaults == null) { + throw new NullPointerException("Region values cannot be null unless defaults have been set."); + } + this.min = min; + } + + public boolean isMaxSet() { + return max != null; + } + + public Number getMax() { + return isMaxSet() ? max : defaults.max; + } + + public void setMax(Number max) { + if(max == null && defaults == null) { + throw new NullPointerException("Region values can never be null unless defaults have been set."); + } + this.max = max; + } + + /** + * + * @return True if both min and max values are non-null, false otherwise. Does *not* consider defaults. + */ + public boolean isDefined() { + return min != null && max != null; + } +} 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 6412628a..a53e38d4 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java @@ -190,7 +190,9 @@ protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentForm // 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. - drawSegmentLabel(canvas, labelOrigin, seg, f); + if(f.getLabelPaint() != null) { + drawSegmentLabel(canvas, labelOrigin, seg, f); + } } protected void drawSegmentLabel(Canvas canvas, PointF origin, diff --git a/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java b/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java index be81b9fb..d4e63aa1 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/SegmentFormatter.java @@ -16,6 +16,7 @@ package com.androidplot.pie; +import android.content.*; import android.graphics.Color; import android.graphics.Paint; import com.androidplot.ui.SeriesRenderer; @@ -76,11 +77,6 @@ public class SegmentFormatter extends Formatter { getLabelMarkerPaint().setStrokeWidth(DEFAULT_LABEL_MARKER_THICKNESS); } - /** - * Should only be used in conjunction with calls to configure()... - */ - public SegmentFormatter() {} - public SegmentFormatter(Integer fillColor) { if(fillColor != null) { getFillPaint().setColor(fillColor); @@ -89,6 +85,10 @@ public SegmentFormatter(Integer fillColor) { } } + public SegmentFormatter(Context context, int xmlCfgId) { + configure(context, xmlCfgId); + } + public SegmentFormatter(Integer fillColor, Integer borderColor) { this(fillColor); getInnerEdgePaint().setColor(borderColor); @@ -99,8 +99,6 @@ public SegmentFormatter(Integer fillColor, Integer borderColor) { public SegmentFormatter(Integer fillColor, Integer outerEdgeColor, Integer innerEdgeColor, Integer radialEdgeColor) { this(fillColor); - - if(getOuterEdgePaint() != null) { getOuterEdgePaint().setColor(outerEdgeColor); } else { diff --git a/androidplot-core/src/main/java/com/androidplot/ui/BoxModelable.java b/androidplot-core/src/main/java/com/androidplot/ui/BoxModelable.java index ffdf6b9e..9c953b45 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/BoxModelable.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/BoxModelable.java @@ -19,9 +19,11 @@ import android.graphics.RectF; /** - * Encapsulates the functionality of a BoxModel. - * See http://www.w3.org/TR/CSS21/box.html for a good explanation of how - * the box model works. + * Defines the properties of a BoxModel as used + * by Androidplot. Essentially, the BoxModel composes three nested (but not necessarily concentric) rectangles: + * * The bounding box, which is the outer-most box. + * * The marginated box, which is calculated by applying the margin insets to the bounding box. + * * The padded box, which is calculated by applying the padding insets to the marginated box. */ public interface BoxModelable { /** diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java index 51b1a94a..c63d8ae5 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java @@ -18,7 +18,7 @@ import android.content.Context; import com.androidplot.Plot; -import com.androidplot.util.Configurator; +import com.halfhp.fig.*; /** * Base class of all Formatters. Encapsulates visual elements of a series; line style, color etc. @@ -39,12 +39,15 @@ */ public abstract class Formatter { - public Formatter configure(Context ctx, int xmlCfgId) { - Configurator.configure(ctx, this, xmlCfgId); - return this; - } + public Formatter() {} + public Formatter(Context ctx, int xmlCfgId) { + configure(ctx, xmlCfgId); + } + public void configure(Context ctx, int xmlCfgId) { + Fig.configure(ctx, this, xmlCfgId); + } /** * diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Size.java b/androidplot-core/src/main/java/com/androidplot/ui/Size.java index 1aefa20d..a5461962 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Size.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Size.java @@ -78,16 +78,6 @@ public RectF getRectF(RectF canvasRect) { height.getPixelValue(canvasRect.height())); } - /** - * Same as getRectF but with edges rounded to the nearest full pixel. - * @param canvasRect - * @return - */ - public RectF getRoundedRect(RectF canvasRect) { - return PixelUtils.nearestPixRect(0, 0, width.getPixelValue(canvasRect.width()), - height.getPixelValue(canvasRect.height())); - } - public void setWidth(SizeMetric width) { this.width = width; } diff --git a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java index 4149c2b4..b8b6c7f7 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java @@ -18,8 +18,10 @@ import android.content.res.TypedArray; import android.graphics.Paint; -import android.util.TypedValue; +import android.util.*; + import com.androidplot.ui.*; +import com.androidplot.ui.Size; import com.androidplot.ui.widget.Widget; import com.androidplot.xy.StepMode; import com.androidplot.xy.StepModel; @@ -30,6 +32,8 @@ */ public class AttrUtils { + private static final String TAG = AttrUtils.class.getName(); + public static void configureInsets(TypedArray attrs, Insets insets, int topAttr, int bottomAttr, int leftAttr, int rightAttr) { insets.setTop(attrs.getDimension(topAttr, insets.getTop())); @@ -100,11 +104,15 @@ public static void configureLinePaint(TypedArray attrs, Paint paint, int colorAt } } - private static void setColor(TypedArray attrs, Paint paint, int attrId) { - paint.setColor(attrs.getColor(attrId, paint.getColor())); + public static void setColor(TypedArray attrs, Paint paint, int attrId) { + if(paint == null) { + Log.w(TAG, "Attempt to configure null Paint property for attrId: " + attrId); + } else { + paint.setColor(attrs.getColor(attrId, paint.getColor())); + } } - private static void setTextSize(TypedArray attrs, Paint paint, int attrId) { + public static void setTextSize(TypedArray attrs, Paint paint, int attrId) { paint.setTextSize(attrs.getDimension(attrId, paint.getTextSize())); } @@ -193,7 +201,7 @@ public static void configureWidget(TypedArray attrs, Widget widget, int heightSi public static void configurePositionMetrics(TypedArray attrs, PositionMetrics metrics, int xLayoutStyleAttr, int xLayoutValueAttr, int yLayoutStyleAttr, int yLayoutValueAttr, int anchorPositionAttr) { - if(attrs != null) { + if(attrs != null && metrics != null) { metrics.getXPositionMetric().set( getIntFloatDimenValue(attrs, xLayoutValueAttr, metrics.getXPositionMetric().getValue()).floatValue(), getXLayoutStyle(attrs, xLayoutStyleAttr, metrics.getXPositionMetric().getLayoutType())); diff --git a/androidplot-core/src/main/java/com/androidplot/util/Configurator.java b/androidplot-core/src/main/java/com/androidplot/util/Configurator.java deleted file mode 100644 index ac2eb063..00000000 --- a/androidplot-core/src/main/java/com/androidplot/util/Configurator.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot.util; - -import android.content.Context; -import android.content.res.XmlResourceParser; -import android.graphics.Color; -import android.util.Log; -import android.util.TypedValue; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.HashMap; - -/** - * Utility class for "configuring" objects via XML config files. Supports the following field types: - * String - * Enum - * int - * float - * boolean - *

- * Config files should be stored in /res/xml. Given the XML configuration /res/xml/myConfig.xml, one can apply the - * configuration to an Object instance as follows: - *

- * MyObject obj = new MyObject(); - * Configurator.configure(obj, R.xml.myConfig); - *

- * WHAT IT DOES: - * Given a series of parameters stored in an XML file, Configurator iterates through each parameter, using the name - * as a map to the field within a given object. For example: - *

- *

- * {@code
- * 
- * }
- * 
- *

- * Given a Car instance car and assuming the method setCondition(String) exists within the SparkPlug class, - * Configurator does the following: - *

- *

- * {@code
- * car.getEngine().getSparkPlug().setCondition("poor");
- * }
- * 
- *

- * Now let's pretend that setCondition takes an instance of the Condition enum as it's argument. - * Configurator then does the following: - *

- * car.getEngine().getSparkPlug().setCondition(Condition.valueOf("poor"); - *

- * Now let's look at how ints are handled. Given the following xml: - *

- * - *

- * would result in: - * car.getEngine.setMiles(Integer.ParseInt("100000"); - *

- * That's pretty straight forward. But colors are expressed as ints too in Android - * but can be defined using hex values or even names of colors. When Configurator - * attempts to parse a parameter for a method that it knows takes an int as it's argument, - * Configurator will first attempt to parse the parameter as a color. Only after this - * attempt fails will Configurator resort to Integer.ParseInt. So: - *

- * - *

- * would result in: - * car.getHood().getPaint().setColor(Color.parseColor("Red"); - *

- * Next lets talk about float. Floats can appear in XML a few different ways in Android, - * especially when it comes to defining dimensions: - *

- * - *

- * Configurator will correctly parse each of these into their corresponding real pixel value expressed as a float. - *

- * One last thing to keep in mind when using Configurator: - * Values for Strings and ints can be assigned to localized values, allowing - * a cleaner solution for those developing apps to run on multiple form factors - * or in multiple languages: - *

- * - */ -@SuppressWarnings("WeakerAccess") -public abstract class Configurator { - - private static final String TAG = Configurator.class.getName(); - protected static final String CFG_ELEMENT_NAME = "config"; - - protected static int parseResId(Context ctx, String prefix, String value) { - String[] split = value.split("/"); - // is this a localized resource? - if (split.length > 1 && split[0].equalsIgnoreCase(prefix)) { - String pack = split[0].replace("@", ""); - String name = split[1]; - return ctx.getResources().getIdentifier(name, pack, ctx.getPackageName()); - } else { - throw new IllegalArgumentException(); - } - } - - protected static int parseIntAttr(Context ctx, String value) { - try { - return ctx.getResources().getColor(parseResId(ctx, "@color", value)); - } catch (IllegalArgumentException e1) { - try { - return Color.parseColor(value); - } catch (IllegalArgumentException e2) { - // wasn't a color so try parsing as a plain old int: - return Integer.parseInt(value); - } - } - } - - /** - * Treats value as a float parameter. First value is tested to see whether - * it contains a resource identifier. Failing that, it is tested to see whether - * a dimension suffix (dp, em, mm etc.) exists. Failing that, it is evaluated as - * a plain old float. - * @param ctx - * @param value - * @return - */ - protected static float parseFloatAttr(Context ctx, String value) { - try { - return ctx.getResources().getDimension(parseResId(ctx, "@dimen", value)); - } catch (IllegalArgumentException e1) { - try { - return PixelUtils.stringToDimension(value); - } catch (Exception e2) { - return Float.parseFloat(value); - } - } - } - - protected static String parseStringAttr(Context ctx, String value) { - try { - return ctx.getResources().getString(parseResId(ctx, "@string", value)); - } catch (IllegalArgumentException e1) { - return value; - } - } - - - protected static Method getSetter(Class clazz, final String fieldId) throws NoSuchMethodException { - Method[] methods = clazz.getMethods(); - - String methodName = "set" + fieldId; - for (Method method : methods) { - if (method.getName().equalsIgnoreCase(methodName)) { - return method; - } - } - throw new NoSuchMethodException("No such public method (case insensitive): " + - methodName + " in " + clazz); - } - - @SuppressWarnings("unchecked") - protected static Method getGetter(Class clazz, final String fieldId) throws NoSuchMethodException { - Log.d(TAG, "Attempting to find getter for " + fieldId + " in class " + clazz.getName()); - String firstLetter = fieldId.substring(0, 1); - String methodName = "get" + firstLetter.toUpperCase() + fieldId.substring(1, fieldId.length()); - return clazz.getMethod(methodName); - } - - /** - * Returns the object containing the field specified by path. - * @param obj - * @param path Path through member hierarchy to the destination field. - * @return null if the object at path cannot be found. - * @throws java.lang.reflect.InvocationTargetException - * - * @throws IllegalAccessException - */ - protected static Object getObjectContaining(Object obj, String path) throws - InvocationTargetException, IllegalAccessException, NoSuchMethodException { - if(obj == null) { - throw new NullPointerException("Attempt to call getObjectContaining(Object obj, String path) " + - "on a null Object instance. Path was: " + path); - } - Log.d(TAG, "Looking up object containing: " + path); - int separatorIndex = path.indexOf("."); - - // not there yet, descend deeper: - if (separatorIndex > 0) { - String lhs = path.substring(0, separatorIndex); - String rhs = path.substring(separatorIndex + 1, path.length()); - - // use getter to retrieve the instance - Method m = getGetter(obj.getClass(), lhs); - if(m == null) { - throw new NullPointerException("No getter found for field: " + lhs + " within " + obj.getClass()); - } - Log.d(TAG, "Invoking " + m.getName() + " on instance of " + obj.getClass().getName()); - Object o = m.invoke(obj); - // delve into o - return getObjectContaining(o, rhs); - //} catch (NoSuchMethodException e) { - // TODO: log a warning - // return null; - //} - } else { - // found it! - return obj; - } - } - - @SuppressWarnings("unchecked") - private static Object[] inflateParams(Context ctx, Class[] params, String[] vals) throws NoSuchMethodException, - InvocationTargetException, IllegalAccessException { - Object[] out = new Object[params.length]; - int i = 0; - for (Class param : params) { - if (Enum.class.isAssignableFrom(param)) { - out[i] = param.getMethod("valueOf", String.class).invoke(null, vals[i].toUpperCase()); - } else if (param.equals(Float.TYPE)) { - out[i] = parseFloatAttr(ctx, vals[i]); - } else if (param.equals(Integer.TYPE)) { - out[i] = parseIntAttr(ctx, vals[i]); - } else if (param.equals(Boolean.TYPE)) { - out[i] = Boolean.valueOf(vals[i]); - } else if (param.equals(String.class)) { - out[i] = parseStringAttr(ctx, vals[i]); - } else { - throw new IllegalArgumentException( - "Error inflating XML: Setter requires param of unsupported type: " + param); - } - i++; - } - return out; - } - - /** - * - * @param ctx - * @param obj - * @param xmlFileId ID of the XML config file within /res/xml - */ - public static void configure(Context ctx, Object obj, int xmlFileId) { - XmlResourceParser xrp = ctx.getResources().getXml(xmlFileId); - try { - HashMap params = new HashMap(); - while (xrp.getEventType() != XmlResourceParser.END_DOCUMENT) { - xrp.next(); - String name = xrp.getName(); - if (xrp.getEventType() == XmlResourceParser.START_TAG) { - if (name.equalsIgnoreCase(CFG_ELEMENT_NAME)) - for (int i = 0; i < xrp.getAttributeCount(); i++) { - params.put(xrp.getAttributeName(i), xrp.getAttributeValue(i)); - } - break; - } - } - configure(ctx, obj, params); - } catch (XmlPullParserException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - xrp.close(); - } - } - - public static void configure(Context ctx, Object obj, HashMap params) { - for (String key : params.keySet()) { - try { - configure(ctx, obj, key, params.get(key)); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - Log.w(TAG, "Error inflating XML: Setter for field \"" + key + "\" does not exist. "); - e.printStackTrace(); - } - } - } - - /** - * Recursively descend into an object using key as the pathway and invoking the corresponding setter - * if one exists. - * - * @param key - * @param value - */ - protected static void configure(Context ctx, Object obj, String key, String value) - throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { - Object o = getObjectContaining(obj, key); - if (o != null) { - int idx = key.lastIndexOf("."); - String fieldId = idx > 0 ? key.substring(idx + 1, key.length()) : key; - - Method m = getSetter(o.getClass(), fieldId); - Class[] paramTypes = m.getParameterTypes(); - // TODO: add support for generic type params - if (paramTypes.length >= 1) { - - // split on "|" - // TODO: add support for String args containing a | - String[] paramStrs = value.split("\\|"); - if (paramStrs.length == paramTypes.length) { - - Object[] oa = inflateParams(ctx, paramTypes, paramStrs); - Log.d(TAG, "Invoking " + m.getName() + " with arg(s) " + argArrToString(oa)); - m.invoke(o, oa); - } else { - throw new IllegalArgumentException("Error inflating XML: Unexpected number of argments passed to \"" - + m.getName() + "\". Expected: " + paramTypes.length + " Got: " + paramStrs.length); - } - } else { - // Obvious this is not a setter - throw new IllegalArgumentException("Error inflating XML: no setter method found for param \"" + - fieldId + "\"."); - } - } - } - - protected static String argArrToString(Object[] args) { - String out = ""; - for(Object obj : args) { - out += (obj == null ? (out += "[null] ") : - ("[" + obj.getClass() + ": " + obj + "] ")); - } - return out; - } -} - diff --git a/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java b/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java index b790dc81..edae4d30 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java @@ -16,8 +16,7 @@ package com.androidplot.util; -import android.graphics.Paint; -import android.graphics.Rect; +import android.graphics.*; public class FontUtils { @@ -64,4 +63,18 @@ public static Rect getStringDimensions(String text, Paint paint) { return size; } + /** + * Draws text vertically centered on the specified coordinates + * @param canvas + * @param paint + * @param text + * @param cx + * @param cy + */ + public static void drawTextVerticallyCentered(Canvas canvas, String text, float cx, float cy, Paint paint) { + Rect textBounds = new Rect(); + paint.getTextBounds(text, 0, text.length(), textBounds); + canvas.drawText(text, cx, cy - textBounds.exactCenterY(), paint); + } + } diff --git a/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java b/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java index 1686583b..7070e6dc 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java @@ -30,7 +30,6 @@ public class PixelUtils { private static DisplayMetrics metrics; - private static final float FLOAT_INT_AVG_NUDGE = 0.5f; /** * Recalculates scale value etc. Should be called when an application starts or @@ -48,26 +47,6 @@ public static PointF sub(PointF lhs, PointF rhs) { return new PointF(lhs.x - rhs.x, lhs.y - rhs.y); } - /** - * Converts a sub-pixel accurate RectF to - * a single pixel accurate rect. This is helpful - * for clipping operations which dont do a good job with - * subpixel vals. - * @param in - * @return - */ - public static RectF sink(RectF in) { - return nearestPixRect(in.left, in.top, in.right, in.bottom); - } - - public static RectF nearestPixRect(float left, float top, float right, float bottom) { - return new RectF( - (int) (left + FLOAT_INT_AVG_NUDGE), - (int) (top + FLOAT_INT_AVG_NUDGE), - (int) (right + FLOAT_INT_AVG_NUDGE), - (int) (bottom + FLOAT_INT_AVG_NUDGE)); - } - /** * Converts a dp value to pixels. * @param dp @@ -90,29 +69,6 @@ public static float spToPix(float sp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics); } - - /** - * - * @param fraction A float value between 0 and 1. - * @return Number of pixels fraction represents on the current device's display. - */ - public static float fractionToPixH(float fraction) { - checkMetrics(); - return metrics.heightPixels * fraction; - - } - - /** - * - * @param fraction A float value between 0 and 1. - * @return Number of pixels fraction represents on the current device's display. - */ - public static float fractionToPixW(float fraction) { - checkMetrics(); - return metrics.widthPixels * fraction; - } - - /** * * CODE BELOW IS ADAPTED IN PART FROM MINDRIOT'S SAMPLE CODE HERE: diff --git a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java index 2849970b..95537723 100644 --- a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java +++ b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java @@ -16,23 +16,21 @@ package com.androidplot.util; -import com.androidplot.Bounds; -import com.androidplot.xy.XYBounds; -import com.androidplot.xy.XYConstraints; -import com.androidplot.xy.XYSeries; +import com.androidplot.*; +import com.androidplot.xy.*; -import java.util.List; +import java.util.*; /** * Utilities for dealing with Series data. */ public class SeriesUtils { - public static XYBounds minMax(List seriesList) { + public static RectRegion minMax(List seriesList) { return minMax(null, seriesList); } - public static XYBounds minMax(XYSeries... seriesList) { + public static RectRegion minMax(XYSeries... seriesList) { return minMax(null, seriesList); } @@ -42,7 +40,7 @@ public static XYBounds minMax(XYSeries... seriesList) { * @return * @since 0.9.7 */ - public static XYBounds minMax(XYConstraints constraints, List seriesList) { + public static RectRegion minMax(XYConstraints constraints, List seriesList) { // TODO: this is inefficient...clean it up! return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()])); } @@ -53,9 +51,9 @@ public static XYBounds minMax(XYConstraints constraints, List seriesLi * @return * @since 0.9.7 */ - public static XYBounds minMax(XYConstraints constraints, XYSeries... seriesArray) { + public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArray) { - final XYBounds bounds = new XYBounds(); + final RectRegion bounds = new RectRegion(); // make sure there is series data to iterate over: if (seriesArray != null && seriesArray.length > 0) { @@ -69,27 +67,8 @@ public static XYBounds minMax(XYConstraints constraints, XYSeries... seriesArray // if constraints have been set, make sure this xy coordinate exists within them: if (constraints == null || constraints.contains(xi, yi)) { - if (xi != null) { - if (bounds.getMinX() == null || - xi.doubleValue() < bounds.getMinX().doubleValue()) { - bounds.setMinX(xi); - } - if (bounds.getMaxX() == null || - xi.doubleValue() > bounds.getMaxX().doubleValue()) { - bounds.setMaxX(xi); - } - } - if (yi != null) { - - if (bounds.getMinY() == null || - yi.doubleValue() < bounds.getMinY().doubleValue()) { - bounds.setMinY(yi); - } - if (bounds.getMaxY() == null || - yi.doubleValue() > bounds.getMaxY().doubleValue()) { - bounds.setMaxY(yi); - } - } + minMax(bounds.getxRegion(), xi); + minMax(bounds.getyRegion(), yi); } } } @@ -98,27 +77,68 @@ public static XYBounds minMax(XYConstraints constraints, XYSeries... seriesArray return bounds; } + /** + * + * @param bounds Starting minMax values to work from; only lists values that are greater than or less + * than those in bounds will be be used. + * @param lists lists to be evaluated for min/max values. + * @return the original bounds instance passed in + */ + public static Region minMax(Region bounds, List... lists) { + for (final List list : lists) { + for (final Number i : list) { + minMax(bounds, i); + } + } + return bounds; + } + + /** + * Compares a number against the current min/max values in a region, updating the region + * with the new value if appropriate. + * @param bounds + * @param number + * @return + */ + public static Region minMax(Region bounds, Number number) { + if (number != null) { + final double di = number.doubleValue(); + if (bounds.getMin() == null || di < bounds.getMin().doubleValue()) { + bounds.setMin(number); + } + if (bounds.getMax() == null || di > bounds.getMax().doubleValue()) { + bounds.setMax(number); + } + } + return bounds; + } + /** * @param lists * @return * @since 0.9.7 */ - public static Bounds minMax(List... lists) { - Number min = null; - Number max = null; - for (List list : lists) { - for (Number i : list) { - if (i != null) { - double di = i.doubleValue(); - if (min == null || di < min.doubleValue()) { - min = i; - } - if (max == null || di > max.doubleValue()) { - max = i; - } - } - } + public static Region minMax(List... lists) { + return minMax(new Region(), lists); + } + + public static void main(String[] args) { + + // seed the list: + ArrayList values = new ArrayList<>(); + for (int i = 0; i < 1000000; i++) { + values.add(Math.random() * 100); + } + final int numIterations = 20; + long sumTime = 0; + for(int j = 0; j < numIterations; j++) { + final long startTime = System.currentTimeMillis(); + Region bounds = minMax(values); + final long thisIteration = System.currentTimeMillis() - startTime; + System.out.println("thisIteration took: " + thisIteration + "ms"); + sumTime += thisIteration; } - return new Bounds(min, max); + + System.out.println("Benchmark avg:" + (sumTime / numIterations) + "ms."); } } diff --git a/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java b/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java deleted file mode 100644 index bd910a76..00000000 --- a/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot.util; - -import android.graphics.PointF; -import android.graphics.RectF; - -/** - * Utility methods for converting pixel coordinates into real values and vice versa. - */ -public class ValPixConverter { - private static final int ZERO = 0; - - - public static double valToPix(double val, double min, double max, double lengthPix, boolean flip) { - if(lengthPix <= ZERO) { - throw new IllegalArgumentException("Length in pixels must be greater than 0."); - } - double range = range(min, max); - double scale = lengthPix / range; - double raw = val - min; - double pix = raw * scale; - - if(flip) { - pix = (lengthPix - pix); - } - return pix; - } - - public static double range(double min, double max) { - return (max-min); - } - - - public static double valPerPix(double min, double max, double lengthPix) { - double valRange = range(min, max); - return valRange / lengthPix; - } - - /** - * Convert a value in pixels to the type passed into min/max - * @param pix - * @param min - * @param max - * @param lengthPix - * @param flip True if the axis should be reversed before calculated. This is the case - * with the y axis for screen coords. - * @return - */ - public static double pixToVal(double pix, double min, double max, double lengthPix, boolean flip) { - if(pix < ZERO) { - throw new IllegalArgumentException("pixel values cannot be negative."); - } - - if(lengthPix <= ZERO) { - throw new IllegalArgumentException("Length in pixels must be greater than 0."); - } - double pMult = pix; - if(flip) { - pMult = lengthPix - pix; - } - double range = range(min, max); - return ((range / lengthPix) * pMult) + min; - } - - /** - * Converts a real value into a pixel value. - * @param x Real d (domain) component of the point to convert. - * @param y Real y (range) component of the point to convert. - * @param plotArea - * @param minX Minimum visible real value on the d (domain) axis. - * @param maxX Maximum visible real value on the y (domain) axis. - * @param minY Minimum visible real value on the y (range) axis. - * @param maxY Maximum visible real value on the y (range axis. - * @return - */ - public static PointF valToPix(Number x, Number y, RectF plotArea, Number minX, Number maxX, Number minY, Number maxY) { - float pixX = (float) ValPixConverter.valToPix(x.doubleValue(), minX.doubleValue(), - maxX.doubleValue(), plotArea.width(), false) + (plotArea.left); - float pixY = (float) ValPixConverter.valToPix(y.doubleValue(), minY.doubleValue(), - maxY.doubleValue(), plotArea.height(), true) + plotArea.top; - return new PointF(pixX, pixY); - } - -} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java index 64c560e9..27d3e0f5 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java @@ -16,12 +16,11 @@ package com.androidplot.xy; +import android.content.*; import android.graphics.*; import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesRenderer; -import com.androidplot.util.ValPixConverter; -import java.util.ArrayList; /** * This is an experimental (but stable) implementation of an {@link XYSeriesRenderer} that provides instrumentation @@ -45,20 +44,13 @@ public AdvancedLineAndPointRenderer(XYPlot plot) { protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) throws PlotRenderException { PointF thisPoint; PointF lastPoint = null; - ArrayList points = new ArrayList<>(series.size()); for (int i = 0; i < series.size(); i++) { Number y = series.getY(i); Number x = series.getX(i); if (y != null && x != null) { - thisPoint = ValPixConverter.valToPix( - x, y, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - points.add(thisPoint); + thisPoint = getPlot().getBounds() + .transformScreen(x, y, plotArea); } else { thisPoint = null; } @@ -91,14 +83,21 @@ public void setLatestIndex(int latestIndex) { */ public static class Formatter extends XYSeriesFormatter { + private static final int DEFAULT_STROKE_WIDTH = 3; + private Paint linePaint; - { + public Formatter() { linePaint = new Paint(); - linePaint.setStrokeWidth(3); + linePaint.setStrokeWidth(DEFAULT_STROKE_WIDTH); linePaint.setColor(Color.RED); } + public Formatter(Context context, int xmlConfigId) { + this(); + configure(context, xmlConfigId); + } + @Override public Class getRendererClass() { return AdvancedLineAndPointRenderer.class; @@ -125,7 +124,6 @@ public Paint getLinePaint(int thisIndex, int latestIndex, int seriesSize) { return getLinePaint(); } - public void setLinePaint(Paint linePaint) { this.linePaint = linePaint; } 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 fa072c04..e005dda7 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java @@ -15,6 +15,7 @@ */ package com.androidplot.xy; +import android.content.*; import android.graphics.Paint; import com.androidplot.ui.SeriesRenderer; @@ -39,9 +40,11 @@ public void setBorderPaint(Paint borderPaint) { private Paint fillPaint; private Paint borderPaint; - { + /** + * Should only be used in conjunction with calls to configure()... + */ + public BarFormatter() { fillPaint = new Paint(); - //fillPaint.setColor(Color.RED); fillPaint.setStyle(Paint.Style.FILL); fillPaint.setAlpha(100); borderPaint = new Paint(); @@ -49,17 +52,17 @@ public void setBorderPaint(Paint borderPaint) { borderPaint.setAlpha(100); } - /** - * Should only be used in conjunction with calls to configure()... - */ - public BarFormatter() { - } - public BarFormatter(int fillColor, int borderColor) { + this(); fillPaint.setColor(fillColor); borderPaint.setColor(borderColor); } + public BarFormatter(Context context, int xmlCfgId) { + this(); + configure(context, xmlCfgId); + } + @Override public Class getRendererClass() { return BarRenderer.class; 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 857b5df7..43c3899e 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java @@ -28,26 +28,31 @@ import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesAndFormatter; -import com.androidplot.util.ValPixConverter; /** * Renders the points in an XYSeries as bars. */ public class BarRenderer extends GroupRenderer { - private BarRenderStyle renderStyle = BarRenderStyle.OVERLAID; // default Render Style - private BarWidthStyle widthStyle = BarWidthStyle.FIXED_WIDTH; // default Width Style + 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(); - public enum BarRenderStyle { + /** + * How bars should be laid out when in a group of 2 or more series. + */ + public enum Style { OVERLAID, // bars are overlaid in descending y-val order (largest val in back) STACKED, // bars are drawn stacked vertically on top of each other SIDE_BY_SIDE // bars are drawn horizontally next to each-other } - public enum BarWidthStyle { + /** + * 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 } @@ -72,17 +77,25 @@ public void setBarGap(float barGap) { this.barGap = barGap; } - public void setBarRenderStyle(BarRenderStyle renderStyle) { - this.renderStyle = renderStyle; + public void setStyle(Style renderStyle) { + this.style = renderStyle; } + + public Style getStyle() { + return this.style; + } - public void setBarWidthStyle(BarWidthStyle widthStyle) { - this.widthStyle = widthStyle; + public void setBarWidthMode(BarWidthMode widthStyle) { + this.barWidthMode = widthStyle; } + + public BarWidthMode getBarWidthMode() { + return this.barWidthMode; + } - public void setBarWidthStyle(BarWidthStyle style, float value) { - setBarWidthStyle(style); - switch (style) { + public void setBarWidth(BarWidthMode mode, float value) { + setBarWidthMode(mode); + switch (mode) { case FIXED_WIDTH: setBarWidth(value); break; @@ -103,7 +116,9 @@ public void setBarComparator(Comparator barComparator) { @Override public void doDrawLegendIcon(Canvas canvas, RectF rect, BarFormatter formatter) { - canvas.drawRect(rect, formatter.getFillPaint()); + if(formatter.hasFillPaint()) { + canvas.drawRect(rect, formatter.getFillPaint()); + } canvas.drawRect(rect, formatter.getBorderPaint()); } @@ -183,7 +198,7 @@ public void onRender(Canvas canvas, RectF plotArea, List= 2) { + if (formatter.hasFillPaint() && bar.barGroup.width >= 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 (bar.barGroup.width >= 2) { + 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 && pointLabeler != null) { + 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()); } } @@ -259,24 +273,25 @@ public void onRender(Canvas canvas, RectF plotArea, List= 2) { + if (formatter.hasFillPaint() && bar.barGroup.width >= 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 (bar.barGroup.width >= 2) { + 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 && pointLabeler != null) { + 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; @@ -287,19 +302,20 @@ public void onRender(Canvas canvas, RectF plotArea, List= 2) { + 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 && pointLabeler != null) { + 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()); @@ -319,7 +335,7 @@ public class Bar { public final int seriesIndex; public final double yVal, xVal; public final int intX, intY; - public final float pixX, pixY; + public final double pixX, pixY; protected BarGroup barGroup; public Bar(XYSeries series, FormatterType formatter, int seriesIndex, RectF plotArea) { @@ -328,15 +344,15 @@ public Bar(XYSeries series, FormatterType formatter, int seriesIndex, RectF plot this.seriesIndex = seriesIndex; this.xVal = series.getX(seriesIndex).doubleValue(); - this.pixX = (float) ValPixConverter.valToPix(xVal, getPlot().getCalculatedMinX().doubleValue(), - getPlot().getCalculatedMaxX().doubleValue(), plotArea.width(), false) + (plotArea.left); - this.intX = (int) pixX; + 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 = (float) ValPixConverter.valToPix(yVal, getPlot().getCalculatedMinY().doubleValue(), - getPlot().getCalculatedMaxY().doubleValue(), plotArea.height(), true) + plotArea.top; - this.intY = (int) pixY; + 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; @@ -375,7 +391,7 @@ public class BarComparator implements Comparator{ @Override public int compare(Bar bar1, Bar bar2) { - switch (renderStyle) { + switch (style) { case OVERLAID: return Integer.valueOf(bar1.intY).compareTo(bar2.intY); case SIDE_BY_SIDE: diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java new file mode 100644 index 00000000..4c4573f9 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016 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.content.*; +import android.graphics.*; + +import com.androidplot.ui.*; +import com.androidplot.util.*; + +/** + * Format for drawing a value using {@link BubbleRenderer}. + * @since 1.2.2 + */ +public class BubbleFormatter extends XYSeriesFormatter { + + private static final float DEFAULT_STROKE_PIX = 1; + + private Paint strokePaint; + private Paint fillPaint; + + { + strokePaint = new Paint(); + strokePaint.setAntiAlias(true); + strokePaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_STROKE_PIX)); + strokePaint.setStyle(Paint.Style.STROKE); + + fillPaint = new Paint(); + fillPaint.setAntiAlias(true); + + // default point labeler should draw z for bubbles: + setPointLabeler(new PointLabeler() { + @Override + public String getLabel(BubbleSeries series, int index) { + return series.getZ(index) + ""; + } + }); + } + + public BubbleFormatter(Context context, int xmlCfgId) { + configure(context, xmlCfgId); + } + + public BubbleFormatter(int fillColor, int strokeColor) { + fillPaint.setColor(fillColor); + strokePaint.setColor(strokeColor); + } + + @Override + public Class getRendererClass() { + return BubbleRenderer.class; + } + + @Override + public SeriesRenderer getRendererInstance(XYPlot plot) { + return new BubbleRenderer(plot); + } + + public Paint getStrokePaint() { + return strokePaint; + } + + public void setStrokePaint(Paint strokePaint) { + this.strokePaint = strokePaint; + } + + public Paint getFillPaint() { + return fillPaint; + } + + public void setFillPaint(Paint fillPaint) { + this.fillPaint = fillPaint; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java new file mode 100644 index 00000000..9811ffe3 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java @@ -0,0 +1,152 @@ +package com.androidplot.xy; + +import android.graphics.*; + +import com.androidplot.Region; +import com.androidplot.exception.*; +import com.androidplot.ui.*; +import com.androidplot.util.*; + +/** + * Renders three dimensional data onto an {@link XYPlot} as bubbles; the x/y values define the position + * of the bubble and z is uses as a scaling value for the bubble's radius. + * @since 1.2.2 + */ +public class BubbleRenderer extends XYSeriesRenderer { + + protected static final float MIN_BUBBLE_RADIUS_DEFAULT_DP = 9; + protected static final float MAX_BUBBLE_RADIUS_DEFAULT_DP = 25; + + private Region bubbleBounds; + + private BubbleScaleMode bubbleScaleMode = BubbleScaleMode.SQUARE_ROOT; + + public enum BubbleScaleMode { + + /** + * Bubble radius is scaled directly by {@link BubbleSeries} z-vals + */ + LINEAR, + + /** + * Bubble radius is scaled by the square root of {@link BubbleSeries} z-vals. + * This is the default scaling used. + */ + SQUARE_ROOT + } + + public BubbleRenderer(XYPlot plot) { + super(plot); + + bubbleBounds = new Region( + PixelUtils.dpToPix(MIN_BUBBLE_RADIUS_DEFAULT_DP), + PixelUtils.dpToPix(MAX_BUBBLE_RADIUS_DEFAULT_DP)); + } + + @Override + protected void onRender(Canvas canvas, RectF plotArea, BubbleSeries series, + FormatterType formatter, RenderStack stack) throws PlotRenderException { + + Region magnitudeBounds = calculateBounds(); + for(int i = 0; i < series.size(); i++) { + + // only render non-null values greater than zero: + if(series.getY(i) != null && series.getZ(i).doubleValue() > 0) { + + final PointF centerPoint = getPlot().getBounds(). + transform(series.getX(i), series.getY(i), plotArea, false, true); + + // calculate bubble radius: + float bubbleRadius = magnitudeBounds. + transform(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT ? + Math.sqrt(series.getZ(i).doubleValue()) : + series.getZ(i).doubleValue(), bubbleBounds).floatValue(); + drawBubble(canvas, formatter, series, i, centerPoint, bubbleRadius); + } + } + } + + /** + * Render a bubble onto the canvas + * @param canvas + * @param formatter + * @param series + * @param index + * @param centerPoint the x/y coords of the center of the bubble + * @param radius size of the bubble + */ + protected void drawBubble(Canvas canvas, FormatterType formatter, BubbleSeries series, + int index, PointF centerPoint, float radius) { + canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getFillPaint()); + canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getStrokePaint()); + if(series != null && formatter.hasPointLabelFormatter() && formatter.getPointLabeler() != null) { + FontUtils.drawTextVerticallyCentered( + canvas, + formatter.getPointLabeler().getLabel(series, index), + centerPoint.x, + centerPoint.y, + formatter.getPointLabelFormatter().getTextPaint()); + } + } + + @Override + protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) { + drawBubble(canvas, formatter, null, 0, + new PointF(rect.centerX(), rect.centerY()), (rect.width()/2.5f)); + } + + public float getMinBubbleRadius() { + return bubbleBounds.getMin().floatValue(); + } + + public void setMinBubbleRadius(float minBubbleRadius) { + bubbleBounds.setMin(minBubbleRadius); + } + + public float getMaxBubbleRadius() { + return bubbleBounds.getMax().floatValue(); + } + + public void setMaxBubbleRadius(float maxBubbleRadius) { + bubbleBounds.setMax(maxBubbleRadius); + } + + public BubbleScaleMode getBubbleScaleMode() { + return bubbleScaleMode; + } + + public void setBubbleScaleMode(BubbleScaleMode bubbleScaleMode) { + this.bubbleScaleMode = bubbleScaleMode; + } + + protected Region calculateBounds() { + Region bounds = new Region(); + for(SeriesAndFormatter f : getSeriesAndFormatterList()) { + SeriesUtils.minMax(bounds, f.getSeries().getZVals()); + } + + if(bounds.getMax() != null && bounds.getMax().doubleValue() > 0) { + if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) { + // scale for easier visual interpretation. see: + // https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly + bounds.setMax(Math.sqrt(bounds.getMax().doubleValue())); + } + } else { + // no non-null, greater than zero vals so bounds are undefined + return null; + } + + if(bounds.getMin().doubleValue() > 0) { + + if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) { + // scale for easier visual interpretation. see: + // https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly + bounds.setMin(Math.sqrt(bounds.getMin().doubleValue())); + } + } else { + // if the smallest value is negative, use zero instead since those vals arent visible: + bounds.setMax(0); + } + return bounds; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java new file mode 100644 index 00000000..39982f7a --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java @@ -0,0 +1,62 @@ +package com.androidplot.xy; + +import java.util.*; + +/** + * Created by halfhp on 9/17/16. + */ +public class BubbleSeries implements XYSeries { + + private List xVals; + private List yVals; + private List zVals; + private String title; + + public BubbleSeries(List yVals, List zVals, String title) { + this.yVals = yVals; + this.zVals = zVals; + this.title = title; + // populate x with iVals: + this.xVals = new ArrayList<>(zVals.size()); + for(int i = 0; i < zVals.size(); i++) { + this.xVals.add(i); + } + } + + public BubbleSeries(List xVals, List yVals, List zVals, String title) { + this.xVals = xVals; + this.yVals = yVals; + this.zVals = zVals; + this.title = title; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public int size() { + return xVals.size(); + } + + @Override + public Number getX(int index) { + return xVals.get(index); + } + + @Override + public Number getY(int index) { + return yVals.get(index); + } + + public Number getZ(int index) { + return zVals.get(index); + } + + public List getZVals() { + return zVals; + } + + +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java index b3af9d79..873b7cbc 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java @@ -16,6 +16,7 @@ package com.androidplot.xy; +import android.content.*; import android.graphics.Color; import android.graphics.Paint; import com.androidplot.ui.SeriesRenderer; @@ -45,8 +46,8 @@ public class CandlestickFormatter extends XYSeriesFormatter { private BodyStyle bodyStyle; public enum BodyStyle { - Square, - Triangle + SQUARE, + TRIANGULAR } protected static Paint getDefaultFillPaint(int color) { @@ -65,6 +66,11 @@ protected static Paint getDefaultStrokePaint(int color) { return p; } + public CandlestickFormatter(Context context, int xmlCfgId) { + this(); + configure(context, xmlCfgId); + } + public CandlestickFormatter() { this(getDefaultStrokePaint(Color.YELLOW), getDefaultFillPaint(Color.GREEN), @@ -73,7 +79,7 @@ public CandlestickFormatter() { getDefaultStrokePaint(Color.RED), getDefaultStrokePaint(Color.YELLOW), getDefaultStrokePaint(Color.YELLOW), - BodyStyle.Square); + BodyStyle.SQUARE); } public CandlestickFormatter(Paint wickPaint, Paint risingBodyFillPaint, Paint fallingBodyFillPaint, diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java index 49f559ef..1505d0d4 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java @@ -19,7 +19,6 @@ import android.graphics.*; import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesAndFormatter; -import com.androidplot.util.ValPixConverter; import java.util.List; @@ -39,10 +38,10 @@ */ public class CandlestickRenderer extends GroupRenderer { - private static final int HIGH_INDEX = 0; - private static final int LOW_INDEX = 1; - private static final int OPEN_INDEX = 2; - private static final int CLOSE_INDEX = 3; + protected static final int HIGH_INDEX = 0; + protected static final int LOW_INDEX = 1; + protected static final int OPEN_INDEX = 2; + protected static final int CLOSE_INDEX = 3; public CandlestickRenderer(XYPlot plot) { super(plot); @@ -53,57 +52,50 @@ public CandlestickRenderer(XYPlot plot) { public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) { + final FormatterType formatter = sfList.get(0).getFormatter(); for(int i = 0; i < seriesSize; i++) { - // x-val for all series should be identical so just grab x from the first series: - Number x = sfList.get(HIGH_INDEX).getSeries().getX(i); + final XYSeries highSeries = sfList.get(HIGH_INDEX).getSeries(); + final XYSeries lowSeries = sfList.get(LOW_INDEX).getSeries(); + final XYSeries openSeries = sfList.get(OPEN_INDEX).getSeries(); + final XYSeries closeSeries = sfList.get(CLOSE_INDEX).getSeries(); - Number high = sfList.get(HIGH_INDEX).getSeries().getY(i); - Number low = sfList.get(LOW_INDEX).getSeries().getY(i); - Number open = sfList.get(OPEN_INDEX).getSeries().getY(i); - Number close = sfList.get(CLOSE_INDEX).getSeries().getY(i); - drawValue(canvas, plotArea, sfList.get(0).getFormatter(), x, high, low, open, close); + // x-val for all series should be identical so just grab x from the first series: + Number x = highSeries.getX(i); + + Number high = highSeries.getY(i); + Number low = lowSeries.getY(i); + Number open = openSeries.getY(i); + Number close = closeSeries.getY(i); + + // draw the candlestick: + final PointF highPix = getPlot().getBounds().transformScreen(x, high, plotArea); + final PointF lowPix = getPlot().getBounds().transformScreen(x, low, plotArea); + final PointF openPix = getPlot().getBounds().transformScreen(x, open, plotArea); + final PointF closePix = getPlot().getBounds().transformScreen(x, close, plotArea); + + drawWick(canvas, highPix, lowPix, formatter); + drawBody(canvas, openPix, closePix, formatter); + drawUpperCap(canvas, highPix, formatter); + drawLowerCap(canvas, lowPix, formatter); + + // draw labels, if any: + final PointLabelFormatter plf = formatter.hasPointLabelFormatter() + ? formatter.getPointLabelFormatter() : null; + final PointLabeler pointLabeler = formatter.getPointLabeler(); + if(plf != null && pointLabeler != null) { + drawTextLabel(canvas, highPix, pointLabeler.getLabel(highSeries, i), plf); + drawTextLabel(canvas, lowPix, pointLabeler.getLabel(lowSeries, i), plf); + drawTextLabel(canvas, openPix, pointLabeler.getLabel(openSeries, i), plf); + drawTextLabel(canvas, closePix, pointLabeler.getLabel(closeSeries, i), plf); + } } } - protected void drawValue(Canvas canvas, RectF plotArea, FormatterType formatter, - Number x, Number high, Number low, Number open, Number close) { - final PointF highPix = ValPixConverter.valToPix( - x, high, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - - final PointF lowPix = ValPixConverter.valToPix( - x, low, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - - final PointF openPix = ValPixConverter.valToPix( - x, open, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - - final PointF closePix = ValPixConverter.valToPix( - x, close, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); - - drawWick(canvas, highPix, lowPix, formatter); - drawBody(canvas, openPix, closePix, formatter); - drawUpperCap(canvas, highPix, formatter); - drawLowerCap(canvas, lowPix, formatter); + protected void drawTextLabel(Canvas canvas, PointF coords, String text, PointLabelFormatter plf) { + if(text != null) { + canvas.drawText(text, coords.x + plf.hOffset, coords.y + plf.vOffset, plf.getTextPaint()); + } } protected void drawWick(Canvas canvas, PointF min, PointF max, FormatterType formatter) { @@ -121,11 +113,11 @@ protected void drawBody(Canvas canvas, PointF open, PointF close, FormatterType formatter.getRisingBodyStrokePaint() : formatter.getFallingBodyStrokePaint(); switch(formatter.getBodyStyle()) { - case Square: + case SQUARE: canvas.drawRect(rect, bodyFillPaint); canvas.drawRect(rect, bodyStrokePaint); break; - case Triangle: + case TRIANGULAR: drawTriangle(canvas, rect, bodyFillPaint, bodyStrokePaint); } } 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 29dcd897..d9d502e8 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/FastLineAndPointRenderer.java @@ -20,7 +20,6 @@ import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; import com.androidplot.ui.SeriesRenderer; -import com.androidplot.util.ValPixConverter; import java.util.*; @@ -51,10 +50,10 @@ 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(); +// 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; @@ -70,11 +69,7 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte isLastPointNull = false; } - thisPoint = ValPixConverter.valToPix( - x, y, - plotArea, - minX, maxX, - minY, maxY); + thisPoint = getPlot().getBounds().transformScreen(x, y, plotArea); points[j] = thisPoint.x; points[j+1] = thisPoint.y; segmentLen+=2; @@ -95,11 +90,6 @@ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatte final int len = segmentLengths.get(i); final int offset = segmentOffsets.get(i); drawSegment(canvas, points, offset, len, formatter); - -// if(formatter.vertexPaint != null) { -// // draw vertices: -// canvas.drawPoints(points, offset, len, formatter.vertexPaint); -// } } } } @@ -127,7 +117,7 @@ protected void drawSegment(Canvas canvas, float[] points, int offset, int len, F @Override protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) { - if(formatter.getLinePaint() != null) { + if(formatter.hasLinePaint()) { canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint()); } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java index 118af5bf..711b8d46 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/GroupRenderer.java @@ -26,8 +26,9 @@ import java.util.List; /** - * Renders data to an XYPlot that potentially contains more than a single yVal per index. - * Examples of such data are bar plot groups, candlestick charts, etc. + * Renders data to an XYPlot that potentially contains more than a single yVal per index, or in other + * words data with potentially more than two dimensions. + * Examples of such data are bar plot groups and candlestick charts. * @since 0.9.7 */ public abstract class GroupRenderer> diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java index f03e3e26..1995502f 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointFormatter.java @@ -16,6 +16,7 @@ package com.androidplot.xy; +import android.content.*; import android.graphics.Color; import android.graphics.Paint; import com.androidplot.ui.SeriesRenderer; @@ -30,14 +31,6 @@ public class LineAndPointFormatter extends XYSeriesFormatter private static final float DEFAULT_LINE_STROKE_WIDTH_DP = 1.5f; private static final float DEFAULT_VERTEX_STROKE_WIDTH_DP = 4.5f; - // default implementation prints point's yVal: - private PointLabeler pointLabeler = new PointLabeler() { - @Override - public String getLabel(XYSeries series, int index) { - return series.getY(index) + ""; - } - }; - public FillDirection getFillDirection() { return fillDirection; } @@ -56,10 +49,9 @@ public void setFillDirection(FillDirection fillDirection) { protected Paint vertexPaint; protected Paint fillPaint; protected InterpolationParams interpolationParams; - private PointLabelFormatter pointLabelFormatter; - { - initLinePaint(Color.BLACK); + public LineAndPointFormatter(Context context, int xmlCfgId) { + super(context, xmlCfgId); } /** @@ -69,12 +61,13 @@ public LineAndPointFormatter() { this(Color.RED, Color.GREEN, Color.BLUE, null); } - public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, PointLabelFormatter plf) { + public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, + PointLabelFormatter plf) { this(lineColor, vertexColor, fillColor, plf, FillDirection.BOTTOM); } - public LineAndPointFormatter(Integer lineColor, Integer vertexColor, - Integer fillColor, PointLabelFormatter plf, FillDirection fillDir) { + public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, + PointLabelFormatter plf, FillDirection fillDir) { initLinePaint(lineColor); initVertexPaint(vertexColor); initFillPaint(fillColor); @@ -127,20 +120,23 @@ protected void initFillPaint(Integer fillColor) { } /** - * Enables the shadow layer on linePaint and shadowPaint by calling - * setShadowLayer() with preset values. + * + * @return True if linePaint has been set, false otherwise. */ - public void enableShadows() { - linePaint.setShadowLayer(1, 3, 3, Color.BLACK); - vertexPaint.setShadowLayer(1, 3, 3, Color.BLACK); - } - - public void disableShadows() { - linePaint.setShadowLayer(0, 0, 0, Color.BLACK); - vertexPaint.setShadowLayer(0, 0, 0, Color.BLACK); + public boolean hasLinePaint() { + return linePaint != null; } + /** + * Get the {@link Paint} used to draw lines. Will instantiate and a new default instance + * if it is currently null. To check whether or not line paint has been set, use + * {@link #hasLinePaint()}. + * @return + */ public Paint getLinePaint() { + if(linePaint == null) { + initLinePaint(Color.TRANSPARENT); + } return linePaint; } @@ -148,7 +144,24 @@ public void setLinePaint(Paint linePaint) { this.linePaint = linePaint; } + /** + * + * @return True if vertexPaint has been set, false otherwise. + */ + public boolean hasVertexPaint() { + return vertexPaint != null; + } + + /** + * Get the {@link Paint} used to draw vertices (points). Will instantiate and a new default instance + * if it is currently null. To check whether or not vertex paint has been set, use + * {@link #hasVertexPaint()}. + * @return + */ public Paint getVertexPaint() { + if(vertexPaint == null) { + initVertexPaint(Color.TRANSPARENT); + } return vertexPaint; } @@ -156,7 +169,23 @@ public void setVertexPaint(Paint vertexPaint) { this.vertexPaint = vertexPaint; } + /** + * + * @return True if fillPaint has been set, false otherwise. + */ + public boolean hasFillPaint() { + return fillPaint != null; + } + /** + * Get the {@link Paint} used to fill series areas. Will instantiate and a new default instance + * if it is currently null. To check whether or not fill paint has been set, use + * {@link #hasFillPaint()}. + * @return + */ public Paint getFillPaint() { + if(fillPaint == null) { + initFillPaint(Color.TRANSPARENT); + } return fillPaint; } @@ -164,22 +193,6 @@ public void setFillPaint(Paint fillPaint) { this.fillPaint = fillPaint; } - public PointLabelFormatter getPointLabelFormatter() { - return pointLabelFormatter; - } - - public void setPointLabelFormatter(PointLabelFormatter pointLabelFormatter) { - this.pointLabelFormatter = pointLabelFormatter; - } - - public PointLabeler getPointLabeler() { - return pointLabeler; - } - - public void setPointLabeler(PointLabeler pointLabeler) { - this.pointLabeler = pointLabeler; - } - public InterpolationParams getInterpolationParams() { return interpolationParams; } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java index bfe019fb..89547fbf 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/LineAndPointRenderer.java @@ -19,7 +19,7 @@ import android.graphics.*; import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; -import com.androidplot.util.ValPixConverter; + import java.util.ArrayList; import java.util.List; @@ -50,11 +50,11 @@ public void doDrawLegendIcon(Canvas canvas, RectF rect, LineAndPointFormatter fo if(formatter.getFillPaint() != null) { canvas.drawRect(rect, formatter.getFillPaint()); } - if(formatter.getLinePaint() != null) { + if(formatter.hasLinePaint()) { canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint()); } - if(formatter.getVertexPaint() != null) { + if(formatter.hasVertexPaint()) { canvas.drawPoint(centerX, centerY, formatter.getVertexPaint()); } } @@ -73,7 +73,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn PointF thisPoint; PointF lastPoint = null; PointF firstPoint = null; - Paint linePaint = formatter.getLinePaint(); + //Paint linePaint = formatter.getLinePaint(); Path path = null; ArrayList points = new ArrayList<>(series.size()); for (int i = 0; i < series.size(); i++) { @@ -81,20 +81,14 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn Number x = series.getX(i); if (y != null && x != null) { - thisPoint = ValPixConverter.valToPix( - x, y, - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); + thisPoint = getPlot().getBounds().transformScreen(x, y, plotArea); points.add(thisPoint); } else { thisPoint = null; } // don't need to do any of this if the line isnt going to be drawn: - if(linePaint != null && formatter.getInterpolationParams() == null) { + if(formatter.hasLinePaint() && formatter.getInterpolationParams() == null) { if (thisPoint != null) { // record the first point of the new Path @@ -120,7 +114,7 @@ protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAn } } } - if(linePaint != null) { + if(formatter.hasLinePaint()) { if(formatter.getInterpolationParams() != null) { List interpolatedPoints = getInterpolator( formatter.getInterpolationParams()).interpolate(series, @@ -158,32 +152,25 @@ protected Interpolator getInterpolator(InterpolationParams params) { } protected PointF convertPoint(XYCoords coord, RectF plotArea) { - return ValPixConverter.valToPix( - coord.x.doubleValue(), - coord.y.doubleValue(), - plotArea, - getPlot().getCalculatedMinX(), - getPlot().getCalculatedMaxX(), - getPlot().getCalculatedMinY(), - getPlot().getCalculatedMaxY()); + return getPlot().getBounds().transformScreen(coord, plotArea); } protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, List points, LineAndPointFormatter formatter) { - Paint vertexPaint = formatter.getVertexPaint(); - PointLabelFormatter plf = formatter.getPointLabelFormatter(); - if (vertexPaint != null || plf != null) { + //PointLabelFormatter plf = formatter.getPointLabelFormatter(); + if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) { int i = 0; for (PointF p : points) { PointLabeler pointLabeler = formatter.getPointLabeler(); // if vertexPaint is available, draw vertex: - if (vertexPaint != null) { + if (formatter.hasVertexPaint()) { canvas.drawPoint(p.x, p.y, formatter.getVertexPaint()); } // if textPaint and pointLabeler are available, draw point's text label: - if (plf != null && pointLabeler != null) { + if (formatter.hasPointLabelFormatter() && pointLabeler != null) { + final PointLabelFormatter plf = formatter.getPointLabelFormatter(); canvas.drawText(pointLabeler.getLabel(series, i), p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint()); } @@ -210,41 +197,37 @@ protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF first path.close(); break; case RANGE_ORIGIN: - float originPix = (float) ValPixConverter.valToPix( - getPlot().getRangeOrigin().doubleValue(), - getPlot().getCalculatedMinY().doubleValue(), - getPlot().getCalculatedMaxY().doubleValue(), - plotArea.height(), - true); - originPix += plotArea.top; - + float originPix = (float) getPlot().getBounds().getxRegion() + .transform(getPlot().getRangeOrigin() + .doubleValue(), plotArea.top, plotArea.bottom, true); path.lineTo(lastPoint.x, originPix); path.lineTo(firstPoint.x, originPix); path.close(); break; default: - throw new UnsupportedOperationException("Fill direction not yet implemented: " + formatter.getFillDirection()); + throw new UnsupportedOperationException( + "Fill direction not yet implemented: " + formatter.getFillDirection()); } if (formatter.getFillPaint() != null) { canvas.drawPath(path, formatter.getFillPaint()); } - // draw any visible regions on top of the base region: - double minX = getPlot().getCalculatedMinX().doubleValue(); - double maxX = getPlot().getCalculatedMaxX().doubleValue(); - double minY = getPlot().getCalculatedMinY().doubleValue(); - double maxY = getPlot().getCalculatedMaxY().doubleValue(); + final RectRegion bounds = getPlot().getBounds(); + final RectRegion plotRegion = new RectRegion(plotArea); // draw each region: - for (RectRegion r : RectRegion.regionsWithin(formatter.getRegions().elements(), minX, maxX, minY, maxY)) { - XYRegionFormatter f = formatter.getRegionFormatter(r); - RectF regionRect = r.getRectF(plotArea, minX, maxX, minY, maxY); - if (regionRect != null) { + for (RectRegion thisRegion : bounds.intersects(formatter.getRegions().elements())) { + XYRegionFormatter regionFormatter = formatter.getRegionFormatter(thisRegion); + RectRegion thisRegionTransformed = bounds + .transform(thisRegion, plotRegion, false, true); + thisRegionTransformed.clip(plotRegion); + RectF thisRegionRectF = thisRegionTransformed.asRectF(); + if (thisRegionRectF != null) { try { - canvas.save(Canvas.ALL_SAVE_FLAG); - canvas.clipPath(path); - canvas.drawRect(regionRect, f.getPaint()); + canvas.save(Canvas.ALL_SAVE_FLAG); + canvas.clipPath(path); + canvas.drawRect(thisRegionRectF, regionFormatter.getPaint()); } finally { canvas.restore(); } @@ -252,7 +235,7 @@ protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF first } // finally we draw the outline path on top of everything else: - if(formatter.getLinePaint() != null) { + if(formatter.hasLinePaint()) { canvas.drawPath(outlinePath, formatter.getLinePaint()); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java index c1fe5e98..e868676f 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/PanZoom.java @@ -109,10 +109,11 @@ protected void setDomainBoundaries(final Number lowerBoundary, final BoundaryMod if(mCalledBySelf) { mCalledBySelf = false; } else { + final RectRegion bounds = plot.getBounds(); minXLimit = lowerBoundaryMode == BoundaryMode.FIXED ? - lowerBoundary.floatValue() : plot.getCalculatedMinX().floatValue(); + lowerBoundary.floatValue() : bounds.getMinX().floatValue(); maxXLimit = upperBoundaryMode == BoundaryMode.FIXED ? - upperBoundary.floatValue() : plot.getCalculatedMaxX().floatValue(); + upperBoundary.floatValue() : bounds.getMaxX().floatValue(); lastMinX = minXLimit; lastMaxX = maxXLimit; } @@ -124,43 +125,24 @@ protected void setRangeBoundaries(final Number lowerBoundary, final BoundaryMode if(mCalledBySelf) { mCalledBySelf = false; } else { + final RectRegion bounds = plot.getBounds(); minYLimit = lowerBoundaryMode == BoundaryMode.FIXED ? - lowerBoundary.floatValue() : plot.getCalculatedMinY().floatValue(); + lowerBoundary.floatValue() : bounds.getMinY().floatValue(); maxYLimit = upperBoundaryMode == BoundaryMode.FIXED ? - upperBoundary.floatValue() : plot.getCalculatedMaxY().floatValue(); + upperBoundary.floatValue() : bounds.getMaxY().floatValue(); lastMinY = minYLimit; lastMaxY = maxYLimit; } } protected void setDomainBoundaries(final Number lowerBoundary, - final Number upperBoundary, final BoundaryMode mode) { - plot.setDomainBoundaries(lowerBoundary, upperBoundary, mode); - if(mCalledBySelf) { - mCalledBySelf = false; - } else { - minXLimit = mode == BoundaryMode.FIXED ? - lowerBoundary.floatValue() : plot.getCalculatedMinX().floatValue(); - maxXLimit = mode == BoundaryMode.FIXED ? - upperBoundary.floatValue() : plot.getCalculatedMaxX().floatValue(); - lastMinX = minXLimit; - lastMaxX = maxXLimit; - } +final Number upperBoundary, final BoundaryMode mode) { + plot.setDomainBoundaries(lowerBoundary, mode, upperBoundary, mode); } protected synchronized void setRangeBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) { - plot.setRangeBoundaries(lowerBoundary, upperBoundary, mode); - if(mCalledBySelf) { - mCalledBySelf = false; - } else { - minYLimit = mode == BoundaryMode.FIXED ? - lowerBoundary.floatValue() : plot.getCalculatedMinY().floatValue(); - maxYLimit = mode == BoundaryMode.FIXED ? - upperBoundary.floatValue() : plot.getCalculatedMaxY().floatValue(); - lastMinY = minYLimit; - lastMaxY = maxYLimit; - } + plot.setRangeBoundaries(lowerBoundary, mode, upperBoundary, mode); } @Override @@ -231,7 +213,7 @@ private RectF getDistance(final MotionEvent evt) { private float getMinXLimit() { if(minXLimit == Float.MAX_VALUE) { - minXLimit = plot.getCalculatedMinX().floatValue(); + minXLimit = plot.getBounds().getMinX().floatValue(); lastMinX = minXLimit; } return minXLimit; @@ -239,7 +221,7 @@ private float getMinXLimit() { protected float getMaxXLimit() { if(maxXLimit == Float.MAX_VALUE) { - maxXLimit = plot.getCalculatedMaxX().floatValue(); + maxXLimit = plot.getBounds().getMaxX().floatValue(); lastMaxX = maxXLimit; } return maxXLimit; @@ -247,7 +229,7 @@ protected float getMaxXLimit() { protected float getMinYLimit() { if(minYLimit == Float.MAX_VALUE) { - minYLimit = plot.getCalculatedMinY().floatValue(); + minYLimit = plot.getBounds().getMinY().floatValue(); lastMinY = minYLimit; } return minYLimit; @@ -255,7 +237,7 @@ protected float getMinYLimit() { protected float getMaxYLimit() { if(maxYLimit == Float.MAX_VALUE) { - maxYLimit = plot.getCalculatedMaxY().floatValue(); + maxYLimit = plot.getBounds().getMaxY().floatValue(); lastMaxY = maxYLimit; } return maxYLimit; @@ -263,28 +245,28 @@ protected float getMaxYLimit() { protected float getLastMinX() { if(lastMinX == Float.MAX_VALUE) { - lastMinX = plot.getCalculatedMinX().floatValue(); + lastMinX = plot.getBounds().getMinX().floatValue(); } return lastMinX; } protected float getLastMaxX() { if(lastMaxX == Float.MAX_VALUE) { - lastMaxX = plot.getCalculatedMaxX().floatValue(); + lastMaxX = plot.getBounds().getMaxX().floatValue(); } return lastMaxX; } protected float getLastMinY() { if(lastMinY == Float.MAX_VALUE) { - lastMinY = plot.getCalculatedMinY().floatValue(); + lastMinY = plot.getBounds().getMinY().floatValue(); } return lastMinY; } private float getLastMaxY() { if(lastMaxY == Float.MAX_VALUE) { - lastMaxY = plot.getCalculatedMaxY().floatValue(); + lastMaxY = plot.getBounds().getMaxY().floatValue(); } return lastMaxY; } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java index 1d9fe520..4f5f762d 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/PointLabelFormatter.java @@ -16,10 +16,8 @@ package com.androidplot.xy; -import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.PointF; import com.androidplot.util.PixelUtils; public class PointLabelFormatter { @@ -52,7 +50,14 @@ public PointLabelFormatter(int textColor, float hOffset, float vOffset) { this.vOffset = vOffset; } + public boolean hasTextPaint() { + return textPaint != null; + } + public Paint getTextPaint() { + if(textPaint == null) { + initTextPaint(Color.TRANSPARENT); + } return textPaint; } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java b/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java index 205d62a5..12504dff 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/PointLabeler.java @@ -16,7 +16,7 @@ package com.androidplot.xy; -public interface PointLabeler { +public interface PointLabeler { - String getLabel(XYSeries series, int index); + String getLabel(SeriesType series, int index); } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java index c406d9e2..5681c782 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/RectRegion.java @@ -18,9 +18,8 @@ import android.graphics.PointF; import android.graphics.RectF; -import com.androidplot.LineRegion; -import com.androidplot.util.ValPixConverter; +import com.androidplot.Region; import java.util.ArrayList; import java.util.List; @@ -31,23 +30,49 @@ */ public class RectRegion { - LineRegion xLineRegion; - LineRegion yLineRegion; + Region xRegion; + Region yRegion; private String label; + public RectRegion() { + xRegion = new Region(); + yRegion = new Region(); + } + + public static RectRegion withDefaults(RectRegion defaults) { + if(defaults == null || !defaults.isFullyDefined()) { + throw new IllegalArgumentException("When specifying defaults, RectRegion param must contain no null values."); + } + + RectRegion r = new RectRegion(); + r.xRegion = Region.withDefaults(defaults.getxRegion()); + r.yRegion = Region.withDefaults(defaults.getyRegion()); + return r; + } + + public RectRegion(XYCoords min, XYCoords max) { + this(min.x, max.x, min.y, max.y); + } + /** - * * @param minX * @param maxX * @param minY * @param maxY */ public RectRegion(Number minX, Number maxX, Number minY, Number maxY, String label) { - xLineRegion = new LineRegion(minX, maxX); - yLineRegion = new LineRegion(minY, maxY); + xRegion = new Region(minX, maxX); + yRegion = new Region(minY, maxY); this.setLabel(label); } + public RectRegion(RectF rect) { + this(rect.left < rect.right ? rect.left : rect.right, + rect.right > rect.left ? rect.right : rect.left, + rect.bottom < rect.top ? rect.bottom : rect.top, + rect.top > rect.bottom ? rect.top : rect.bottom); + } + @SuppressWarnings("SameParameterValue") public RectRegion(Number minX, Number maxX, Number minY, Number maxY) { this(minX, maxX, minY, maxY, null); @@ -62,18 +87,91 @@ public boolean containsValue(Number x, Number y) { } public boolean containsDomainValue(Number value) { - return xLineRegion.contains(value); + return xRegion.contains(value); } public boolean containsRangeValue(Number value) { - return yLineRegion.contains(value); + return yRegion.contains(value); + } + + public XYCoords transform(Number x, Number y, RectRegion region2, boolean flipX, boolean flipY) { + Number xx = xRegion.transform(x.doubleValue(), region2.xRegion, flipX); + Number yy = yRegion.transform(y.doubleValue(), region2.yRegion, flipY); + return new XYCoords(xx, yy); + } + + public XYCoords transform(Number x, Number y, RectRegion region2) { + return transform(x, y, region2, false, false); + } + + public XYCoords transform(XYCoords value, RectRegion region2) { + return transform(value.x, value.y, region2); + } + + /** + * Transform a region (r) from the current region space (this) into the specified one (r2) + * @param r The region to which the transformation applies + * @param r2 The region into which r is being transformed + * @return + */ + public RectRegion transform(RectRegion r, RectRegion r2, boolean flipX, boolean flipY) { + return new RectRegion( + transform(r.getMinX(), r.getMinY(), r2, flipX, flipY), + transform(r.getMaxX(), r.getMaxY(), r2, flipX, flipY) + ); + } + + /** + * Convenience method to transform into screen coordinate space. Equivalent to invoking + * {@link #transform(Number, Number, RectF, boolean, boolean)} with + * flipX = false and flipY = true. + * @param x + * @param y + * @param region2 + * @return + */ + public PointF transformScreen(Number x, Number y, RectF region2) { + return transform(x, y, region2, false, true); + } + + public PointF transform(Number x, Number y, RectF region2, boolean flipX, boolean flipY) { + float xx = (float) xRegion.transform(x.doubleValue(), region2.left, region2.right, flipX); + float yy = (float) yRegion.transform(y.doubleValue(), region2.top, region2.bottom, flipY); + return new PointF(xx, yy); + } + + public PointF transformScreen(XYCoords value, RectF region2) { + return transform(value, region2, false, true); + } + + /** + * Convenience method to transform into screen coordinate space. Equivalent to invoking + * {@link #transform(XYCoords, RectF, boolean, boolean)} with flipX = false and flipY = true. + * @param value + * @param region2 + * @param flipX + * @param flipY + * @return + */ + public PointF transform(XYCoords value, RectF region2, boolean flipX, boolean flipY) { + return transform(value.x, value.y, region2, flipX, flipY); + } + + /** + * Compares the input bounds xy min/max vals against this instance's current xy min/max vals. + * If the input.min is less than this.min then this.min will be set to input.min. + * If the input.max is greater than this.max then this.max will be set to input.max + * @param input + */ + public void union(RectRegion input) { + xRegion.union(input.xRegion); + yRegion.union(input.yRegion); } public boolean intersects(RectRegion region) { return intersects(region.getMinX(), region.getMaxX(), region.getMinY(), region.getMaxY()); } - /** * Tests whether this region intersects the region defined by params. Use * null to represent infinity. Negative and positive infinity is implied by @@ -86,51 +184,62 @@ public boolean intersects(RectRegion region) { * @return */ public boolean intersects(Number minX, Number maxX, Number minY, Number maxY) { - return xLineRegion.intersects(minX, maxX) && yLineRegion.intersects(minY, maxY); + return xRegion.intersects(minX, maxX) && yRegion.intersects(minY, maxY); } - public boolean intersects(RectF region, Number visMinX, Number visMaxX, Number visMinY, Number visMaxY) { - - RectF thisRegion = getRectF(region, visMinX.doubleValue(), visMaxX.doubleValue(), - visMinY.doubleValue(), visMaxY.doubleValue()); - return RectF.intersects(thisRegion, region); + public RectF asRectF() { + return new RectF(getMinX().floatValue(), getMinY().floatValue(), + getMaxX().floatValue(), getMaxY().floatValue()); } - public RectF getRectF(RectF plotRect, Number visMinX, Number visMaxX, Number visMinY, Number visMaxY) { - PointF topLeftPoint = ValPixConverter.valToPix( - xLineRegion.getMinVal().doubleValue() != Double.NEGATIVE_INFINITY ? xLineRegion.getMinVal() : visMinX, - yLineRegion.getMaxVal().doubleValue() != Double.POSITIVE_INFINITY ? yLineRegion.getMaxVal() : visMaxY, - plotRect, - visMinX, - visMaxX, - visMinY, - visMaxY); - PointF bottomRightPoint = ValPixConverter.valToPix( - xLineRegion.getMaxVal().doubleValue() != Double.POSITIVE_INFINITY ? xLineRegion.getMaxVal() : visMaxX, - yLineRegion.getMinVal().doubleValue() != Double.NEGATIVE_INFINITY ? yLineRegion.getMinVal() : visMinY, - plotRect, - visMinX, - visMaxX, - visMinY, - visMaxY); - // TODO: figure out why the y-values are inverted - return new RectF(topLeftPoint.x, topLeftPoint.y, bottomRightPoint.x, bottomRightPoint.y); +// public RectF getRectF(RectF plotRect, Number visMinX, Number visMaxX, Number visMinY, +// Number visMaxY) { +// PointF topLeftPoint = transform(new XYCoords( +// xRegion.getMin().doubleValue() != Double.NEGATIVE_INFINITY +// ? xRegion.getMin() : visMinX, +// yRegion.getMax().doubleValue() != Double.POSITIVE_INFINITY +// ? yRegion.getMax() : visMaxY), plotRect, false, true); +// +// PointF bottomRightPoint = transform(new XYCoords( +// xRegion.getMin().doubleValue() != Double.NEGATIVE_INFINITY +// ? xRegion.getMin() : visMaxX, +// yRegion.getMax().doubleValue() != Double.POSITIVE_INFINITY +// ? yRegion.getMax() : visMinY), plotRect, false, true); +// return new RectF( +// topLeftPoint.x, +// topLeftPoint.y, +// bottomRightPoint.x, +// bottomRightPoint.y); +// } + + public void clip(RectRegion clippingBounds) { + if(getMinX().doubleValue() < clippingBounds.getMinX().doubleValue()) { + setMinX(clippingBounds.getMinX()); + } + + if(getMinY().doubleValue() < clippingBounds.getMinY().doubleValue()) { + setMinY(clippingBounds.getMinY()); + } + + if(getMaxX().doubleValue() > clippingBounds.getMaxX().doubleValue()) { + setMaxX(clippingBounds.getMaxX()); + } + + if(getMaxY().doubleValue() > clippingBounds.getMaxY().doubleValue()) { + setMaxY(clippingBounds.getMaxY()); + } } /** * Returns a list of XYRegions that either completely or partially intersect the area * defined by params. A null value for any parameter represents infinity / no boundary. * @param regions The list of regions to search through - * @param minX - * @param maxX - * @param minY - * @param maxY * @return */ - public static List regionsWithin(List regions, Number minX, Number maxX, Number minY, Number maxY) { - ArrayList intersectingRegions = new ArrayList(); - for(RectRegion r : regions) { - if(r.intersects(minX, maxX, minY, maxY)) { + public List intersects(List regions) { + ArrayList intersectingRegions = new ArrayList<>(); + for (RectRegion r : regions) { + if (r.intersects(getMinX(), getMaxX(), getMinY(), getMaxY())) { intersectingRegions.add(r); } } @@ -138,7 +247,6 @@ public static List regionsWithin(List regions, Number mi } /** - * * @return Width of this region, in native units */ public Number getWidth() { @@ -146,7 +254,6 @@ public Number getWidth() { } /** - * * @return Height of this region, in native units */ public Number getHeight() { @@ -163,36 +270,52 @@ private Number distanceBetween(Number x, Number y) { return Math.abs(x.doubleValue() - y.doubleValue()); } + public boolean isMinXSet() { + return xRegion.isMinSet(); + } + public Number getMinX() { - return xLineRegion.getMinVal(); + return xRegion.getMin(); } - public void setMinX(double minX) { - xLineRegion.setMinVal(minX); + public void setMinX(Number minX) { + xRegion.setMin(minX); + } + + public boolean isMaxXSet() { + return xRegion.isMaxSet(); } public Number getMaxX() { - return xLineRegion.getMaxVal(); + return xRegion.getMax(); } public void setMaxX(Number maxX) { - xLineRegion.setMaxVal(maxX); + xRegion.setMax(maxX); + } + + public boolean isMinYSet() { + return yRegion.isMinSet(); } public Number getMinY() { - return yLineRegion.getMinVal(); + return yRegion.getMin(); } public void setMinY(Number minY) { - yLineRegion.setMinVal(minY); + yRegion.setMin(minY); + } + + public boolean isMaxYSet() { + return yRegion.isMaxSet(); } public Number getMaxY() { - return yLineRegion.getMaxVal(); + return yRegion.getMax(); } public void setMaxY(Number maxY) { - yLineRegion.setMaxVal(maxY); + yRegion.setMax(maxY); } public String getLabel() { @@ -202,4 +325,28 @@ public String getLabel() { public void setLabel(String label) { this.label = label; } + + public Region getxRegion() { + return xRegion; + } + + public void setxRegion(Region xRegion) { + this.xRegion = xRegion; + } + + public Region getyRegion() { + return yRegion; + } + + public void setyRegion(Region yRegion) { + this.yRegion = yRegion; + } + + /** + * + * @return True if both xRegion and yRegion are defined, false otherwise + */ + public boolean isFullyDefined() { + return xRegion.isDefined() && yRegion.isDefined(); + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java index 37e64701..58678d96 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/StepFormatter.java @@ -16,9 +16,7 @@ package com.androidplot.xy; -import android.content.Context; import com.androidplot.ui.SeriesRenderer; -import com.androidplot.util.Configurator; public class StepFormatter extends LineAndPointFormatter { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYBounds.java b/androidplot-core/src/main/java/com/androidplot/xy/XYBounds.java deleted file mode 100644 index 1d503ad6..00000000 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYBounds.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot.xy; - -import com.androidplot.Bounds; - -/** - * Defines a rectangle using xy min/max values. XYBounds differs from {@link RectRegion} in that - * it accepts null values. - * @since 0.9.7 - */ -public class XYBounds { - - private Bounds xBounds; - private Bounds yBounds; - - public XYBounds() { - this(null, null, null, null); - } - - public XYBounds(Number minX, Number maxX, Number minY, Number maxY) { - xBounds = new Bounds(minX, maxX); - yBounds = new Bounds(minY, maxY); - } - - /** - * Compares the input bounds xy min/max vals against this instance's current xy min/max vals. - * If the input.min is less than this.min then this.min will be set to input.min. - * If the input.max is greater than this.max then this.max will be set to input.max - * @param input - */ - public void union(XYBounds input) { - xBounds.union(input.getXBounds()); - yBounds.union(input.getYBounds()); - } - - public void setXBounds(Bounds xBounds) { - this.xBounds = xBounds; - } - - public void setYBounds(Bounds yBounds) { - this.yBounds = yBounds; - } - - public Bounds getXBounds() { - return xBounds; - } - - public Bounds getYBounds() { - return yBounds; - } - - public Number getMinX() { - return xBounds.getMin(); - } - - public void setMinX(Number minX) { - xBounds.setMin(minX); - } - - public Number getMaxX() { - return xBounds.getMax(); - } - - public void setMaxX(Number maxX) { - xBounds.setMax(maxX); - } - - public Number getMinY() { - return yBounds.getMin(); - } - - public void setMinY(Number minY) { - yBounds.setMin(minY); - } - - public Number getMaxY() { - return yBounds.getMax(); - } - - public void setMaxY(Number maxY) { - yBounds.setMax(maxY); - } -} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java index 398d0379..ae22a4ea 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYConstraints.java @@ -23,9 +23,9 @@ public class XYConstraints { // used for calculating the domain/range extents that will be displayed on the plot. - // using boundaries and origins are mutually exclusive. because of this, - // setting one will disable the other. when only setting the FramingModel, - // the origin or boundary is set to the current value of the plot. + // using boundaries and origins are mutually exclusive and enabling one will disable + // the other. when only setting the FramingModel, the origin or boundary is set to + // the current value of the plot. private XYFramingModel domainFramingModel = XYFramingModel.EDGE; private XYFramingModel rangeFramingModel = XYFramingModel.EDGE; diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java b/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java index 67edd506..1bd6c0c8 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYCoords.java @@ -23,6 +23,8 @@ public class XYCoords { public Number x; public Number y; + public XYCoords() {} + public XYCoords(Number x, Number y) { this.x = x; this.y = y; diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java index 59b0b394..c35c98da 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java @@ -20,6 +20,7 @@ import android.graphics.*; import com.androidplot.*; +import com.androidplot.Region; import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.*; import com.androidplot.ui.widget.Widget; @@ -129,38 +130,6 @@ public class XYGraphWidget extends Widget { private HashMap lineLabelStyles = getDefaultLineLabelStyles(); private HashMap lineLabelRenderers = getDefaultLineLabelRenderers(); - public float getLineExtensionTop() { - return lineExtensionTop; - } - - public void setLineExtensionTop(float lineExtensionTop) { - this.lineExtensionTop = lineExtensionTop; - } - - public float getLineExtensionBottom() { - return lineExtensionBottom; - } - - public void setLineExtensionBottom(float lineExtensionBottom) { - this.lineExtensionBottom = lineExtensionBottom; - } - - public float getLineExtensionLeft() { - return lineExtensionLeft; - } - - public void setLineExtensionLeft(float lineExtensionLeft) { - this.lineExtensionLeft = lineExtensionLeft; - } - - public float getLineExtensionRight() { - return lineExtensionRight; - } - - public void setLineExtensionRight(float lineExtensionRight) { - this.lineExtensionRight = lineExtensionRight; - } - public static class LineLabelRenderer { public void drawLabel(Canvas canvas, LineLabelStyle style, Number val, float x, float y, boolean isOrigin) { @@ -215,49 +184,6 @@ public void setPaint(Paint paint) { } } - protected HashMap getDefaultLineLabelStyles() { - HashMap defaults = new HashMap<>(); - defaults.put(Edge.TOP, new LineLabelStyle()); - defaults.put(Edge.BOTTOM, new LineLabelStyle()); - defaults.put(Edge.LEFT, new LineLabelStyle()); - defaults.put(Edge.RIGHT, new LineLabelStyle()); - return defaults; - } - - protected HashMap getDefaultLineLabelRenderers() { - HashMap defaults = new HashMap<>(); - defaults.put(Edge.TOP, new LineLabelRenderer()); - defaults.put(Edge.BOTTOM, new LineLabelRenderer()); - defaults.put(Edge.LEFT, new LineLabelRenderer()); - defaults.put(Edge.RIGHT, new LineLabelRenderer()); - return defaults; - } - - public LineLabelRenderer getLineLabelRenderer(Edge edge) { - return lineLabelRenderers.get(edge); - } - - public void setLineLabelRenderer(Edge edge, LineLabelRenderer renderer) { - lineLabelRenderers.put(edge, renderer); - } - - public LineLabelStyle getLineLabelStyle(Edge edge) { - return lineLabelStyles.get(edge); - } - - public void setLineLabelStyle(Edge edge, LineLabelStyle style) { - lineLabelStyles.put(edge, style); - } - - public CursorLabelFormatter getCursorLabelFormatter() { - return cursorLabelFormatter; - } - - public void setCursorLabelFormatter( - CursorLabelFormatter cursorLabelFormatter) { - this.cursorLabelFormatter = cursorLabelFormatter; - } - public interface CursorLabelFormatter { /** @@ -275,6 +201,58 @@ public interface CursorLabelFormatter { String getLabelText(Number x, Number y); } + public enum Edge { + LEFT(1), + RIGHT(2), + TOP(4), + BOTTOM(8); + + private final int value; + + Edge(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + { + gridBackgroundPaint = new Paint(); + gridBackgroundPaint.setColor(Color.rgb(140, 140, 140)); + gridBackgroundPaint.setStyle(Paint.Style.FILL); + + final Paint defaultLinePaint = new Paint(); + defaultLinePaint.setColor(Color.rgb(180, 180, 180)); + defaultLinePaint.setAntiAlias(true); + defaultLinePaint.setStyle(Paint.Style.STROKE); + + rangeGridLinePaint = new Paint(defaultLinePaint); + domainGridLinePaint = new Paint(defaultLinePaint); + domainSubGridLinePaint = new Paint(defaultLinePaint); + rangeSubGridLinePaint = new Paint(defaultLinePaint); + domainOriginLinePaint = new Paint(defaultLinePaint); + rangeOriginLinePaint = new Paint(defaultLinePaint); + + domainCursorPaint = new Paint(); + domainCursorPaint.setColor(Color.YELLOW); + + rangeCursorPaint = new Paint(); + rangeCursorPaint.setColor(Color.YELLOW); + + setMarginTop(7); + setMarginRight(4); + setMarginBottom(4); + setClippingEnabled(true); + } + + public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) { + super(layoutManager, size); + this.plot = plot; + renderStack = new RenderStack(plot); + } + /** * Apply xml attrs * @param attrs @@ -401,163 +379,11 @@ public void processAttrs(TypedArray attrs) { R.styleable.xy_XYPlot_rangeLineColor, R.styleable.xy_XYPlot_rangeLineThickness); - getBackgroundPaint().setColor(attrs.getColor( - R.styleable.xy_XYPlot_graphBackgroundColor, - getBackgroundPaint().getColor())); + AttrUtils.setColor(attrs, getBackgroundPaint(), + R.styleable.xy_XYPlot_graphBackgroundColor); - getGridBackgroundPaint().setColor(attrs.getColor( - R.styleable.xy_XYPlot_gridBackgroundColor, - getGridBackgroundPaint().getColor())); - } - - /** - * Grid insets - */ - public Insets getGridInsets() { - return gridInsets; - } - - public void setGridInsets(Insets gridInsets) { - this.gridInsets = gridInsets; - } - - /** - * Domain / Range label insets - */ - public Insets getLineLabelInsets() { - return lineLabelInsets; - } - - public void setLineLabelInsets(Insets lineLabelInsets) { - this.lineLabelInsets = lineLabelInsets; - } - - public RectF getGridRect() { - return gridRect; - } - - public void setGridRect(RectF gridRect) { - this.gridRect = gridRect; - } - - public RectF getLabelRect() { - return labelRect; - } - - public void setLabelRect(RectF labelRect) { - this.labelRect = labelRect; - } - - public boolean isGridClippingEnabled() { - return isGridClippingEnabled; - } - - public void setGridClippingEnabled(boolean gridClippingEnabled) { - isGridClippingEnabled = gridClippingEnabled; - } - - public boolean isLineLabelEnabled(Edge position) { - return lineLabelEdges.contains(position); - } - - public void setLineLabelEdges(Edge... positions) { - Set positionSet = new HashSet<>(); - if(positions != null) { - for(Edge position : positions) { - positionSet.add(position); - } - } - setLineLabelEdges(positionSet); - } - - public void setLineLabelEdges(Set positions) { - this.lineLabelEdges = positions; - } - - protected void setLineLabelEdges(int bitfield) { - for(Edge tp : Edge.values()) { - if((tp.value & bitfield) == tp.value) { - lineLabelEdges.add(tp); - } - } - } - - public enum Edge { - LEFT(1), - RIGHT(2), - TOP(4), - BOTTOM(8); - - private final int value; - - Edge(int value) { - this.value = value; - } - - public int getValue() { - return value; - } - } - - public Paint getDomainCursorPaint() { - return domainCursorPaint; - } - - /** - * - * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line. - * Set to null (default) to disable. - */ - public void setDomainCursorPaint(Paint domainCursorPaint) { - this.domainCursorPaint = domainCursorPaint; - } - - public Paint getRangeCursorPaint() { - return rangeCursorPaint; - } - - /** - * - * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line. - * Set to null (default) to disable. - */ - public void setRangeCursorPaint(Paint rangeCursorPaint) { - this.rangeCursorPaint = rangeCursorPaint; - } - - { - gridBackgroundPaint = new Paint(); - gridBackgroundPaint.setColor(Color.rgb(140, 140, 140)); - gridBackgroundPaint.setStyle(Paint.Style.FILL); - - final Paint defaultLinePaint = new Paint(); - defaultLinePaint.setColor(Color.rgb(180, 180, 180)); - defaultLinePaint.setAntiAlias(true); - defaultLinePaint.setStyle(Paint.Style.STROKE); - - rangeGridLinePaint = new Paint(defaultLinePaint); - domainGridLinePaint = new Paint(defaultLinePaint); - domainSubGridLinePaint = new Paint(defaultLinePaint); - rangeSubGridLinePaint = new Paint(defaultLinePaint); - domainOriginLinePaint = new Paint(defaultLinePaint); - rangeOriginLinePaint = new Paint(defaultLinePaint); - - domainCursorPaint = new Paint(); - domainCursorPaint.setColor(Color.YELLOW); - - rangeCursorPaint = new Paint(); - rangeCursorPaint.setColor(Color.YELLOW); - - setMarginTop(7); - setMarginRight(4); - setMarginBottom(4); - setClippingEnabled(true); - } - - public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) { - super(layoutManager, size); - this.plot = plot; - renderStack = new RenderStack(plot); + AttrUtils.setColor(attrs, getGridBackgroundPaint(), + R.styleable.xy_XYPlot_gridBackgroundColor); } /** @@ -566,7 +392,7 @@ public XYGraphWidget(LayoutManager layoutManager, XYPlot plot, Size size) { * @param point * @return */ - public Double getYVal(PointF point) { + public Number getYVal(PointF point) { return getYVal(point.y); } @@ -576,14 +402,12 @@ public Double getYVal(PointF point) { * @param yPix * @return */ - public Double getYVal(float yPix) { - if (plot.getCalculatedMinY() == null - || plot.getCalculatedMaxY() == null) { + public Number getYVal(float yPix) { + if (!plot.getBounds().getyRegion().isDefined()) { return null; } - return ValPixConverter.pixToVal(yPix - gridRect.top, plot - .getCalculatedMinY().doubleValue(), plot.getCalculatedMaxY() - .doubleValue(), gridRect.height(), true); + return new Region(gridRect.top, gridRect.bottom) + .transform(yPix, plot.getBounds().getyRegion(), true); } /** @@ -592,7 +416,7 @@ public Double getYVal(float yPix) { * @param point * @return */ - public Double getXVal(PointF point) { + public Number getXVal(PointF point) { return getXVal(point.x); } @@ -602,14 +426,12 @@ public Double getXVal(PointF point) { * @param xPix * @return */ - public Double getXVal(float xPix) { - if (plot.getCalculatedMinX() == null - || plot.getCalculatedMaxX() == null) { + public Number getXVal(float xPix) { + if (!plot.getBounds().xRegion.isDefined()) { return null; } - return ValPixConverter.pixToVal(xPix - gridRect.left, plot - .getCalculatedMinX().doubleValue(), plot.getCalculatedMaxX() - .doubleValue(), gridRect.width(), false); + return new Region(gridRect.left, gridRect.right) + .transform(xPix, plot.getBounds().getxRegion()); } @Override @@ -626,10 +448,11 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect) // don't draw if we have no space to draw into if (gridRect.height() > ZERO && gridRect.width() > ZERO) { - if (plot.getCalculatedMinX() != null - && plot.getCalculatedMaxX() != null - && plot.getCalculatedMinY() != null - && plot.getCalculatedMaxY() != null) { + final RectRegion bounds = plot.getBounds(); + if (bounds.getMinX() != null + && bounds.getMaxX() != null + && bounds.getMinY() != null + && bounds.getMaxY() != null) { if(drawGridOnTop) { drawData(canvas); drawGrid(canvas); @@ -645,7 +468,7 @@ protected void doOnDraw(Canvas canvas, RectF widgetRect) } } - private void drawDomainLine(Canvas canvas, float xPix, Number xVal, + protected void drawDomainLine(Canvas canvas, float xPix, Number xVal, Paint linePaint, boolean isOrigin) { // lines @@ -659,7 +482,7 @@ private void drawDomainLine(Canvas canvas, float xPix, Number xVal, drawLineLabel(canvas, Edge.BOTTOM, xVal, xPix, labelRect.bottom, isOrigin); } - public void drawRangeLine(Canvas canvas, float yPix, Number yVal, + protected void drawRangeLine(Canvas canvas, float yPix, Number yVal, Paint linePaint, boolean isOrigin) { // lines if (linePaint != null) { @@ -688,22 +511,17 @@ protected void drawGrid(Canvas canvas) { drawGridBackground(canvas); } + double domainOrigin; if (plot.getDomainOrigin() != null) { - double domainOriginVal = plot.getDomainOrigin().doubleValue(); - domainOrigin = ValPixConverter.valToPix(domainOriginVal, plot - .getCalculatedMinX().doubleValue(), plot - .getCalculatedMaxX().doubleValue(), gridRect.width(), - false); - domainOrigin += gridRect.left; - // if no origin is set, use the leftmost value visible on the grid: + domainOrigin = plot.getBounds().getxRegion().transform( + plot.getDomainOrigin().doubleValue(), gridRect.left, gridRect.right, false); } else { + // if no domain origin is set, use the leftmost value visible on the grid: domainOrigin = gridRect.left; } - Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, - gridRect, plot.getCalculatedMinX().doubleValue(), plot - .getCalculatedMaxX().doubleValue()); + Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect); // draw domain origin: if (domainOrigin >= gridRect.left @@ -744,20 +562,14 @@ protected void drawGrid(Canvas canvas) { double rangeOrigin; if (plot.getRangeOrigin() != null) { - double rangeOriginD = plot.getRangeOrigin().doubleValue(); - rangeOrigin = ValPixConverter.valToPix(rangeOriginD, plot - .getCalculatedMinY().doubleValue(), plot - .getCalculatedMaxY().doubleValue(), - gridRect.height(), true); - rangeOrigin += gridRect.top; - // if no origin is set, use the leftmost value visible on the grid + rangeOrigin = plot.getBounds().getyRegion().transform( + plot.getRangeOrigin().doubleValue(), gridRect.top, gridRect.bottom, true); } else { + // if no range origin is set, use the bottom-most value visible on the grid: rangeOrigin = gridRect.bottom; } - Step rangeStep = XYStepCalculator.getStep(plot, Axis.RANGE, - gridRect, plot.getCalculatedMinY().doubleValue(), plot - .getCalculatedMaxY().doubleValue()); + Step rangeStep = XYStepCalculator.getStep(plot, Axis.RANGE, gridRect); // draw range origin: if (rangeOrigin >= gridRect.top && rangeOrigin <= gridRect.bottom) { @@ -825,38 +637,39 @@ private void drawMarkerText(Canvas canvas, String text, ValueMarker marker, } protected void drawMarkers(Canvas canvas) { - for (YValueMarker marker : plot.getYValueMarkers()) { - if (marker.getValue() != null) { - double yVal = marker.getValue().doubleValue(); - float yPix = (float) ValPixConverter.valToPix(yVal, plot - .getCalculatedMinY().doubleValue(), plot - .getCalculatedMaxY().doubleValue(), gridRect.height(), true); - yPix += gridRect.top; - canvas.drawLine(gridRect.left, yPix, - gridRect.right, yPix, marker.getLinePaint()); - - float xPix = marker.getTextPosition().getPixelValue( - gridRect.width()); - xPix += gridRect.left; - - if (marker.getText() != null) { - drawMarkerText(canvas, marker.getText(), marker, xPix, yPix); + if(plot.getYValueMarkers() != null && plot.getYValueMarkers().size() > 0) { + for (YValueMarker marker : plot.getYValueMarkers()) { + if (marker.getValue() != null) { + float yPix = (float) plot.getBounds().yRegion + .transform(marker.getValue() + .doubleValue(), gridRect.top, gridRect.bottom, true); + canvas.drawLine(gridRect.left, yPix, + gridRect.right, yPix, marker.getLinePaint()); + + float xPix = marker.getTextPosition().getPixelValue( + gridRect.width()); + xPix += gridRect.left; + + if (marker.getText() != null) { + drawMarkerText(canvas, marker.getText(), marker, xPix, yPix); + } } } } - for (XValueMarker marker : plot.getXValueMarkers()) { - if (marker.getValue() != null) { - double xVal = marker.getValue().doubleValue(); - float xPix = (float) ValPixConverter.valToPix(xVal, plot - .getCalculatedMinX().doubleValue(), plot - .getCalculatedMaxX().doubleValue(), gridRect.width(), false); - xPix += gridRect.left; - canvas.drawLine(xPix, gridRect.top, xPix, gridRect.bottom, marker.getLinePaint()); - float yPix = marker.getTextPosition().getPixelValue(gridRect.height()); - yPix += gridRect.top; - if (marker.getText() != null) { - drawMarkerText(canvas, marker.getText(), marker, xPix, yPix); + if(plot.getXValueMarkers() != null && plot.getXValueMarkers().size() > 0) { + for (XValueMarker marker : plot.getXValueMarkers()) { + if (marker.getValue() != null) { + float xPix = (float) plot.getBounds().xRegion + .transform(marker.getValue() + .doubleValue(), gridRect.left, gridRect.right, false); + canvas.drawLine(xPix, gridRect.top, xPix, gridRect.bottom, + marker.getLinePaint()); + float yPix = marker.getTextPosition().getPixelValue(gridRect.height()); + yPix += gridRect.top; + if (marker.getText() != null) { + drawMarkerText(canvas, marker.getText(), marker, xPix, yPix); + } } } } @@ -1073,7 +886,7 @@ public float getDomainCursorPosition() { return domainCursorPosition; } - public Double getDomainCursorVal() { + public Number getDomainCursorVal() { return getXVal(getDomainCursorPosition()); } @@ -1085,7 +898,7 @@ public float getRangeCursorPosition() { return rangeCursorPosition; } - public Double getRangeCursorVal() { + public Number getRangeCursorVal() { return getYVal(getRangeCursorPosition()); } @@ -1109,6 +922,179 @@ public void setDrawMarkersEnabled(boolean drawMarkersEnabled) { this.drawMarkersEnabled = drawMarkersEnabled; } + public Paint getDomainCursorPaint() { + return domainCursorPaint; + } + + /** + * + * @param domainCursorPaint The {@link Paint} used to draw the domain cursor line. + * Set to null (default) to disable. + */ + public void setDomainCursorPaint(Paint domainCursorPaint) { + this.domainCursorPaint = domainCursorPaint; + } + + public Paint getRangeCursorPaint() { + return rangeCursorPaint; + } + + /** + * + * @param rangeCursorPaint The {@link Paint} used to draw the range cursor line. + * Set to null (default) to disable. + */ + public void setRangeCursorPaint(Paint rangeCursorPaint) { + this.rangeCursorPaint = rangeCursorPaint; + } + + public float getLineExtensionTop() { + return lineExtensionTop; + } + + public void setLineExtensionTop(float lineExtensionTop) { + this.lineExtensionTop = lineExtensionTop; + } + + public float getLineExtensionBottom() { + return lineExtensionBottom; + } + + public void setLineExtensionBottom(float lineExtensionBottom) { + this.lineExtensionBottom = lineExtensionBottom; + } + + public float getLineExtensionLeft() { + return lineExtensionLeft; + } + + public void setLineExtensionLeft(float lineExtensionLeft) { + this.lineExtensionLeft = lineExtensionLeft; + } + + public float getLineExtensionRight() { + return lineExtensionRight; + } + + public void setLineExtensionRight(float lineExtensionRight) { + this.lineExtensionRight = lineExtensionRight; + } + + protected HashMap getDefaultLineLabelStyles() { + HashMap defaults = new HashMap<>(); + defaults.put(Edge.TOP, new LineLabelStyle()); + defaults.put(Edge.BOTTOM, new LineLabelStyle()); + defaults.put(Edge.LEFT, new LineLabelStyle()); + defaults.put(Edge.RIGHT, new LineLabelStyle()); + return defaults; + } + + protected HashMap getDefaultLineLabelRenderers() { + HashMap defaults = new HashMap<>(); + defaults.put(Edge.TOP, new LineLabelRenderer()); + defaults.put(Edge.BOTTOM, new LineLabelRenderer()); + defaults.put(Edge.LEFT, new LineLabelRenderer()); + defaults.put(Edge.RIGHT, new LineLabelRenderer()); + return defaults; + } + + public LineLabelRenderer getLineLabelRenderer(Edge edge) { + return lineLabelRenderers.get(edge); + } + + public void setLineLabelRenderer(Edge edge, LineLabelRenderer renderer) { + lineLabelRenderers.put(edge, renderer); + } + + public LineLabelStyle getLineLabelStyle(Edge edge) { + return lineLabelStyles.get(edge); + } + + public void setLineLabelStyle(Edge edge, LineLabelStyle style) { + lineLabelStyles.put(edge, style); + } + + public CursorLabelFormatter getCursorLabelFormatter() { + return cursorLabelFormatter; + } + + public void setCursorLabelFormatter( + CursorLabelFormatter cursorLabelFormatter) { + this.cursorLabelFormatter = cursorLabelFormatter; + } + + /** + * Grid insets + */ + public Insets getGridInsets() { + return gridInsets; + } + + public void setGridInsets(Insets gridInsets) { + this.gridInsets = gridInsets; + } + + /** + * Domain / Range label insets + */ + public Insets getLineLabelInsets() { + return lineLabelInsets; + } + + public void setLineLabelInsets(Insets lineLabelInsets) { + this.lineLabelInsets = lineLabelInsets; + } + + public RectF getGridRect() { + return gridRect; + } + + public void setGridRect(RectF gridRect) { + this.gridRect = gridRect; + } + + public RectF getLabelRect() { + return labelRect; + } + + public void setLabelRect(RectF labelRect) { + this.labelRect = labelRect; + } + + public boolean isGridClippingEnabled() { + return isGridClippingEnabled; + } + + public void setGridClippingEnabled(boolean gridClippingEnabled) { + isGridClippingEnabled = gridClippingEnabled; + } + + public boolean isLineLabelEnabled(Edge position) { + return lineLabelEdges.contains(position); + } + + public void setLineLabelEdges(Edge... positions) { + Set positionSet = new HashSet<>(); + if(positions != null) { + for(Edge position : positions) { + positionSet.add(position); + } + } + setLineLabelEdges(positionSet); + } + + public void setLineLabelEdges(Set positions) { + this.lineLabelEdges = positions; + } + + protected void setLineLabelEdges(int bitfield) { + for(Edge tp : Edge.values()) { + if((tp.value & bitfield) == tp.value) { + lineLabelEdges.add(tp); + } + } + } + /** * Checks whether the point exists within the visible grid space. * 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 a9b24f36..4b709de0 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -84,10 +84,11 @@ public class XYPlot extends Plot private XYConstraints constraints = new XYConstraints(); // these are the final min/max used for dispplaying data - private Number calculatedMinX; - private Number calculatedMaxX; - private Number calculatedMinY; - private Number calculatedMaxY; +// private Number calculatedMinX; +// private Number calculatedMaxX; +// private Number calculatedMinY; +// private Number calculatedMaxY; + private RectRegion bounds = RectRegion.withDefaults(new RectRegion(-1, 1, -1, 1)); // previous calculated min/max vals. // primarily used for GROW/SHRINK operations. @@ -110,8 +111,7 @@ public class XYPlot extends Plot private Number userDomainOrigin; private Number userRangeOrigin; - private Number calculatedDomainOrigin; - private Number calculatedRangeOrigin; + private XYCoords calculatedOrigin = new XYCoords(); @SuppressWarnings("FieldCanBeLocal") private Number domainOriginExtent = null; @@ -119,14 +119,9 @@ public class XYPlot extends Plot @SuppressWarnings("FieldCanBeLocal") private Number rangeOriginExtent = null; - private boolean drawDomainOriginEnabled = true; - private boolean drawRangeOriginEnabled = true; - private ArrayList yValueMarkers; private ArrayList xValueMarkers; - private RectRegion defaultBounds; - private PreviewMode previewMode; public enum PreviewMode { LineAndPoint, @@ -239,8 +234,6 @@ protected void onPreInit() { xValueMarkers = new ArrayList<>(); yValueMarkers = new ArrayList<>(); - setDefaultBounds(new RectRegion(-1, 1, -1, 1)); - domainStepModel = new StepModel(StepMode.SUBDIVIDE, 10); rangeStepModel = new StepModel(StepMode.SUBDIVIDE, 10); } @@ -252,11 +245,14 @@ protected void onAfterConfig() { switch (previewMode) { case LineAndPoint: { - addSeries(new SimpleXYSeries(Arrays.asList(1, 2, 3, 3, 4), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Red"), + addSeries(new SimpleXYSeries(Arrays.asList(1, 2, 3, 3, 4), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Red"), new LineAndPointFormatter(Color.RED, null, null, null)); - addSeries(new SimpleXYSeries(Arrays.asList(2, 1, 4, 2, 5), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Green"), + addSeries(new SimpleXYSeries(Arrays.asList(2, 1, 4, 2, 5), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Green"), new LineAndPointFormatter(Color.GREEN, null, null, null)); - addSeries(new SimpleXYSeries(Arrays.asList(3, 3, 2, 3, 3), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Blue"), + addSeries(new SimpleXYSeries(Arrays.asList(3, 3, 2, 3, 3), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Blue"), new LineAndPointFormatter(Color.BLUE, null, null, null)); } break; @@ -378,25 +374,34 @@ public Number getXVal(PointF point) { } public void calculateMinMaxVals() { - prevMinX = calculatedMinX; - prevMaxX = calculatedMaxX; - prevMinY = calculatedMinY; - prevMaxY = calculatedMaxY; + prevMinX = bounds.isMinXSet() ? bounds.getMinX() : null; + prevMaxX = bounds.isMaxXSet() ? bounds.getMaxX() : null; + prevMinY = bounds.isMinYSet() ? bounds.getMinY() : null; + prevMaxY = bounds.isMaxYSet() ? bounds.getMaxY() : null; - calculatedMinX = constraints.getMinX(); - calculatedMaxX = constraints.getMaxX(); - calculatedMinY = constraints.getMinY(); - calculatedMaxY = constraints.getMaxY(); + bounds.setMinX(constraints.getMinX()); + bounds.setMaxX(constraints.getMaxX()); + bounds.setMinY(constraints.getMinY()); + bounds.setMaxY(constraints.getMaxY()); // only calculate if we must: - if(calculatedMinX == null || calculatedMaxX == null || calculatedMinY == null || calculatedMaxY == null) { + if(!bounds.isFullyDefined()) { - XYBounds bounds = SeriesUtils.minMax(constraints, getSeriesRegistry().getSeriesList()); + RectRegion b = SeriesUtils.minMax(constraints, getSeriesRegistry().getSeriesList()); - if(calculatedMinX == null) calculatedMinX = bounds.getMinX(); - if(calculatedMaxX == null) calculatedMaxX = bounds.getMaxX(); - if(calculatedMinY == null) calculatedMinY = bounds.getMinY(); - if(calculatedMaxY == null) calculatedMaxY = bounds.getMaxY(); + if(!bounds.isMinXSet()) { + bounds.setMinX(b.getMinX()); + } + if(!bounds.isMaxXSet()) { + bounds.setMaxX(b.getMaxX()); + } + + if(!bounds.isMinYSet()) { + bounds.setMinY(b.getMinY()); + } + if(!bounds.isMaxYSet()) { + bounds.setMaxY(b.getMaxY()); + } } // at this point we now know what points are going to be visible on our @@ -407,14 +412,13 @@ public void calculateMinMaxVals() { updateDomainMinMaxForOriginModel(); break; case EDGE: - calculatedMaxX = getCalculatedUpperBoundary( - constraints.getDomainUpperBoundaryMode(), prevMaxX, calculatedMaxX); - calculatedMinX = getCalculatedLowerBoundary( - constraints.getDomainLowerBoundaryMode(), prevMinX, calculatedMinX); - calculatedMinX = applyUserMinMax(calculatedMinX, domainLeftMin, - domainLeftMax); - calculatedMaxX = applyUserMinMax(calculatedMaxX, - domainRightMin, domainRightMax); + bounds.setMaxX(applyUserMinMax(getCalculatedUpperBoundary( + constraints.getDomainUpperBoundaryMode(), prevMaxX, bounds.getMaxX()), + domainRightMin, domainRightMax)); + bounds.setMinX(applyUserMinMax(getCalculatedLowerBoundary( + constraints.getDomainLowerBoundaryMode(), + prevMinX, bounds.getMinX()), + domainLeftMin, domainLeftMax)); break; default: throw new UnsupportedOperationException( @@ -427,12 +431,12 @@ public void calculateMinMaxVals() { break; case EDGE: if (getSeriesRegistry().size() > 0) { - calculatedMaxY = getCalculatedUpperBoundary( - constraints.getRangeUpperBoundaryMode(), prevMaxY, calculatedMaxY); - calculatedMinY = getCalculatedLowerBoundary( - constraints.getRangeLowerBoundaryMode(), prevMinY, calculatedMinY); - calculatedMinY = applyUserMinMax(calculatedMinY, rangeBottomMin, rangeBottomMax); - calculatedMaxY = applyUserMinMax(calculatedMaxY, rangeTopMin, rangeTopMax); + bounds.setMaxY(applyUserMinMax(getCalculatedUpperBoundary( + constraints.getRangeUpperBoundaryMode(), + prevMaxY, bounds.getMaxY()), rangeTopMin, rangeTopMax)); + bounds.setMinY(applyUserMinMax(getCalculatedLowerBoundary( + constraints.getRangeLowerBoundaryMode(), + prevMinY, bounds.getMinY()), rangeBottomMin, rangeBottomMax)); } break; default: @@ -440,11 +444,12 @@ public void calculateMinMaxVals() { "Range Framing Model not yet supported: " + constraints.getRangeFramingModel()); } - calculatedDomainOrigin = userDomainOrigin != null ? - userDomainOrigin : getCalculatedMinX(); - calculatedRangeOrigin = this.userRangeOrigin != null ? - userRangeOrigin : getCalculatedMinY(); + calculatedOrigin.x = userDomainOrigin != null ? + userDomainOrigin : bounds.getMinX(); + + calculatedOrigin.y = this.userRangeOrigin != null ? + userRangeOrigin : bounds.getMinY(); } protected Number getCalculatedUpperBoundary(BoundaryMode mode, Number previousMax, Number calculatedMax) { @@ -606,15 +611,15 @@ private double distance(double x, double y) { public void updateDomainMinMaxForOriginModel() { double origin = userDomainOrigin.doubleValue(); - double maxXDelta = distance(calculatedMaxX.doubleValue(), origin); - double minXDelta = distance(calculatedMinX.doubleValue(), origin); + double maxXDelta = distance(bounds.getMaxX().doubleValue(), origin); + double minXDelta = distance(bounds.getMinX().doubleValue(), origin); double delta = maxXDelta > minXDelta ? maxXDelta : minXDelta; double dlb = origin - delta; double dub = origin + delta; switch (domainOriginBoundaryMode) { case AUTO: - calculatedMinX = dlb; - calculatedMaxX = dub; + bounds.setMinX(dlb); + bounds.setMaxX(dub); break; // if fixed, then the value already exists within "user" vals. @@ -623,29 +628,29 @@ public void updateDomainMinMaxForOriginModel() { case GROW: { if (prevMinX == null || dlb < prevMinX.doubleValue()) { - calculatedMinX = dlb; + bounds.setMinX(dlb); } else { - calculatedMinX = prevMinX; + bounds.setMinX(prevMinX); } if (prevMaxX == null || dub > prevMaxX.doubleValue()) { - calculatedMaxX = dub; + bounds.setMaxX(dub); } else { - calculatedMaxX = prevMaxX; + bounds.setMaxX(prevMaxX); } } break; case SHRINK: if (prevMinX == null || dlb > prevMinX.doubleValue()) { - calculatedMinX = dlb; + bounds.setMinX(dlb); } else { - calculatedMinX = prevMinX; + bounds.setMinX(prevMinX); } if (prevMaxX == null || dub < prevMaxX.doubleValue()) { - calculatedMaxX = dub; + bounds.setMaxX(dub); } else { - calculatedMaxX = prevMaxX; + bounds.setMaxX(prevMaxX); } break; default: @@ -657,14 +662,14 @@ public void updateRangeMinMaxForOriginModel() { switch (rangeOriginBoundaryMode) { case AUTO: double origin = userRangeOrigin.doubleValue(); - double maxYDelta = distance(calculatedMaxY.doubleValue(), origin); - double minYDelta = distance(calculatedMinY.doubleValue(), origin); + double maxYDelta = distance(bounds.getMaxY().doubleValue(), origin); + double minYDelta = distance(bounds.getMinY().doubleValue(), origin); if (maxYDelta > minYDelta) { - calculatedMinY = origin - maxYDelta; - calculatedMaxY = origin + maxYDelta; + bounds.setMinY(origin - maxYDelta); + bounds.setMaxY(origin + maxYDelta); } else { - calculatedMinY = origin - minYDelta; - calculatedMaxY = origin + minYDelta; + bounds.setMinY(origin - minYDelta); + bounds.setMaxY(origin + minYDelta); } break; case FIXED: @@ -928,12 +933,16 @@ public synchronized void setRangeLowerBoundary(Number boundary, BoundaryMode mod setRangeFramingModel(XYFramingModel.EDGE); } + public XYCoords getOrigin() { + return calculatedOrigin; + } + public Number getDomainOrigin() { - return calculatedDomainOrigin; + return calculatedOrigin.x; } public Number getRangeOrigin() { - return calculatedRangeOrigin; + return calculatedOrigin.y; } public synchronized void setUserDomainOrigin(Number origin) { @@ -961,39 +970,12 @@ protected void setRangeFramingModel(XYFramingModel model) { } /** - * CalculatedMinX value after the the framing model has been applied. * - * @return + * @return The current min/max values for real domain and range that fall within the visible + * graph space. */ - public Number getCalculatedMinX() { - return calculatedMinX != null ? calculatedMinX : getDefaultBounds().getMinX(); - } - - /** - * CalculatedMaxX value after the the framing model has been applied. - * - * @return - */ - public Number getCalculatedMaxX() { - return calculatedMaxX != null ? calculatedMaxX : getDefaultBounds().getMaxX(); - } - - /** - * CalculatedMinY value after the the framing model has been applied. - * - * @return - */ - public Number getCalculatedMinY() { - return calculatedMinY != null ? calculatedMinY : getDefaultBounds().getMinY(); - } - - /** - * CalculatedMaxY value after the the framing model has been applied. - * - * @return - */ - public Number getCalculatedMaxY() { - return calculatedMaxY != null ? calculatedMaxY : getDefaultBounds().getMaxY(); + public RectRegion getBounds() { + return bounds; } /** @@ -1091,13 +1073,13 @@ protected List getXValueMarkers() { return xValueMarkers; } - public RectRegion getDefaultBounds() { - return defaultBounds; - } - - public void setDefaultBounds(RectRegion defaultBounds) { - this.defaultBounds = defaultBounds; - } +// public RectRegion getDefaultBounds() { +// return defaultBounds; +// } +// +// public void setDefaultBounds(RectRegion defaultBounds) { +// this.defaultBounds = defaultBounds; +// } /** * @return the rangeTopMin diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java index ae2bc93d..b0a3186f 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java @@ -18,7 +18,8 @@ import android.content.Context; import android.graphics.Paint; -import com.androidplot.util.Configurator; + +import com.halfhp.fig.*; /** * Base class of all XYRegionFormatters. @@ -42,7 +43,7 @@ public class XYRegionFormatter { public XYRegionFormatter(Context ctx, int xmlCfgId) { // prevent configuration of classes derived from this one: if (getClass().equals(XYRegionFormatter.class)) { - Configurator.configure(ctx, this, xmlCfgId); + Fig.configure(ctx, this, xmlCfgId); } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java index 3bc1cb03..0c18d2e9 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYSeriesFormatter.java @@ -16,17 +16,36 @@ package com.androidplot.xy; +import android.content.*; + import com.androidplot.ui.Formatter; import com.androidplot.util.LayerHash; import com.androidplot.util.Layerable; public abstract class XYSeriesFormatter extends Formatter { + + // instantiate a default implementation prints point's yVal: + private PointLabeler pointLabeler = new PointLabeler() { + @Override + public String getLabel(XYSeries series, int index) { + return series.getY(index) + ""; + } + }; + + private PointLabelFormatter pointLabelFormatter; + LayerHash regions; { regions = new LayerHash<>(); } + public XYSeriesFormatter() {} + + public XYSeriesFormatter(Context context, int xmlCfgId) { + super(context, xmlCfgId); + } + public void addRegion(RectRegion region, XYRegionFormatterType regionFormatter) { regions.addToBottom(region, regionFormatter); } @@ -50,4 +69,28 @@ public Layerable getRegions() { public XYRegionFormatterType getRegionFormatter(RectRegion region) { return regions.get(region); } + + + public PointLabeler getPointLabeler() { + return pointLabeler; + } + + public void setPointLabeler(PointLabeler pointLabeler) { + this.pointLabeler = pointLabeler; + } + + public boolean hasPointLabelFormatter() { + return pointLabelFormatter != null; + } + + public PointLabelFormatter getPointLabelFormatter() { + if(pointLabelFormatter == null) { + pointLabelFormatter = new PointLabelFormatter(); + } + return pointLabelFormatter; + } + + public void setPointLabelFormatter(PointLabelFormatter pointLabelFormatter) { + this.pointLabelFormatter = pointLabelFormatter; + } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java b/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java index 1fb04d0b..d90d9172 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYStepCalculator.java @@ -17,7 +17,8 @@ package com.androidplot.xy; import android.graphics.RectF; -import com.androidplot.util.ValPixConverter; + +import com.androidplot.*; /** * Calculates "stepping" values for a plot. These values are most commonly used for @@ -30,44 +31,47 @@ public class XYStepCalculator { * Convenience method - wraps other form of getStep(). * @param plot * @param axisType - * @param rect - * @param minVal - * @param maxVal + * @param pixRect * @return */ - public static Step getStep(XYPlot plot, Axis axisType, RectF rect, Number minVal, Number maxVal) { + public static Step getStep(XYPlot plot, Axis axisType, RectF pixRect) { Step step = null; switch(axisType) { case DOMAIN: - step = getStep(plot.getDomainStepMode(), rect.width(), plot.getDomainStepValue(), minVal, maxVal); + step = getStep(plot.getDomainStepMode(), + plot.getDomainStepValue(), + plot.getBounds().getxRegion(), + new Region(pixRect.left, pixRect.right)); break; case RANGE: - step = getStep(plot.getRangeStepMode(), rect.height(), plot.getRangeStepValue(), minVal, maxVal); + step = getStep(plot.getRangeStepMode(), + plot.getRangeStepValue(), + plot.getBounds().getyRegion(), + new Region(pixRect.top, pixRect.bottom)); break; } return step; } - public static Step getStep(StepMode typeXY, float plotPixelSize, double stepValue, Number minVal, Number maxVal) { - //XYStep step = new XYStep(); + public static Step getStep(StepMode typeXY, double stepValue, Region realBounds, Region pixelBounds) { double stepVal = 0; double stepPix = 0; double stepCount = 0; switch(typeXY) { case INCREMENT_BY_VAL: stepVal = stepValue; - stepPix = stepValue / ValPixConverter.valPerPix(minVal.doubleValue(), maxVal.doubleValue(), plotPixelSize); - stepCount = plotPixelSize /stepPix; + stepPix = stepValue / realBounds.ratio(pixelBounds).doubleValue(); + stepCount = pixelBounds.length().doubleValue() / stepPix; break; case INCREMENT_BY_PIXELS: stepPix = stepValue; - stepCount = plotPixelSize /stepPix; - stepVal = ValPixConverter.valPerPix(minVal.doubleValue(), maxVal.doubleValue(), plotPixelSize)*stepPix; + stepVal = realBounds.ratio(pixelBounds).doubleValue() * stepPix; + stepCount = pixelBounds.length().doubleValue() / stepPix; break; case SUBDIVIDE: stepCount = stepValue; - stepPix = (plotPixelSize /(stepCount-1)); - stepVal = ValPixConverter.valPerPix(minVal.doubleValue(), maxVal.doubleValue(), plotPixelSize)*stepPix; + stepPix = pixelBounds.length().doubleValue() / (stepCount - 1); + stepVal = realBounds.ratio(pixelBounds).doubleValue() * stepPix; break; } return new Step(stepCount, stepPix, stepVal); diff --git a/androidplot-core/src/main/java/com/androidplot/xy/package-info.java b/androidplot-core/src/main/java/com/androidplot/xy/package-info.java new file mode 100644 index 00000000..60a9f91c --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes involved in the creation and presentation of {@link com.androidplot.xy.XYPlot}. + */ +package com.androidplot.xy; \ No newline at end of file diff --git a/androidplot-core/src/test/java/android/net/http/AndroidHttpClient.java b/androidplot-core/src/test/java/android/net/http/AndroidHttpClient.java deleted file mode 100644 index c27de3cf..00000000 --- a/androidplot-core/src/test/java/android/net/http/AndroidHttpClient.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.net.http; - -/** - * Workaround for Robolectric bug: - * https://github.com/robolectric/robolectric/issues/1862 - * we can and should remove this package/class once Robolectric 3.1 is available. - */ -public class AndroidHttpClient {} \ No newline at end of file diff --git a/androidplot-core/src/test/java/com/androidplot/LineRegionTest.java b/androidplot-core/src/test/java/com/androidplot/LineRegionTest.java deleted file mode 100644 index a2415840..00000000 --- a/androidplot-core/src/test/java/com/androidplot/LineRegionTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class LineRegionTest { - - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testConstructor() throws Exception { - LineRegion lr = new LineRegion(0d, 0d); - assertEquals(0d, lr.getMinVal()); - assertEquals(0d, lr.getMaxVal()); - - lr = new LineRegion(1.5d, -2d); - assertEquals(-2d, lr.getMinVal()); - assertEquals(1.5d, lr.getMaxVal()); - - lr = new LineRegion(10d, 20d); - assertEquals(10d, lr.getMinVal()); - assertEquals(20d, lr.getMaxVal()); - } - - - @Test - public void testContains() throws Exception { - - } - - @Test - public void testIntersects() throws Exception { - LineRegion line1 = new LineRegion(1, 10); - LineRegion line2 = new LineRegion(11, 20); - assertFalse(line1.intersects(line2)); - - line1.setMaxVal(15); - assertTrue(line1.intersects(line2)); - - //l1end = 30; - line1.setMaxVal(30); - assertTrue(line1.intersects(line2)); - - //l1start = 21; - line1.setMinVal(21); - assertFalse(line1.intersects(line2)); - } - - @Test - public void testLength() throws Exception { - LineRegion lr = new LineRegion(0, 10); - assertEquals(10d, lr.length().doubleValue(), 0); - - lr = new LineRegion(-5, 5); - assertEquals(10d, lr.length().doubleValue(), 0); - } -} diff --git a/androidplot-core/src/test/java/com/androidplot/PlotTest.java b/androidplot-core/src/test/java/com/androidplot/PlotTest.java index bf492164..18ef38f3 100644 --- a/androidplot-core/src/test/java/com/androidplot/PlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/PlotTest.java @@ -20,10 +20,10 @@ import android.graphics.*; import com.androidplot.exception.PlotRenderException; import com.androidplot.ui.RenderStack; -import com.androidplot.ui.SeriesAndFormatter; import com.androidplot.ui.SeriesRenderer; import com.androidplot.ui.Formatter; -import com.androidplot.util.Configurator; +import com.halfhp.fig.*; + import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -378,7 +378,7 @@ public void testConfigure() throws Exception { params.put("renderMode", param2); params.put("backgroundPaint.color", param3); - Configurator.configure(RuntimeEnvironment.application, plot, params); + Fig.configure(RuntimeEnvironment.application, plot, params); assertEquals(param1, plot.getTitle().getText()); assertEquals(Plot.RenderMode.USE_BACKGROUND_THREAD, plot.getRenderMode()); diff --git a/androidplot-core/src/test/java/com/androidplot/RegionTest.java b/androidplot-core/src/test/java/com/androidplot/RegionTest.java index 27ae4d34..122320ba 100644 --- a/androidplot-core/src/test/java/com/androidplot/RegionTest.java +++ b/androidplot-core/src/test/java/com/androidplot/RegionTest.java @@ -1,57 +1,134 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class RegionTest { - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testContains() throws Exception { - - } - - @Test - public void testIntersects() throws Exception { - LineRegion line1 = new LineRegion(1, 10); - LineRegion line2 = new LineRegion(11, 20); - assertFalse(line1.intersects(line2)); - - line1.setMaxVal(15); - assertTrue(line1.intersects(line2)); - - line1.setMaxVal(30); - assertTrue(line1.intersects(line2)); - - line1.setMinVal(21); - assertFalse(line1.intersects(line2)); - } -} +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class RegionTest { + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testConstructor() throws Exception { + Region lr = new Region(0d, 0d); + assertEquals(0d, lr.getMin()); + assertEquals(0d, lr.getMax()); + + lr = new Region(1.5d, -2d); + assertEquals(-2d, lr.getMin()); + assertEquals(1.5d, lr.getMax()); + + lr = new Region(10d, 20d); + assertEquals(10d, lr.getMin()); + assertEquals(20d, lr.getMax()); + } + + + @Test + public void testContains() throws Exception { + + } + + @Test + public void testIntersects() throws Exception { + Region line1 = new Region(1, 10); + Region line2 = new Region(11, 20); + assertFalse(line1.intersects(line2)); + + line1.setMax(15); + assertTrue(line1.intersects(line2)); + + //l1end = 30; + line1.setMax(30); + assertTrue(line1.intersects(line2)); + + //l1start = 21; + line1.setMin(21); + assertFalse(line1.intersects(line2)); + } + + @Test + public void testLength() throws Exception { + Region lr = new Region(0, 10); + assertEquals(10d, lr.length().doubleValue(), 0); + + lr = new Region(-5, 5); + assertEquals(10d, lr.length().doubleValue(), 0); + } + + @Test + public void testCenter() throws Exception { + Region r1 = new Region(1, 2); + assertEquals(1.5, r1.center()); + + Region r2 = new Region(-10, 10); + assertEquals(0.0, r2.center()); + + Region r3 = new Region(-2, -1); + assertEquals(-1.5, r3.center()); + } + + @Test + public void testTransform() throws Exception { + Region r1 = new Region(1, 2); + Region r2 = new Region(0, 10); + + assertEquals(5.0, r1.transform(1.5, r2)); + assertEquals(0.0, r1.transform(1, r2)); + assertEquals(10.0, r1.transform(2, r2)); + + Region r3 = new Region(-10, 10); + assertEquals(-10.0, r1.transform(1, r3)); + } + + @Test + public void testTransformWithFlip() throws Exception { + Region r1 = new Region(1, 2); + Region r2 = new Region(0, 10); + + assertEquals(5.0, r1.transform(1.5, r2, true)); + assertEquals(10.0, r1.transform(1, r2, true)); + assertEquals(0.0, r1.transform(2, r2, true)); + + Region r3 = new Region(-10, 10); + assertEquals(10.0, r1.transform(1, r3, true)); + } + + @Test + public void testRatio() throws Exception { + Region r1 = new Region(1, 2); + Region r2 = new Region(0, 100); + assertEquals(0.01, r1.ratio(r2)); + assertEquals(100.0, r2.ratio(r1)); + } + +} diff --git a/androidplot-core/src/test/java/com/androidplot/pie/TestPieRenderer.java b/androidplot-core/src/test/java/com/androidplot/pie/TestPieRenderer.java new file mode 100644 index 00000000..14762f3e --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/pie/TestPieRenderer.java @@ -0,0 +1,96 @@ +/* + * 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.pie; + +import android.content.res.*; +import android.graphics.*; +import android.view.*; + +import com.androidplot.*; +import com.androidplot.test.*; +import com.androidplot.ui.*; + +import org.junit.*; +import org.mockito.*; + +import static org.mockito.Mockito.*; + +public class TestPieRenderer extends AndroidplotTest { + + @Mock + LayoutManager layoutManager; + + @Mock + PieChart pieChart; + + @Mock + TypedArray typedArray; + + @Mock + SeriesRegistry seriesRegistry; + + @Before + public void setUp() throws Exception { + //when(xyPlot.getSeriesRegistry()).thenReturn(seriesRegistry); + } + + @After + public void tearDown() throws Exception { + + } + + @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); + + pieRenderer.drawSegment( + new Canvas(), + new RectF(0, 0, 100, 100), + segment, + formatterWithoutTextPaint, + 100, 100, 100); + + SegmentFormatter formatterWithTextPaint = new SegmentFormatter(Color.GREEN); + + pieRenderer.drawSegment( + canvas, + new RectF(0, 0, 100, 100), + segment, + formatterWithTextPaint, + 100, 100, 100); + + verify(pieRenderer, times(0)) + .drawSegmentLabel( + any(Canvas.class), + any(PointF.class), + any(Segment.class), + eq(formatterWithoutTextPaint)); + + verify(pieRenderer, times(1)) + .drawSegmentLabel( + eq(canvas), + any(PointF.class), + eq(segment), + eq(formatterWithTextPaint)); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java b/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java index e2eb9ac6..4648ccd2 100644 --- a/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/test/AndroidplotTest.java @@ -17,17 +17,34 @@ package com.androidplot.test; import android.content.Context; -import org.junit.BeforeClass; + +import com.androidplot.util.*; + +import org.junit.*; import org.junit.runner.RunWith; +import org.mockito.junit.*; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.*; /** * Base class for all Androidplot tests that make use of Android platform classes. */ @RunWith(MyTestRunner.class) +@Config(manifest=Config.NONE) public abstract class AndroidplotTest { + /** + * Enable mockito without requiring usage of {@link org.mockito.runners.MockitoJUnitRunner}. + * See: http://site.mockito.org/mockito/docs/current/org/mockito/junit/MockitoRule.html + */ + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + { + PixelUtils.init(getContext()); + } + /** * Convience method - to access the application context. * @return diff --git a/androidplot-core/src/test/java/com/androidplot/util/ConfiguratorTest.java b/androidplot-core/src/test/java/com/androidplot/util/ConfiguratorTest.java deleted file mode 100644 index c084d1ee..00000000 --- a/androidplot-core/src/test/java/com/androidplot/util/ConfiguratorTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot.util; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import java.lang.reflect.Method; -import static junit.framework.Assert.assertEquals; - -@RunWith(RobolectricTestRunner.class) -public class ConfiguratorTest { - - class A { - int d = 0; - - public int getD() { - return d; - } - - public void setD(int d) { - this.d = d; - } - } - - class B { - A a = new A(); - - public A getA() { - return a; - } - - public void setA(A a) { - this.a = a; - } - } - - class C { - B b = new B(); - - public B getB() { - return b; - } - - public void setB(B a) { - this.b = b; - } - } - - @org.junit.Before - public void setUp() throws Exception { - - } - - @org.junit.After - public void tearDown() throws Exception { - - } - - - @Test - public void testGetFieldAt() throws Exception { - C c = new C(); - assertEquals(c, Configurator.getObjectContaining(c, "b")); - assertEquals(c.getB(), Configurator.getObjectContaining(c, "b.a")); - assertEquals(c.getB().getA(), Configurator.getObjectContaining(c, "b.a.d")); - } - - @Test - public void testGetSetter() throws Exception { - C c = new C(); - - Method m = Configurator.getSetter(c.getClass(), "b"); - assertEquals(1, m.getParameterTypes().length); - assertEquals(B.class, m.getParameterTypes()[0]); - } -} diff --git a/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java b/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java new file mode 100644 index 00000000..3d479495 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/util/LayerHashTest.java @@ -0,0 +1,97 @@ +/* + * 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.util; + +import org.junit.*; + +import static junit.framework.Assert.assertEquals; + +public class LayerHashTest { + + Object obj1 = new Object(); + Object obj2 = new Object(); + Object obj3 = new Object(); + + LayerHash layerHash; + + @Before + public void setUp() throws Exception { + layerHash = new LayerHash<>(); + layerHash.addToBottom(obj1, obj1); + layerHash.addToBottom(obj2, obj2); + layerHash.addToBottom(obj3, obj3); + + assertEquals(obj3, layerHash.getKeysAsList().get(0)); + assertEquals(obj2, layerHash.getKeysAsList().get(1)); + assertEquals(obj1, layerHash.getKeysAsList().get(2)); + } + + // "top" == last element in the list: + @Test + public void testMoveUp() throws Exception { + layerHash.moveUp(obj3); + assertEquals(obj2, layerHash.getKeysAsList().get(0)); + assertEquals(obj3, layerHash.getKeysAsList().get(1)); + assertEquals(obj1, layerHash.getKeysAsList().get(2)); + + layerHash.moveUp(obj3); + assertEquals(obj2, layerHash.getKeysAsList().get(0)); + assertEquals(obj1, layerHash.getKeysAsList().get(1)); + assertEquals(obj3, layerHash.getKeysAsList().get(2)); + + layerHash.moveUp(obj3); + assertEquals(obj2, layerHash.getKeysAsList().get(0)); + assertEquals(obj1, layerHash.getKeysAsList().get(1)); + assertEquals(obj3, layerHash.getKeysAsList().get(2)); + } + + // "bottom" == first element in the list: + @Test + public void testMoveDown() throws Exception { + layerHash.moveDown(obj1); + assertEquals(obj3, layerHash.getKeysAsList().get(0)); + assertEquals(obj1, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + + layerHash.moveDown(obj1); + assertEquals(obj1, layerHash.getKeysAsList().get(0)); + assertEquals(obj3, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + + layerHash.moveDown(obj1); + assertEquals(obj1, layerHash.getKeysAsList().get(0)); + assertEquals(obj3, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + } + + @Test + public void testMoveAbove() throws Exception { + layerHash.moveAbove(obj2, obj1); + assertEquals(obj3, layerHash.getKeysAsList().get(0)); + assertEquals(obj1, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + } + + @Test + public void testMoveBeneath() throws Exception { + layerHash.moveBeneath(obj1, obj2); + assertEquals(obj3, layerHash.getKeysAsList().get(0)); + assertEquals(obj1, layerHash.getKeysAsList().get(1)); + assertEquals(obj2, layerHash.getKeysAsList().get(2)); + } + +} diff --git a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java index 4697dd41..0a4d297f 100644 --- a/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java +++ b/androidplot-core/src/test/java/com/androidplot/util/SeriesUtilsTest.java @@ -16,9 +16,9 @@ package com.androidplot.util; -import com.androidplot.Bounds; -import com.androidplot.xy.SimpleXYSeries; -import com.androidplot.xy.XYBounds; +import com.androidplot.*; +import com.androidplot.xy.*; + import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; @@ -48,7 +48,7 @@ public void tearDown() throws Exception { @Test public void testSeriesMinMax() { SimpleXYSeries series = new SimpleXYSeries(LINEAR, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, null); - XYBounds minMax = SeriesUtils.minMax(series); + RectRegion minMax = SeriesUtils.minMax(series); assertEquals(0, minMax.getMinX()); assertEquals(7, minMax.getMaxX()); assertEquals(1, minMax.getMinY()); @@ -99,7 +99,7 @@ public void testSeriesMinMax() { @Test public void testListMinMax() { - Bounds minMax = SeriesUtils.minMax(LINEAR); + Region minMax = SeriesUtils.minMax(LINEAR); assertEquals(1, minMax.getMin()); assertEquals(8, minMax.getMax()); diff --git a/androidplot-core/src/test/java/com/androidplot/util/ValPixConverterTest.java b/androidplot-core/src/test/java/com/androidplot/util/ValPixConverterTest.java deleted file mode 100644 index 0ed12865..00000000 --- a/androidplot-core/src/test/java/com/androidplot/util/ValPixConverterTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2015 AndroidPlot.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.androidplot.util; - -import org.junit.Test; - -import static junit.framework.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class ValPixConverterTest { - @org.junit.Before - public void setUp() throws Exception { - - } - - @org.junit.After - public void tearDown() throws Exception { - - } - - @org.junit.Test - public void testValToPix() throws Exception { - int sizeInPix = 100; - int min = 0; - int max = 100; - - double value = 50; - - assertEquals(value, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be closer to the top: - // (remember that 0,0 is the top left pixel) - value = 75; - assertEquals(25d, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be at the very top: - value = 100; - assertEquals(0d, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be at the very top: - value = 0; - assertEquals(100d, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be smack in the middle: - min = -100; - value = 0; - assertEquals(50d, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be at the very bottom: - value = 100; - assertEquals(0d, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - // should be in the middle: - value = 0; - assertEquals(50d, ValPixConverter.valToPix(value, min, max, sizeInPix, true)); - - min = -100; - max = 100; - sizeInPix = 200; - assertEquals(0d, ValPixConverter.valToPix(-100, min, max, sizeInPix, false)); - assertEquals(200d, ValPixConverter.valToPix(100, min, max, sizeInPix, false)); - } - - @org.junit.Test - public void testpixToVal() throws Exception { - int sizeInPix = 100; - int min = 0; - int max = 100; - - double value = 50; - - double pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - value = 75; - pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - - min = -100; - value = 50; - pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - min = -100; - value = 60; - pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - min = 20; - value = 50; - pixel = ValPixConverter.valToPix(value, min, max, sizeInPix, true); - assertEquals(value, ValPixConverter.pixToVal(pixel, min, max, sizeInPix, true)); - - try { - ValPixConverter.pixToVal(-5, 0, 0, 0, true); - fail("IllegalArgumentException expected."); - } catch(IllegalArgumentException ex) {} - } - - @Test - public void testValPerPix() { - assertEquals(1.0, ValPixConverter.valPerPix(0, 100, 100)); - double expected = 200d/100; - assertEquals(expected, ValPixConverter.valPerPix(100, 300, 100)); - expected = 50d/100; - assertEquals(expected, ValPixConverter.valPerPix(0, 50, 100)); - expected = 200d/100; - assertEquals(expected, ValPixConverter.valPerPix(-100, 100, 100)); - } -} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java new file mode 100644 index 00000000..8bf3fa74 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/LineAndPointRendererTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2015 AndroidPlot.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.androidplot.xy; + +import android.graphics.*; + +import com.androidplot.test.*; + +import org.junit.*; +import org.junit.runner.*; +import org.mockito.*; + +import java.util.*; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class LineAndPointRendererTest extends AndroidplotTest { + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + /** + * Sanity check to make sure that at the end of the day, points are being drawn at the expected + * screen-coords. + * @throws Exception + */ + @Test + public void testRenderPoints() throws Exception { + + // 100x100 plot space: + RectF plotArea = new RectF(0, 0, 99, 99); + XYPlot plot = new XYPlot(getContext(), "Test"); + + FastLineAndPointRenderer.Formatter formatter = + new FastLineAndPointRenderer.Formatter(Color.RED, Color.RED, null, null); + + // create a series composed of 3 "segments"; series portions separated by null values: + XYSeries series = new SimpleXYSeries( + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "some data", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + LineAndPointRenderer renderer = Mockito.spy(new LineAndPointRenderer(plot)); + + plot.addSeries(series, formatter); + + Canvas canvas = new Canvas(); + plot.calculateMinMaxVals(); + renderer.onRender(canvas, plotArea, series, formatter, null); + + PointF[] expectedPoints = new PointF[] { + new PointF(0, 99), + new PointF(11, 88), + new PointF(22, 77), + new PointF(33, 66), + new PointF(44, 55), + new PointF(55, 44), + new PointF(66, 33), + new PointF(77, 22), + new PointF(88, 11), + new PointF(99, 0) + }; + ArgumentCaptor capturedPoints= ArgumentCaptor.forClass(List.class); + + verify(renderer, times(1)).renderPoints( + eq(canvas), + eq(plotArea), + eq(series), + capturedPoints.capture(), + eq(formatter)); + + List pList = capturedPoints.getValue(); + + // {0, 1} + assertEquals(expectedPoints[0].x, pList.get(0).x); + assertEquals(expectedPoints[0].y, pList.get(0).y); + + // {1, 2} + assertEquals(expectedPoints[1].x, pList.get(1).x); + assertEquals(expectedPoints[1].y, pList.get(1).y); + + // {2, 3} + assertEquals(expectedPoints[2].x, pList.get(2).x); + assertEquals(expectedPoints[2].y, pList.get(2).y); + + // {3, 4} + assertEquals(expectedPoints[3].x, pList.get(3).x); + assertEquals(expectedPoints[3].y, pList.get(3).y); + + // {4, 5} + assertEquals(expectedPoints[4].x, pList.get(4).x); + assertEquals(expectedPoints[4].y, pList.get(4).y); + + // {5, 6} + assertEquals(expectedPoints[5].x, pList.get(5).x); + assertEquals(expectedPoints[5].y, pList.get(5).y); + + // {6, 7} + assertEquals(expectedPoints[6].x, pList.get(6).x); + assertEquals(expectedPoints[6].y, pList.get(6).y); + + // {7, 8} + assertEquals(expectedPoints[7].x, pList.get(7).x); + assertEquals(expectedPoints[7].y, pList.get(7).y); + + // {8, 9} + assertEquals(expectedPoints[8].x, pList.get(8).x); + assertEquals(expectedPoints[8].y, pList.get(8).y); + + // {9, 10} + assertEquals(expectedPoints[9].x, pList.get(9).x); + assertEquals(expectedPoints[9].y, pList.get(9).y); + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java new file mode 100644 index 00000000..c7746e4c --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/PanZoomTest.java @@ -0,0 +1,92 @@ +/* + * 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.content.res.*; +import android.graphics.*; +import android.view.*; + +import com.androidplot.*; +import com.androidplot.test.*; +import com.androidplot.ui.*; + +import org.junit.*; +import org.mockito.*; + +import static org.mockito.Mockito.*; + +public class PanZoomTest extends AndroidplotTest { + + @Mock + LayoutManager layoutManager; + + @Mock + XYPlot xyPlot; + + @Mock + TypedArray typedArray; + + @Mock + SeriesRegistry seriesRegistry; + + @Before + public void setUp() throws Exception { + when(xyPlot.getSeriesRegistry()).thenReturn(seriesRegistry); + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testOnTouch_notifiesOnTouchListener() throws Exception { + PanZoom panZoom = new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE); + + View.OnTouchListener listener = mock(View.OnTouchListener.class); + panZoom.setDelegate(listener); + + MotionEvent motionEvent = mock(MotionEvent.class); + panZoom.onTouch(xyPlot, motionEvent); + + verify(listener, times(1)).onTouch(xyPlot, motionEvent); + } + + @Test + public void testOnTouch_oneFingerMovePansButDoesNotZoom() throws Exception { + PanZoom panZoom = spy(new PanZoom(xyPlot, PanZoom.Pan.BOTH, PanZoom.Zoom.SCALE)); + + View.OnTouchListener listener = mock(View.OnTouchListener.class); + panZoom.setDelegate(listener); + + MotionEvent moveEvent = mock(MotionEvent.class); + + doNothing().when(panZoom).calculatePan( + any(PointF.class), any(PointF.class), anyBoolean()); + + when(moveEvent.getAction()) + .thenReturn(MotionEvent.ACTION_DOWN) + .thenReturn(MotionEvent.ACTION_MOVE); + + panZoom.onTouch(xyPlot, moveEvent); // fires ACTION_DOWN + panZoom.onTouch(xyPlot, moveEvent); // fires ACTION_MOVE + + verify(panZoom, times(1)).pan(moveEvent); + verify(panZoom, times(0)).zoom(moveEvent); + } + +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java index d22762fa..39617f09 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/RectRegionTest.java @@ -16,11 +16,14 @@ package com.androidplot.xy; +import android.graphics.*; + import com.androidplot.test.AndroidplotTest; import org.junit.After; import org.junit.Before; import org.junit.Test; +import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -81,4 +84,40 @@ public void testIsWithin() throws Exception { assertTrue(region1.intersects(region5)); assertTrue(region5.intersects(region1)); } + + @Test + public void testTransform() throws Exception { + RectRegion r1 = new RectRegion(1, 2, 0, 10); + RectRegion r2 = new RectRegion(2, 4, 10, 20); + + final XYCoords trans1 = r1.transform(new XYCoords(1.5, 5), r2); + assertEquals(3.0, trans1.x); + assertEquals(15.0, trans1.y); + + // try a screen transform: + RectF screen = new RectF(0, 0, 100, 100); + PointF result = r1.transform(2, 10, screen, false, true); + assertEquals(100f, result.x); + assertEquals(0f, result.y); + + // now transform back to real: + RectRegion screenRegion = new RectRegion(screen); + XYCoords result2 = screenRegion.transform(result.x, result.y, r1, false, true); + assertEquals(2d, result2.x); + assertEquals(10d, result2.y); + } + + @Test + public void testIntersects() throws Exception { + RectRegion r1 = new RectRegion(0, 10, 0, 10); + RectRegion r2 = new RectRegion(5, 15, 5, 15); + RectRegion r3 = new RectRegion(11, 21, 11, 21); + + assertTrue(r1.intersects(r2)); + assertTrue(r2.intersects(r1)); + assertFalse(r1.intersects(r3)); + assertFalse(r3.intersects(r1)); + assertTrue(r2.intersects(r3)); + assertTrue(r3.intersects(r2)); + } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/StepCalculatorTest.java b/androidplot-core/src/test/java/com/androidplot/xy/StepCalculatorTest.java index 3cc1cef2..91cc0f9c 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/StepCalculatorTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/StepCalculatorTest.java @@ -16,6 +16,8 @@ package com.androidplot.xy; +import com.androidplot.*; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -41,30 +43,35 @@ public void testGetStep() throws Exception { @Test public void testSubdivide() throws Exception { int numSegments = 10; - double plotSize = 100; - double minVal = 0; - double maxVal = 100; - Step step = XYStepCalculator.getStep(StepMode.SUBDIVIDE, - (float) plotSize, numSegments, minVal, maxVal); - assertEquals(plotSize/(numSegments-1), step.getStepPix()); + Region pixBounds = new Region(0, 100); + Region realBounds = new Region(0, 100); + Step step = XYStepCalculator.getStep(StepMode.SUBDIVIDE,numSegments, realBounds, pixBounds); + + assertEquals(pixBounds.length().doubleValue()/(numSegments-1), step.getStepPix()); // make sure large values dont break anything: - minVal = 1000000000; - maxVal = 2000000000; + realBounds.setMin(1000000000); + realBounds.setMax(2000000000); step = XYStepCalculator.getStep(StepMode.SUBDIVIDE, - (float) plotSize, numSegments, minVal, maxVal); + numSegments, realBounds, pixBounds); - assertEquals(plotSize/(numSegments-1), step.getStepPix()); + assertEquals(pixBounds.length().doubleValue()/(numSegments-1), step.getStepPix()); } @Test public void testIncrementByVal() throws Exception { - + Region pixBounds = new Region(50, 150); + Region realBounds = new Region(100, 200); + Step step = XYStepCalculator.getStep(StepMode.INCREMENT_BY_VAL, 1, realBounds, pixBounds); + assertEquals(1.0, step.getStepPix()); } @Test public void testIncrementByPixels() throws Exception { - + Region pixBounds = new Region(50, 150); + Region realBounds = new Region(100, 200); + Step step = XYStepCalculator.getStep(StepMode.INCREMENT_BY_PIXELS, 1, realBounds, pixBounds); + assertEquals(1.0, step.getStepPix()); } } diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java new file mode 100644 index 00000000..d3f70e38 --- /dev/null +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java @@ -0,0 +1,127 @@ +/* + * 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.content.res.*; +import android.graphics.*; + +import com.androidplot.*; +import com.androidplot.test.*; +import com.androidplot.ui.*; + +import org.junit.*; +import org.mockito.*; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyDouble; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class XYGraphWidgetTest extends AndroidplotTest { + + @Mock + LayoutManager layoutManager; + + XYPlot xyPlot; + + @Mock + TypedArray typedArray; + + @Mock + SeriesRegistry seriesRegistry; + + RectRegion bounds = new RectRegion(0, 100, 0, 100); + + @Mock + Canvas canvas; + + + @Before + public void setUp() throws Exception { + xyPlot = spy(new XYPlot(getContext(), "XYPlot")); + when(xyPlot.getSeriesRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getBounds()).thenReturn(bounds); + when(xyPlot.getDomainOrigin()).thenReturn(0); + when(xyPlot.getRangeOrigin()).thenReturn(0); + + xyPlot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1); + xyPlot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1); + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testProcessAttrs() throws Exception { + Size size = new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE); + XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); + + graphWidget.processAttrs(typedArray); + verify(graphWidget, times(1)).setGridClippingEnabled(false); + } + + @Test + public void testDrawMarkers() throws Exception { + Size size = new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE); + XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); + graphWidget.setGridRect(new RectF(0, 0, 100, 100)); + + xyPlot.addMarker(new XValueMarker(1, "x")); + xyPlot.addMarker(new YValueMarker(-1, "y")); + + graphWidget.drawMarkers(canvas); + + // TODO: verifications + verify(canvas, times(2)) + .drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), any(Paint.class)); + } + + @Test + public void testDrawGrid() throws Exception { + Size size = new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE); + XYGraphWidget graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); + graphWidget.setLabelRect(new RectF(0, 0, 100, 100)); + doNothing().when(graphWidget). + drawDomainLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean()); + + doNothing().when(graphWidget). + drawRangeLine(any(Canvas.class), anyFloat(), any(Number.class), any(Paint.class), anyBoolean()); + + graphWidget.setGridRect(new RectF(0, 0, 100, 100)); + xyPlot.setRangeBoundaries(0, 100, BoundaryMode.FIXED); + xyPlot.setDomainBoundaries(0, 100, BoundaryMode.FIXED); + + graphWidget.drawGrid(canvas); + + // expecting a 100x100 grid to be drawn: + verify(graphWidget, times(100)) + .drawDomainLine(eq(canvas), anyFloat(), anyFloat(), any(Paint.class), eq(false)); + + verify(graphWidget, times(100)) + .drawRangeLine(eq(canvas), anyFloat(), anyFloat(), any(Paint.class), eq(false)); + + } +} 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 027e0c8f..01f4ac34 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYPlotTest.java @@ -19,7 +19,8 @@ import android.graphics.*; import com.androidplot.Plot; import com.androidplot.test.AndroidplotTest; -import com.androidplot.util.Configurator; +import com.halfhp.fig.*; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -60,8 +61,8 @@ public void testOriginFixedMode() throws Exception { plot.calculateMinMaxVals(); - assertEquals(3.0, plot.getCalculatedMinX()); - assertEquals(7.0, plot.getCalculatedMaxX()); + assertEquals(3.0, plot.getBounds().getMinX()); + assertEquals(7.0, plot.getBounds().getMaxX()); } @Test @@ -70,14 +71,14 @@ public void testOriginAutoMode() throws Exception { plot.centerOnDomainOrigin(5); plot.calculateMinMaxVals(); - assertEquals(10.0, plot.getCalculatedMaxX()); // symmetry is @ 10, not 9 - assertEquals(0.0, plot.getCalculatedMinX()); + assertEquals(10.0, plot.getBounds().getMaxX()); // symmetry is @ 10, not 9 + assertEquals(0.0, plot.getBounds().getMinX()); plot.centerOnRangeOrigin(50); plot.calculateMinMaxVals(); - assertEquals(100.0, plot.getCalculatedMaxY()); - assertEquals(0.0, plot.getCalculatedMinY()); + assertEquals(100.0, plot.getBounds().getMaxY()); + assertEquals(0.0, plot.getBounds().getMinY()); } @@ -87,22 +88,22 @@ public void testOriginGrowMode() throws Exception { plot.centerOnDomainOrigin(5, null, BoundaryMode.GROW); plot.calculateMinMaxVals(); - assertEquals(0.0, plot.getCalculatedMinX()); - assertEquals(10.0, plot.getCalculatedMaxX()); + assertEquals(0.0, plot.getBounds().getMinX()); + assertEquals(10.0, plot.getBounds().getMaxX()); // introduce a larger domain set. boundaries should change series1.setModel(numList2, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); - assertEquals(-1.0, plot.getCalculatedMinX()); - assertEquals(11.0, plot.getCalculatedMaxX()); + assertEquals(-1.0, plot.getBounds().getMinX()); + assertEquals(11.0, plot.getBounds().getMaxX()); // revert series model back to the previous set. boundaries should remain the same series1.setModel(numList1, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); - assertEquals(-1.0, plot.getCalculatedMinX()); - assertEquals(11.0, plot.getCalculatedMaxX()); + assertEquals(-1.0, plot.getBounds().getMinX()); + assertEquals(11.0, plot.getBounds().getMaxX()); } @Test @@ -111,14 +112,14 @@ public void testOriginShrinkMode() throws Exception { plot.centerOnDomainOrigin(5, null, BoundaryMode.SHRINK); plot.calculateMinMaxVals(); - assertEquals(0.0, plot.getCalculatedMinX()); - assertEquals(10.0, plot.getCalculatedMaxX()); + assertEquals(0.0, plot.getBounds().getMinX()); + assertEquals(10.0, plot.getBounds().getMaxX()); // update with more extreme values...nothing should change in shrink mode: series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); - assertEquals(0.0, plot.getCalculatedMinX()); - assertEquals(10.0, plot.getCalculatedMaxX()); + assertEquals(0.0, plot.getBounds().getMinX()); + assertEquals(10.0, plot.getBounds().getMaxX()); } @@ -130,23 +131,23 @@ public void testsetDomainBoundaries() throws Exception { plot.calculateMinMaxVals(); // default to auto so check them - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(9, plot.getBounds().getMaxX()); plot.setDomainBoundaries(2, BoundaryMode.FIXED, 8, BoundaryMode.FIXED); plot.calculateMinMaxVals(); // fixed - assertEquals(2, plot.getCalculatedMinX()); - assertEquals(8, plot.getCalculatedMaxX()); + assertEquals(2, plot.getBounds().getMinX()); + assertEquals(8, plot.getBounds().getMaxX()); // back to auto plot.setDomainBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); // check again - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(9, plot.getBounds().getMaxX()); // we are not testing MinY well with this dataset. // try grow @@ -154,24 +155,24 @@ public void testsetDomainBoundaries() throws Exception { plot.calculateMinMaxVals(); // check inital - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(9, plot.getBounds().getMaxX()); // update with more extreme values... series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after growing - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(11, plot.getBounds().getMaxX()); // back to previous series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(11, plot.getBounds().getMaxX()); // back to big series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); @@ -180,32 +181,32 @@ public void testsetDomainBoundaries() throws Exception { plot.calculateMinMaxVals(); // check inital - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(11, plot.getBounds().getMaxX()); // now small series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after shrinking - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(9, plot.getBounds().getMaxX()); // back to previous series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(9, plot.getBounds().getMaxX()); // back to auto plot.setDomainBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); // should of changed. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(11, plot.getBounds().getMaxX()); } @Test @@ -214,47 +215,47 @@ public void testsetRangeBoundaries() throws Exception { plot.calculateMinMaxVals(); // default to auto so check them - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY()); + assertEquals(100, plot.getBounds().getMaxY()); plot.setRangeBoundaries(5, BoundaryMode.FIXED, 80, BoundaryMode.FIXED); plot.calculateMinMaxVals(); // fixed - assertEquals(5, plot.getCalculatedMinY()); - assertEquals(80, plot.getCalculatedMaxY()); + assertEquals(5, plot.getBounds().getMinY()); + assertEquals(80, plot.getBounds().getMaxY()); // back to auto plot.setRangeBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); // check again - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY()); + assertEquals(100, plot.getBounds().getMaxY()); // try grow plot.setRangeBoundaries(2, BoundaryMode.GROW, 8, BoundaryMode.GROW); plot.calculateMinMaxVals(); // check inital - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY()); + assertEquals(100, plot.getBounds().getMaxY()); // update with more extreme values... series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after growing - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY()); + assertEquals(200, plot.getBounds().getMaxY()); // back to previous series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY()); + assertEquals(200, plot.getBounds().getMaxY()); // back to big series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); @@ -263,32 +264,32 @@ public void testsetRangeBoundaries() throws Exception { plot.calculateMinMaxVals(); // check inital - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY()); + assertEquals(200, plot.getBounds().getMaxY()); // now small series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // after shrinking - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY()); + assertEquals(100, plot.getBounds().getMaxY()); // back to previous series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // should not of changed. - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY()); + assertEquals(100, plot.getBounds().getMaxY()); // back to auto plot.setRangeBoundaries(2, BoundaryMode.AUTO, 8, BoundaryMode.AUTO); plot.calculateMinMaxVals(); // should of changed. - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY()); + assertEquals(200, plot.getBounds().getMaxY()); } @Test @@ -297,53 +298,53 @@ public void testSetDomainRightMinMax() throws Exception { plot.calculateMinMaxVals(); // default to auto so check them - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(9, plot.getBounds().getMaxX()); plot.setDomainRightMax(10); plot.calculateMinMaxVals(); // same values. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(9, plot.getBounds().getMaxX()); series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on RightMax - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(10, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(10, plot.getBounds().getMaxX()); plot.setDomainRightMax(null); plot.calculateMinMaxVals(); // back to full - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(11, plot.getBounds().getMaxX()); // now the RightMin plot.setDomainRightMin(10); plot.calculateMinMaxVals(); // still to full - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(11, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(11, plot.getBounds().getMaxX()); // small list series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on RightMin - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(10, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(10, plot.getBounds().getMaxX()); // now off again plot.setDomainRightMin(null); plot.calculateMinMaxVals(); // small values. - assertEquals(0, plot.getCalculatedMinX()); - assertEquals(9, plot.getCalculatedMaxX()); + assertEquals(0, plot.getBounds().getMinX()); + assertEquals(9, plot.getBounds().getMaxX()); } @Test @@ -352,31 +353,31 @@ public void testSetRangeTopBottomMinMax() throws Exception { plot.calculateMinMaxVals(); // default to auto so check them - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY()); + assertEquals(100, plot.getBounds().getMaxY()); plot.setRangeTopMax(110); plot.setRangeBottomMin(-50); plot.calculateMinMaxVals(); // same values. - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY()); + assertEquals(100, plot.getBounds().getMaxY()); series1.setModel(numList2,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on Limits - assertEquals(-50, plot.getCalculatedMinY()); - assertEquals(110, plot.getCalculatedMaxY()); + assertEquals(-50, plot.getBounds().getMinY()); + assertEquals(110, plot.getBounds().getMaxY()); plot.setRangeTopMax(null); plot.setRangeBottomMin(null); plot.calculateMinMaxVals(); // back to full - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY()); + assertEquals(200, plot.getBounds().getMaxY()); // now the Min plot.setRangeTopMin(150); @@ -384,16 +385,16 @@ public void testSetRangeTopBottomMinMax() throws Exception { plot.calculateMinMaxVals(); // still to full - assertEquals(-100, plot.getCalculatedMinY()); - assertEquals(200, plot.getCalculatedMaxY()); + assertEquals(-100, plot.getBounds().getMinY()); + assertEquals(200, plot.getBounds().getMaxY()); // small list series1.setModel(numList1,SimpleXYSeries.ArrayFormat.Y_VALS_ONLY); plot.calculateMinMaxVals(); // on Limits - assertEquals(-60, plot.getCalculatedMinY()); - assertEquals(150, plot.getCalculatedMaxY()); + assertEquals(-60, plot.getBounds().getMinY()); + assertEquals(150, plot.getBounds().getMaxY()); // now off again plot.setRangeTopMin(null); @@ -401,8 +402,8 @@ public void testSetRangeTopBottomMinMax() throws Exception { plot.calculateMinMaxVals(); // small values. - assertEquals(0, plot.getCalculatedMinY()); - assertEquals(100, plot.getCalculatedMaxY()); + assertEquals(0, plot.getBounds().getMinY()); + assertEquals(100, plot.getBounds().getMaxY()); } @Test @@ -445,7 +446,7 @@ public void testConfigure() throws Exception { params.put("renderMode", param2); params.put("backgroundPaint.color", param3); - Configurator.configure(RuntimeEnvironment.application, plot, params); + Fig.configure(RuntimeEnvironment.application, plot, params); assertEquals(param1, plot.getTitle().getText()); assertEquals(Plot.RenderMode.USE_BACKGROUND_THREAD, plot.getRenderMode()); assertEquals(Color.parseColor(param3), plot.getBackgroundPaint().getColor()); diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYSeriesRendererTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYSeriesRendererTest.java index 653d47a5..484dde39 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYSeriesRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYSeriesRendererTest.java @@ -16,9 +16,10 @@ package com.androidplot.xy; -import android.graphics.*; +import android.graphics.RectF; + +import com.androidplot.*; import com.androidplot.test.AndroidplotTest; -import com.androidplot.util.ValPixConverter; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -47,22 +48,21 @@ public void testDataToGridCorrelation() throws Exception { plot.setDomainBoundaries(0, 100, BoundaryMode.FIXED); plot.setRangeBoundaries(0, 100, BoundaryMode.FIXED); plot.calculateMinMaxVals(); - Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect, plot.getCalculatedMinX().doubleValue(), plot.getCalculatedMaxX().doubleValue()); + Step domainStep = XYStepCalculator.getStep(plot, Axis.DOMAIN, gridRect); - int x = 0; - double val = ValPixConverter.valToPix(x, 0, 9, gridRect.width(), false); + Region xBounds = new Region(0, 9); - assertEquals(val, domainStep.getStepPix()*x); + int x = 0; + double val = xBounds.transform(x, gridRect.left, gridRect.right, false); + assertEquals(val, (domainStep.getStepPix()*x) + gridRect.left); x = 1; - val = ValPixConverter.valToPix(x, 0, 9, gridRect.width(), false); - - assertEquals(val, domainStep.getStepPix()*x); + val = xBounds.transform(x, gridRect.left, gridRect.right, false); + assertEquals(val, (domainStep.getStepPix()*x) + gridRect.left); x = 9; - val = ValPixConverter.valToPix(x, 0, 9, gridRect.width(), false); - - assertEquals(val, domainStep.getStepPix()*x); + val = xBounds.transform(x, gridRect.left, gridRect.right, false); + assertEquals(val, (domainStep.getStepPix()*x) + gridRect.left); } } diff --git a/build.gradle b/build.gradle index 4b925745..8c295934 100644 --- a/build.gradle +++ b/build.gradle @@ -16,9 +16,10 @@ apply plugin: 'java' -repositories { - mavenCentral() - jcenter() +allprojects { + repositories { + jcenter() + } } ext { @@ -26,8 +27,8 @@ ext { theCompileSdkVersion = 24 theTargetSdkVersion = 24 theMinSdkVersion = 5 - theVersionName = '1.2.1' - theVersionCode = 19 + theVersionName = '1.2.2' + theVersionCode = 20 } buildscript { @@ -37,9 +38,11 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0-beta3' + classpath 'com.android.tools.build:gradle:2.2.0' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' + classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.5.0' + classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.6.3' } } @@ -49,14 +52,6 @@ allprojects { } } -/*subprojects { -// for now can't do this one here because it has to -// be loaded *before* any other plugin in each submodule -// and unless its listed at the top of the submodule plugin list, -// that doesnt seem to happen. - apply plugin: 'android-sdk-manager' -}*/ - task wrapper(type: Wrapper) { gradleVersion = '2.14.1' } \ No newline at end of file diff --git a/circle.yml b/circle.yml index fea51218..a5d95934 100644 --- a/circle.yml +++ b/circle.yml @@ -1,9 +1,15 @@ +machine: + environment: + KEYSTORE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/sigining.keystore + PUBLISHER_ACCT_JSON_FILE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/publisher_profile.json + dependencies: pre: - echo y | android update sdk --no-ui --all --filter "android-24" - echo y | android update sdk --no-ui --all --filter "build-tools-24.0.2" + - bash ./misc/download_keystore.sh test: @@ -36,9 +42,17 @@ test: - mkdir -p $CIRCLE_TEST_REPORTS/lint/ - find . -type f -regex ".*/build/outputs/.*html" -exec cp {} $CIRCLE_TEST_REPORTS/lint/ \; + # code coverage: + - ./gradlew jacocoTestReportDebug + - mkdir -p $CIRCLE_TEST_REPORTS/jacoco/ + - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/jacoco/debug/. $CIRCLE_TEST_REPORTS/jacoco + - bash <(curl -s https://codecov.io/bash) + deployment: master: branch: master commands: - (./gradlew bintrayUpload): timeout: 360 + - bash ./misc/download_google_publisher_json.sh + - ./gradlew publishApkRelease diff --git a/demoapp/build.gradle b/demoapp/build.gradle index 34ccb014..75197453 100644 --- a/demoapp/build.gradle +++ b/demoapp/build.gradle @@ -15,12 +15,24 @@ */ apply plugin: 'com.android.application' +apply plugin: 'com.github.triplet.play' dependencies { compile project(':androidplot-core') compile 'com.crittercism:crittercism-android-agent:5.4.0' } +buildscript { + + repositories { + mavenCentral() + } + + dependencies { + classpath 'com.github.triplet.gradle:play-publisher:1.1.5' + } +} + android { compileSdkVersion theCompileSdkVersion buildToolsVersion theBuildToolsVersion @@ -33,6 +45,27 @@ android { applicationId "com.androidplot.demos" } + signingConfigs { + release { + storeFile file(System.getenv("KEYSTORE") ?: "keystore.jks") + storePassword System.getenv("KEYSTORE_PASSWORD") + keyAlias System.getenv("KEY_ALIAS") + keyPassword System.getenv("KEY_PASSWORD") + } + } + + buildTypes { + release { + zipAlignEnabled true + signingConfig signingConfigs.release + } + + debug { + debuggable true + } + } + + /** * TODO: uncomment this and address all the lint issues. */ @@ -40,3 +73,9 @@ android { abortOnError false } } + +play { + // see: https://github.com/Triple-T/gradle-play-publisher + jsonFile = file(System.getenv("PUBLISHER_ACCT_JSON_FILE") ?: "publisher.json") + track = 'beta' +} diff --git a/demoapp/src/main/AndroidManifest.xml b/demoapp/src/main/AndroidManifest.xml index d24f8094..9d5a9903 100644 --- a/demoapp/src/main/AndroidManifest.xml +++ b/demoapp/src/main/AndroidManifest.xml @@ -97,6 +97,7 @@ + selection; @Override - public void onCreate(Bundle savedInstanceState) - { + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.bar_plot_example); @@ -119,14 +118,12 @@ public void onCreate(Bundle savedInstanceState) Anchor.TOP_MIDDLE); selectionWidget.pack(); - // reduce the number of range labels plot.setLinesPerRangeLabel(3); plot.setRangeLowerBoundary(0, BoundaryMode.FIXED); plot.setLinesPerDomainLabel(2); - // setup checkbox listers: series1CheckBox = (CheckBox) findViewById(R.id.s1CheckBox); series1CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @@ -139,109 +136,128 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { series2CheckBox = (CheckBox) findViewById(R.id.s2CheckBox); series2CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) {onS2CheckBoxClicked(b); + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + onS2CheckBoxClicked(b); } }); plot.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { - if(motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { onPlotClicked(new PointF(motionEvent.getX(), motionEvent.getY())); } return true; } }); - + spRenderStyle = (Spinner) findViewById(R.id.spRenderStyle); - ArrayAdapter adapter = new ArrayAdapter (this, android.R.layout.simple_spinner_item, BarRenderer.BarRenderStyle.values() ); + ArrayAdapter adapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, BarRenderer.Style + .values()); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spRenderStyle.setAdapter(adapter); - spRenderStyle.setSelection(BarRenderer.BarRenderStyle.OVERLAID.ordinal()); + spRenderStyle.setSelection(BarRenderer.Style.OVERLAID.ordinal()); spRenderStyle.setOnItemSelectedListener(new OnItemSelectedListener() { - public void onItemSelected(AdapterView arg0, View arg1,int arg2, long arg3) { + public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { updatePlot(); } - @Override - public void onNothingSelected(AdapterView arg0) { - } - }); - + + @Override + public void onNothingSelected(AdapterView arg0) { + } + }); + spWidthStyle = (Spinner) findViewById(R.id.spWidthStyle); - ArrayAdapter adapter1 = new ArrayAdapter (this, android.R.layout.simple_spinner_item, BarRenderer.BarWidthStyle.values() ); + ArrayAdapter adapter1 = new ArrayAdapter( + this, android.R.layout.simple_spinner_item, BarRenderer.BarWidthMode + .values()); adapter1.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spWidthStyle.setAdapter(adapter1); - spWidthStyle.setSelection(BarRenderer.BarWidthStyle.FIXED_WIDTH.ordinal()); + spWidthStyle.setSelection(BarRenderer.BarWidthMode.FIXED_WIDTH.ordinal()); spWidthStyle.setOnItemSelectedListener(new OnItemSelectedListener() { - public void onItemSelected(AdapterView arg0, View arg1,int arg2, long arg3) { - if (BarRenderer.BarWidthStyle.FIXED_WIDTH.equals(spWidthStyle.getSelectedItem())) { - sbFixedWidth.setVisibility(View.VISIBLE); - sbVariableWidth.setVisibility(View.INVISIBLE); - } else { - sbFixedWidth.setVisibility(View.INVISIBLE); - sbVariableWidth.setVisibility(View.VISIBLE); - } + public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { + if (BarRenderer.BarWidthMode.FIXED_WIDTH.equals(spWidthStyle.getSelectedItem())) { + sbFixedWidth.setVisibility(View.VISIBLE); + sbVariableWidth.setVisibility(View.INVISIBLE); + } else { + sbFixedWidth.setVisibility(View.INVISIBLE); + sbVariableWidth.setVisibility(View.VISIBLE); + } updatePlot(); } - @Override - public void onNothingSelected(AdapterView arg0) { - } - }); + + @Override + public void onNothingSelected(AdapterView arg0) { + } + }); spSeriesSize = (Spinner) findViewById(R.id.spSeriesSize); - ArrayAdapter adapter11 = new ArrayAdapter (this, android.R.layout.simple_spinner_item, SeriesSize.values() ); + ArrayAdapter adapter11 = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, SeriesSize.values()); adapter11.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spSeriesSize.setAdapter(adapter11); spSeriesSize.setSelection(SeriesSize.TEN.ordinal()); spSeriesSize.setOnItemSelectedListener(new OnItemSelectedListener() { - public void onItemSelected(AdapterView arg0, View arg1,int arg2, long arg3) { - switch ((SeriesSize)arg0.getSelectedItem()) { - case TEN: - series1Numbers = series1Numbers10; - series2Numbers = series2Numbers10; - break; - case TWENTY: - series1Numbers = series1Numbers20; - series2Numbers = series2Numbers20; - break; - case SIXTY: - series1Numbers = series1Numbers60; - series2Numbers = series2Numbers60; - break; - default: - break; + public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { + final SeriesSize selectedSize = (SeriesSize) arg0.getSelectedItem(); + switch (selectedSize) { + case TEN: + series1Numbers = series1Numbers10; + series2Numbers = series2Numbers10; + break; + case TWENTY: + series1Numbers = series1Numbers20; + series2Numbers = series2Numbers20; + break; + case SIXTY: + series1Numbers = series1Numbers60; + series2Numbers = series2Numbers60; + break; + default: + break; } - updatePlot(); + updatePlot(selectedSize); + } + + @Override + public void onNothingSelected(AdapterView arg0) { } - @Override - public void onNothingSelected(AdapterView arg0) { - } - }); - - + }); + sbFixedWidth = (SeekBar) findViewById(R.id.sbFixed); sbFixedWidth.setProgress(50); sbFixedWidth.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override - public void onProgressChanged(SeekBar seekBar, int i, boolean b) {updatePlot();} + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + updatePlot(); + } + @Override - public void onStartTrackingTouch(SeekBar seekBar) {} + public void onStartTrackingTouch(SeekBar seekBar) { + } @Override - public void onStopTrackingTouch(SeekBar seekBar) {} + public void onStopTrackingTouch(SeekBar seekBar) { + } }); - - + sbVariableWidth = (SeekBar) findViewById(R.id.sbVariable); sbVariableWidth.setProgress(1); sbVariableWidth.setVisibility(View.INVISIBLE); sbVariableWidth.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override - public void onProgressChanged(SeekBar seekBar, int i, boolean b) {updatePlot();} + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + updatePlot(); + } + @Override - public void onStartTrackingTouch(SeekBar seekBar) {} + public void onStartTrackingTouch(SeekBar seekBar) { + } + @Override - public void onStopTrackingTouch(SeekBar seekBar) {} + public void onStopTrackingTouch(SeekBar seekBar) { + } }); plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM). @@ -272,35 +288,60 @@ public Number parse(String string, ParsePosition position) { } private void updatePlot() { - - // Remove all current series from each plot + updatePlot(null); + } + + private void updatePlot(SeriesSize seriesSize) { + + // Remove all current series from each plot plot.clear(); // Setup our Series with the selected number of elements - series1 = new SimpleXYSeries(Arrays.asList(series1Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Us"); - series2 = new SimpleXYSeries(Arrays.asList(series2Numbers), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Them"); + series1 = new SimpleXYSeries(Arrays.asList(series1Numbers), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Us"); + series2 = new SimpleXYSeries(Arrays.asList(series2Numbers), + SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Them"); + + plot.setDomainBoundaries(-1, series1.size(), BoundaryMode.FIXED); + plot.setRangeUpperBoundary( + SeriesUtils.minMax(series1, series2). + getMaxY().doubleValue() + 1, BoundaryMode.FIXED); + + if(seriesSize != null) { + switch(seriesSize) { + case TEN: + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 2); + break; + case TWENTY: + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 4); + break; + case SIXTY: + plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 6); + break; + } + } // add a new series' to the xyplot: if (series1CheckBox.isChecked()) plot.addSeries(series1, formatter1); - if (series2CheckBox.isChecked()) plot.addSeries(series2, formatter2); + if (series2CheckBox.isChecked()) plot.addSeries(series2, formatter2); // Setup the BarRenderer with our selected options - MyBarRenderer renderer = ((MyBarRenderer)plot.getRenderer(MyBarRenderer.class)); - renderer.setBarRenderStyle((BarRenderer.BarRenderStyle)spRenderStyle.getSelectedItem()); - renderer.setBarWidthStyle((BarRenderer.BarWidthStyle)spWidthStyle.getSelectedItem()); + 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.BarRenderStyle.STACKED.equals(spRenderStyle.getSelectedItem())) { - plot.setRangeTopMin(15); + + if (BarRenderer.Style.STACKED.equals(spRenderStyle.getSelectedItem())) { + plot.setRangeTopMin(15); } else { - plot.setRangeTopMin(0); + plot.setRangeTopMin(0); } - + plot.redraw(); - - } - + + } + private void onPlotClicked(PointF point) { // make sure the point lies within the graph area. we use gridrect @@ -309,23 +350,22 @@ private void onPlotClicked(PointF point) { Number x = plot.getXVal(point); Number y = plot.getYVal(point); - selection = null; double xDistance = 0; double yDistance = 0; - // find the closest value to the selection: - for (SeriesAndFormatter sfPair : plot.getSeriesRegistry()) { + for (SeriesAndFormatter sfPair : plot + .getSeriesRegistry()) { XYSeries series = sfPair.getSeries(); for (int i = 0; i < series.size(); i++) { Number thisX = series.getX(i); Number thisY = series.getY(i); if (thisX != null && thisY != null) { double thisXDistance = - LineRegion.measure(x, thisX).doubleValue(); + Region.measure(x, thisX).doubleValue(); double thisYDistance = - LineRegion.measure(y, thisY).doubleValue(); + Region.measure(y, thisY).doubleValue(); if (selection == null) { selection = new Pair<>(i, series); xDistance = thisXDistance; @@ -350,7 +390,7 @@ private void onPlotClicked(PointF point) { selection = null; } - if(selection == null) { + if (selection == null) { selectionWidget.setText(NO_SELECTION_TXT); } else { selectionWidget.setText("Selected: " + selection.second.getTitle() + @@ -370,7 +410,7 @@ private void onS1CheckBoxClicked(boolean checked) { private void onS2CheckBoxClicked(boolean checked) { if (checked) { - plot.addSeries(series2, formatter2); + plot.addSeries(series2, formatter2); } else { plot.removeSeries(series2); } @@ -378,6 +418,7 @@ private void onS2CheckBoxClicked(boolean checked) { } class MyBarFormatter extends BarFormatter { + public MyBarFormatter(int fillColor, int borderColor) { super(fillColor, borderColor); } @@ -407,9 +448,9 @@ public MyBarRenderer(XYPlot plot) { * @return */ @Override - public MyBarFormatter getFormatter(int index, XYSeries series) { - if(selection != null && - selection.second == series && + public MyBarFormatter getFormatter(int index, XYSeries series) { + if (selection != null && + selection.second == series && selection.first == index) { return selectionFormatter; } else { diff --git a/demoapp/src/main/java/com/androidplot/demos/BubbleChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/BubbleChartActivity.java new file mode 100644 index 00000000..0ca7537a --- /dev/null +++ b/demoapp/src/main/java/com/androidplot/demos/BubbleChartActivity.java @@ -0,0 +1,71 @@ +/* + * 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.app.*; +import android.graphics.*; +import android.os.*; + +import com.androidplot.xy.*; + +import java.util.*; + +/** + * An example of a bubble chart. + */ +public class BubbleChartActivity extends Activity { + + private XYPlot plot; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.bubble_chart_example); + + // initialize our XYPlot reference: + plot = (XYPlot) findViewById(R.id.plot); + + // turn the above arrays into XYSeries': + // (Y_VALS_ONLY means use the element index as the x value) + BubbleSeries series1 = new BubbleSeries( + Arrays.asList(new Number[]{3, 5, 2, 3, 6}), + Arrays.asList(new Number[]{1, 5, 2, 2, 3}), "s1"); + + BubbleSeries series2 = new BubbleSeries( + Arrays.asList(new Number[]{2, 7, 3, 1, 3}), + Arrays.asList(new Number[]{2, 1, 2, 6, 7}), "s2"); + + BubbleSeries series3 = new BubbleSeries( + Arrays.asList(new Number[]{7, 2, 5, 6, 5}), + Arrays.asList(new Number[]{3, 2, 4, 6, 7}), "s3"); + + plot.setDomainBoundaries(-1, 5, BoundaryMode.FIXED); + plot.setRangeBoundaries(0, 8, BoundaryMode.FIXED); + + BubbleFormatter bf1 = new BubbleFormatter(this, R.xml.bubble_formatter1); + bf1.setPointLabelFormatter(new PointLabelFormatter(Color.BLACK)); + bf1.getPointLabelFormatter().getTextPaint().setTextAlign(Paint.Align.CENTER); + bf1.getPointLabelFormatter().getTextPaint().setFakeBoldText(true); + + // add series to the xyplot: + plot.addSeries(series1, bf1); + plot.addSeries(series2, new BubbleFormatter(this, R.xml.bubble_formatter2)); + plot.addSeries(series3, new BubbleFormatter(this, R.xml.bubble_formatter3)); + PanZoom.attach(plot); + } +} diff --git a/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java index e9c93ff1..432b16ed 100644 --- a/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/CandlestickChartActivity.java @@ -22,16 +22,14 @@ import android.graphics.Paint; import android.os.Bundle; +import com.androidplot.*; +import com.androidplot.util.*; import com.androidplot.xy.CandlestickFormatter; import com.androidplot.xy.CandlestickMaker; import com.androidplot.xy.CandlestickSeries; -import com.androidplot.util.PixelUtils; import com.androidplot.xy.*; -import java.text.DecimalFormat; -import java.text.FieldPosition; -import java.text.Format; -import java.text.ParsePosition; +import java.text.*; /** * A simple example of a candlestick chart rendered on an {@link XYPlot}. @@ -40,6 +38,8 @@ public class CandlestickChartActivity extends Activity { private XYPlot plot; + private DecimalFormat currencyFormat = new DecimalFormat("$0.00"); + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -48,13 +48,13 @@ public void onCreate(Bundle savedInstanceState) { // initialize our XYPlot reference: plot = (XYPlot) findViewById(R.id.plot); - CandlestickSeries candlestickSeries = new CandlestickSeries( - new CandlestickSeries.Item(1, 10, 2, 9), - new CandlestickSeries.Item(4, 18, 6, 5), - new CandlestickSeries.Item(3, 11, 5, 10), - new CandlestickSeries.Item(2, 17, 2, 15), - new CandlestickSeries.Item(6, 11, 11, 7), - new CandlestickSeries.Item(8, 16, 10, 15)); + final CandlestickSeries candlestickSeries = new CandlestickSeries( + new CandlestickSeries.Item(1, 10, 2, 9.04), + new CandlestickSeries.Item(4, 18, 6, 5.50), + new CandlestickSeries.Item(3, 11, 5, 9.21), + new CandlestickSeries.Item(2, 17, 2, 15.25), + new CandlestickSeries.Item(6, 11, 11, 7.12), + new CandlestickSeries.Item(8, 16, 10, 15.02)); // draw a simple line plot of the close vals: LineAndPointFormatter lpf = new LineAndPointFormatter(Color.BLACK, Color.BLACK, null, null); @@ -66,17 +66,12 @@ public void onCreate(Bundle savedInstanceState) { plot.addSeries(candlestickSeries.getCloseSeries(), lpf); - CandlestickFormatter formatter = new CandlestickFormatter(); - Paint p = formatter.getWickPaint(); - p.setColor(Color.BLACK); - formatter.setCapAndWickPaint(p); - formatter.setRisingBodyStrokePaint(p); - formatter.setFallingBodyStrokePaint(p); + CandlestickFormatter formatter = new CandlestickFormatter(this, R.xml.candlestick_formatter); // draw candlestick bodies as triangles instead of squares: // triangles will point up for items that closed higher than they opened // and down for those that closed lower: - formatter.setBodyStyle(CandlestickFormatter.BodyStyle.Square); + formatter.setBodyStyle(CandlestickFormatter.BodyStyle.SQUARE); // add the candlestick series data to the plot: CandlestickMaker.make(plot, formatter, candlestickSeries); @@ -85,7 +80,18 @@ public void onCreate(Bundle savedInstanceState) { plot.setLinesPerRangeLabel(3); plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT). - setFormat(new DecimalFormat("$0.00")); + setFormat(currencyFormat); + + // add some padding to range boundaries: + final Region minMax = SeriesUtils.minMax( + candlestickSeries.getHighSeries().getyVals(), + candlestickSeries.getLowSeries().getyVals()); + + plot.setRangeBoundaries( + minMax.getMin().doubleValue() - 1, + minMax.getMax().doubleValue() + 1, + BoundaryMode.FIXED); + // setup the domain tick label formatting, etc: plot.setDomainBoundaries(-1, 6, BoundaryMode.FIXED); @@ -128,5 +134,21 @@ public Object parseObject(String string, ParsePosition position) { return null; } }); + + formatter.setPointLabelFormatter( + new PointLabelFormatter(Color.BLACK, PixelUtils.dpToPix(8), 0)); + formatter.getPointLabelFormatter().getTextPaint().setFakeBoldText(true); + formatter.getPointLabelFormatter().getTextPaint().setTextAlign(Paint.Align.LEFT); + + // add labels for close vals: + formatter.setPointLabeler(new PointLabeler() { + @Override + public String getLabel(XYSeries series, int index) { + if(series == candlestickSeries.getCloseSeries()) { + return currencyFormat.format(series.getY(index).doubleValue()); + } + return null; + } + }); } } diff --git a/demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java index 5a9ca504..19b01cdb 100644 --- a/demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/FXPlotExampleActivity.java @@ -87,9 +87,8 @@ public void onCreate(Bundle savedInstanceState) { // create formatters to use for drawing a series using LineAndPointRenderer // and configure them from xml: - LineAndPointFormatter series1Format = new LineAndPointFormatter(); - series1Format.configure(getApplicationContext(), - R.xml.line_point_formatter); + LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter); // use our custom renderer to make origin labels red plot.getGraph().setLineLabelRenderer(XYGraphWidget.Edge.BOTTOM, new MyLineLabelRenderer()); diff --git a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java b/demoapp/src/main/java/com/androidplot/demos/MainActivity.java index 2d0997fa..71ef659e 100644 --- a/demoapp/src/main/java/com/androidplot/demos/MainActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/MainActivity.java @@ -164,5 +164,14 @@ public void onClick(View view) { startActivity(new Intent(MainActivity.this, FXPlotExampleActivity.class)); } }); + + // bubble chart + Button bubbleChartButton = (Button) findViewById(R.id.bubbleChartExample); + bubbleChartButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(new Intent(MainActivity.this, BubbleChartActivity.class)); + } + }); } } diff --git a/demoapp/src/main/java/com/androidplot/demos/ScatterPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/ScatterPlotActivity.java index 0056d817..def16c32 100644 --- a/demoapp/src/main/java/com/androidplot/demos/ScatterPlotActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/ScatterPlotActivity.java @@ -45,13 +45,11 @@ public void onCreate(Bundle savedInstanceState) // create formatters to use for drawing a series using LineAndPointRenderer // and configure them from xml: - LineAndPointFormatter series1Format = new LineAndPointFormatter(); - series1Format.configure(getApplicationContext(), - R.xml.point_formatter); + LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.point_formatter); - LineAndPointFormatter series2Format = new LineAndPointFormatter(); - series2Format.configure(getApplicationContext(), - R.xml.point_formatter_2); + LineAndPointFormatter series2Format = + new LineAndPointFormatter(this, R.xml.point_formatter_2); // add each series to the xyplot: plot.addSeries(series1, series1Format); @@ -59,9 +57,6 @@ public void onCreate(Bundle savedInstanceState) // reduce the number of range labels plot.setLinesPerRangeLabel(3); - - // rotate domain labels 45 degrees to make them more compact horizontally: - //plot.getGraphWidget().setLineLabelRotation(XYGraphWidget.Edge.BOTTOM, -45f); } /** diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java index cdf0e5c2..fb18f7af 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java @@ -126,23 +126,19 @@ public void onStopTrackingTouch(SeekBar seekBar) { EmbossMaskFilter emf = new EmbossMaskFilter( new float[]{1, 1, 1}, 0.4f, 10, 8.2f); - SegmentFormatter sf1 = new SegmentFormatter(); - sf1.configure(getApplicationContext(), R.xml.pie_segment_formatter1); + SegmentFormatter sf1 = new SegmentFormatter(this, R.xml.pie_segment_formatter1); sf1.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf1.getFillPaint().setMaskFilter(emf); - SegmentFormatter sf2 = new SegmentFormatter(); - sf2.configure(getApplicationContext(), R.xml.pie_segment_formatter2); + SegmentFormatter sf2 = new SegmentFormatter(this, R.xml.pie_segment_formatter2); sf2.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf2.getFillPaint().setMaskFilter(emf); - SegmentFormatter sf3 = new SegmentFormatter(); - sf3.configure(getApplicationContext(), R.xml.pie_segment_formatter3); + SegmentFormatter sf3 = new SegmentFormatter(this, R.xml.pie_segment_formatter3); sf3.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf3.getFillPaint().setMaskFilter(emf); - SegmentFormatter sf4 = new SegmentFormatter(); - sf4.configure(getApplicationContext(), R.xml.pie_segment_formatter4); + SegmentFormatter sf4 = new SegmentFormatter(this, R.xml.pie_segment_formatter4); sf4.getLabelPaint().setShadowLayer(3, 0, 0, Color.BLACK); sf4.getFillPaint().setMaskFilter(emf); diff --git a/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java index 9b0cebd3..904f02c7 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimpleXYPlotActivity.java @@ -16,8 +16,9 @@ package com.androidplot.demos; import android.app.Activity; -import android.graphics.DashPathEffect; +import android.graphics.*; import android.os.Bundle; + import com.androidplot.util.PixelUtils; import com.androidplot.xy.SimpleXYSeries; import com.androidplot.xy.XYSeries; @@ -58,23 +59,18 @@ public void onCreate(Bundle savedInstanceState) // create formatters to use for drawing a series using LineAndPointRenderer // and configure them from xml: - LineAndPointFormatter series1Format = new LineAndPointFormatter(); - series1Format.setPointLabelFormatter(new PointLabelFormatter()); - series1Format.configure(getApplicationContext(), - R.xml.line_point_formatter_with_labels); + LineAndPointFormatter series1Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter_with_labels); - LineAndPointFormatter series2Format = new LineAndPointFormatter(); - series2Format.setPointLabelFormatter(new PointLabelFormatter()); - series2Format.configure(getApplicationContext(), - R.xml.line_point_formatter_with_labels_2); + LineAndPointFormatter series2Format = + new LineAndPointFormatter(this, R.xml.line_point_formatter_with_labels_2); // add an "dash" effect to the series2 line: - series2Format.getLinePaint().setPathEffect( - new DashPathEffect(new float[] { + series2Format.getLinePaint().setPathEffect(new DashPathEffect(new float[] { - // always use DP when specifying pixel sizes, to keep things consistent across devices: - PixelUtils.dpToPix(20), - PixelUtils.dpToPix(15)}, 0)); + // always use DP when specifying pixel sizes, to keep things consistent across devices: + PixelUtils.dpToPix(20), + PixelUtils.dpToPix(15)}, 0)); // just for fun, add some smoothing to the lines: // see: http://androidplot.com/smooth-curves-and-androidplot/ diff --git a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java index 541eb799..ef9821cc 100644 --- a/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/TouchZoomExampleActivity.java @@ -104,10 +104,9 @@ public void onClick(View view) { // record min/max for the reset button: plot.calculateMinMaxVals(); - minXY = new PointF(plot.getCalculatedMinX().floatValue(), - plot.getCalculatedMinY().floatValue()); - maxXY = new PointF(plot.getCalculatedMaxX().floatValue(), - plot.getCalculatedMaxY().floatValue()); + final RectRegion bounds = plot.getBounds(); + minXY = new PointF(bounds.getMinX().floatValue(), bounds.getMinY().floatValue()); + maxXY = new PointF(bounds.getMaxX().floatValue(), bounds.getMaxY().floatValue()); // enable pan/zoom behavior: panZoom = PanZoom.attach(plot); diff --git a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java index 921884dc..50266b62 100644 --- a/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/XYRegionExampleActivity.java @@ -40,22 +40,16 @@ public class XYRegionExampleActivity extends Activity { private static final float HOME_RUN_DIST = 325; - private static final int LINE_THICKNESS_DP = 2; - private static final int POINT_SIZE_DP = 6; + private static final int LINE_THICKNESS_DP = 4; + private static final int POINT_SIZE_DP = 7; private XYPlot plot; - private final Number[] timHits = {105, 252, 220, 350, 12, 250, 353}; - private final Number[] nickHits = {110, 191, 61, 371, 289, 101, 10}; - private final Number[] joeHits = {25, 375, 364, 128, 178, 289, 346}; - private final Number[] jamesHits = {250, 285, 295, 211, 311, 365, 241}; + private final Number[] timHits = {105, 252, 216, 10, 301, 320, 220, 350, 12, 250, 353}; + private final Number[] nickHits = {110, 191, 61, 300, 205, 40, 224, 371, 289, 101, 10}; private LineAndPointFormatter timFormatter; private LineAndPointFormatter nickFormatter; - private LineAndPointFormatter joeFormatter; - private LineAndPointFormatter jamesFormatter; private XYSeries timSeries; private XYSeries nickSeries; - private XYSeries joeSeries; - private XYSeries jamesSeries; private RectRegion shortRegion; private RectRegion warmupRegion; @@ -67,8 +61,6 @@ public class XYRegionExampleActivity extends Activity { private CheckBox timCB; private CheckBox nickCB; - private CheckBox joeCB; - private CheckBox jamesCB; private CheckBox r2CheckBox; private CheckBox r3CheckBox; @@ -94,22 +86,6 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { } }); - joeCB = (CheckBox) findViewById(R.id.s3CheckBox); - joeCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - onS3CheckBoxClicked(); - } - }); - - jamesCB = (CheckBox) findViewById(R.id.s4CheckBox); - jamesCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - onS4CheckBoxClicked(); - } - }); - r2CheckBox = (CheckBox) findViewById(R.id.r2CheckBox); r2CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override @@ -165,24 +141,6 @@ private void onS2CheckBoxClicked() { plot.redraw(); } - private void onS3CheckBoxClicked() { - if (joeCB.isChecked()) { - plot.addSeries(joeSeries, joeFormatter); - } else { - plot.removeSeries(joeSeries); - } - plot.redraw(); - } - - private void onS4CheckBoxClicked() { - if (jamesCB.isChecked()) { - plot.addSeries(jamesSeries, jamesFormatter); - } else { - plot.removeSeries(jamesSeries); - } - plot.redraw(); - } - /** * Processes a check box event * @param cb The checkbox event origin @@ -228,6 +186,8 @@ public Number parse(String string, ParsePosition position) { plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.BOTTOM) .setFormat(new DecimalFormat("#")); + plot.getGraph().setDomainGridLinePaint(null); + plot.getLegend().setWidth(PixelUtils.dpToPix(100), SizeMode.FILL); // reposition the grid so that it rests above the bottom-left @@ -249,12 +209,11 @@ public Number parse(String string, ParsePosition position) { private void seriesSetup() { // TIM - timFormatter = new LineAndPointFormatter( - Color.rgb(100, 25, 20), - Color.rgb(100, 25, 20), - null, null); + timFormatter = new LineAndPointFormatter(Color.YELLOW, Color.YELLOW, null, null); timFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(LINE_THICKNESS_DP)); timFormatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(POINT_SIZE_DP)); + timFormatter.setInterpolationParams(new CatmullRomInterpolator.Params(8, + CatmullRomInterpolator.Type.Centripetal)); timSeries = new SimpleXYSeries(Arrays.asList(timHits), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Tim"); @@ -262,44 +221,17 @@ private void seriesSetup() { plot.addSeries(timSeries, timFormatter); // SERIES #2: - nickFormatter = new LineAndPointFormatter( - Color.rgb(100, 25, 200), - Color.rgb(100, 25, 200), - null, null); + nickFormatter = new LineAndPointFormatter(Color.BLUE, Color.BLUE, null, null); nickFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(LINE_THICKNESS_DP)); nickFormatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(POINT_SIZE_DP)); + nickFormatter.setInterpolationParams(new CatmullRomInterpolator.Params(8, + CatmullRomInterpolator.Type.Centripetal)); nickSeries = new SimpleXYSeries(Arrays.asList(nickHits), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Nick"); plot.addSeries(nickSeries, nickFormatter); - // SERIES #3: - joeFormatter = new LineAndPointFormatter( - Color.rgb(200, 25, 200), - Color.rgb(200, 25, 200), - null, null); - joeFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(LINE_THICKNESS_DP)); - joeFormatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(POINT_SIZE_DP)); - - joeSeries = new SimpleXYSeries(Arrays.asList(joeHits), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Joe"); - - plot.addSeries(joeSeries, joeFormatter); - - // SERIES #4: - jamesFormatter = new LineAndPointFormatter( - Color.rgb(220, 25, 20), - Color.rgb(220, 25, 20), - null, null); - - jamesFormatter.getLinePaint().setStrokeWidth(PixelUtils.dpToPix(LINE_THICKNESS_DP)); - jamesFormatter.getVertexPaint().setStrokeWidth(PixelUtils.dpToPix(POINT_SIZE_DP)); - - jamesSeries = new SimpleXYSeries(Arrays.asList(jamesHits), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "James"); - plot.addSeries(jamesSeries, jamesFormatter); - plot.setRangeStep(StepMode.INCREMENT_BY_VAL, 100); plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 1); } @@ -366,36 +298,31 @@ private void regionSetup() { // and another region: shortRegionFormatter = new XYRegionFormatter(Color.RED); - shortRegionFormatter.getPaint().setAlpha(75); - shortRegion = new RectRegion(2, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, - HOME_RUN_DIST, "Short"); + shortRegionFormatter.getPaint().setAlpha(60); + shortRegion = new RectRegion( + 2, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, HOME_RUN_DIST, "Short"); + timFormatter.addRegion(shortRegion, shortRegionFormatter); nickFormatter.addRegion(shortRegion, shortRegionFormatter); - joeFormatter.addRegion(shortRegion, shortRegionFormatter); - jamesFormatter.addRegion(shortRegion, shortRegionFormatter); // the next three regions are horizontal regions with minY/maxY // set to negative and positive infinity respectively. - warmupRegionFormatter = new XYRegionFormatter(Color.WHITE); - warmupRegionFormatter.getPaint().setAlpha(75); + warmupRegionFormatter = new XYRegionFormatter(Color.LTGRAY); + warmupRegionFormatter.getPaint().setAlpha(60); + + warmupRegion = new RectRegion( + 0, 2, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, "Warmup"); - warmupRegion = new RectRegion(0, 2, Double.NEGATIVE_INFINITY, - Double.POSITIVE_INFINITY, "Warmup"); timFormatter.addRegion(warmupRegion, warmupRegionFormatter); nickFormatter.addRegion(warmupRegion, warmupRegionFormatter); - joeFormatter.addRegion(warmupRegion, warmupRegionFormatter); - jamesFormatter.addRegion(warmupRegion, warmupRegionFormatter); homeRunRegionFormatter = new XYRegionFormatter(Color.GREEN); - homeRunRegionFormatter.getPaint().setAlpha(75); + homeRunRegionFormatter.getPaint().setAlpha(60); - homeRunRegion = new RectRegion(2, Double.POSITIVE_INFINITY, HOME_RUN_DIST, - Double.POSITIVE_INFINITY, "H. Run"); + homeRunRegion = new RectRegion( + 2, Double.POSITIVE_INFINITY, HOME_RUN_DIST, Double.POSITIVE_INFINITY, "H. Run"); timFormatter.addRegion(homeRunRegion, homeRunRegionFormatter); nickFormatter.addRegion(homeRunRegion, homeRunRegionFormatter); - joeFormatter.addRegion(homeRunRegion, homeRunRegionFormatter); - jamesFormatter.addRegion(homeRunRegion, homeRunRegionFormatter); - nickFormatter.setFillDirection(FillDirection.RANGE_ORIGIN); } } \ No newline at end of file diff --git a/demoapp/src/main/res/layout/bubble_chart_example.xml b/demoapp/src/main/res/layout/bubble_chart_example.xml new file mode 100644 index 00000000..e18289aa --- /dev/null +++ b/demoapp/src/main/res/layout/bubble_chart_example.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/demoapp/src/main/res/layout/main.xml b/demoapp/src/main/res/layout/main.xml index e31298a5..7295e172 100644 --- a/demoapp/src/main/res/layout/main.xml +++ b/demoapp/src/main/res/layout/main.xml @@ -111,6 +111,12 @@ style="@style/toc_button" android:text="f(x) Example" android:enabled="true"/> + +