From 57da176c98745be76f46c9bd1d9a7e0cd3143258 Mon Sep 17 00:00:00 2001 From: Jimmy Shih Date: Fri, 18 May 2012 14:49:49 -0700 Subject: [PATCH] Improve ChartValueSeries --- .../apps/mytracks/ChartValueSeries.java | 354 +++++++++--------- .../android/apps/mytracks/ChartView.java | 14 +- .../apps/mytracks/ChartValueSeriesTest.java | 93 ++--- 3 files changed, 203 insertions(+), 258 deletions(-) diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java index 7a054fe6f..58650580d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java @@ -13,9 +13,11 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.stats.ExtremityMonitor; +import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.graphics.Canvas; @@ -26,122 +28,118 @@ import android.graphics.Path; import java.text.NumberFormat; /** - * This class encapsulates meta data about one series of values for a chart. + * This class encapsulates the meta data for one series of chart values. * * @author Sandor Dornbush */ public class ChartValueSeries { - private final ExtremityMonitor monitor = new ExtremityMonitor(); - private final NumberFormat format; - private final Path path = new Path(); + private static final float STROKE_WIDTH = 2f; + + private final ExtremityMonitor extremityMonitor; + private final NumberFormat numberFormat; + private final Path path; private final Paint fillPaint; private final Paint strokePaint; private final Paint labelPaint; - private final ZoomSettings zoomSettings; - + private final YAxisDimension markingInterval; private String title; - private double min; - private double max = 1.0; - private int effectiveMax; - private int effectiveMin; - private double spread; - private int interval; + private boolean enabled = true; + private int interval = 1; + private int effectiveMin = 0; + private int effectiveMax = 1; /** - * This class controls how effective min/max values of a {@link ChartValueSeries} are calculated. + * This class to calculates the y axis dimension, interval, effective min, and + * effective max. */ - public static class ZoomSettings { - private int intervals; + public static class YAxisDimension { + private final int numberOfIntervals; private final int absoluteMin; private final int absoluteMax; - private final int[] zoomLevels; + private final int[] intervalValues; - public ZoomSettings(int intervals, int[] zoomLevels) { - this.intervals = intervals; - this.absoluteMin = Integer.MAX_VALUE; - this.absoluteMax = Integer.MIN_VALUE; - this.zoomLevels = zoomLevels; - checkArgs(); - } - - public ZoomSettings(int intervals, int absoluteMin, int absoluteMax, int[] zoomLevels) { - this.intervals = intervals; + /** + * Constructor. + * + * @param numberOfIntervals the number of intervals + * @param absoluteMin the absolute minimum value + * @param absoluteMax the absolute maximum value + * @param intervalValues the list of interval values + */ + public YAxisDimension( + int numberOfIntervals, int absoluteMin, int absoluteMax, int[] intervalValues) { + this.numberOfIntervals = numberOfIntervals; this.absoluteMin = absoluteMin; this.absoluteMax = absoluteMax; - this.zoomLevels = zoomLevels; - checkArgs(); - } - - private void checkArgs() { - if (intervals <= 0 || zoomLevels == null || zoomLevels.length == 0) { - throw new IllegalArgumentException("Expecing positive intervals and non-empty zoom levels"); - } - for (int i = 1; i < zoomLevels.length; ++i) { - if (zoomLevels[i] <= zoomLevels[i - 1]) { - throw new IllegalArgumentException("Expecting zoom levels in ascending order"); - } - } - } - - public int getIntervals() { - return intervals; - } - - public int getAbsoluteMin() { - return absoluteMin; - } - - public int getAbsoluteMax() { - return absoluteMax; - } - - public int[] getZoomLevels() { - return zoomLevels; + this.intervalValues = intervalValues; } /** - * Calculates the interval between markings given the min and max values. - * This function attempts to find the smallest zoom level that fits [min,max] after rounding - * it to the current zoom level. - * - * @param min the minimum value in the series - * @param max the maximum value in the series - * @return the calculated interval for the given range + * Gets the y axis dimension. Returns an array of int[3], containing interval, effectiveMin, + * and effectiveMax. + * + * @param min the min value + * @param max the max value */ - public int calculateInterval(double min, double max) { - min = Math.min(min, absoluteMin); - max = Math.max(max, absoluteMax); - for (int i = 0; i < zoomLevels.length; ++i) { - int zoomLevel = zoomLevels[i]; - int roundedMin = (int)(min / zoomLevel) * zoomLevel; - if (roundedMin > min) { - roundedMin -= zoomLevel; - } - double interval = (max - roundedMin) / intervals; - if (zoomLevel >= interval) { - return zoomLevel; + public int[] getYAxisDimension(double min, double max) { + min = Math.max(min, absoluteMin); + max = Math.min(max, absoluteMax); + int interval = 0; + int effectiveMin = 0; + for (int i = 0; i < intervalValues.length; i++) { + interval = intervalValues[i]; + effectiveMin = getEffetiveMin(min, interval); + double targetInterval = (max - effectiveMin) / numberOfIntervals; + if (interval >= targetInterval) { + break; } } - return zoomLevels[zoomLevels.length - 1]; + int effectiveMax = getEffectiveMax(max, interval); + return new int[] { interval, effectiveMin, effectiveMax }; + } + + /** + * Gets the effective min value. + * + * @param min the min value + * @param interval the interval + */ + private int getEffetiveMin(double min, int interval) { + int value = (int) (min / interval) * interval; + // value > min if min is negative + if (value > min) { + return value - interval; + } + return value; + } + + /** + * Gets the effective max value + * + * @param max the max value + * @param interval the interval + */ + private int getEffectiveMax(double max, int interval) { + return ((int) (max / interval)) * interval + interval; } } /** * Constructs a new chart value series. * - * @param context The context for the chart - * @param fillColor The paint for filling the chart - * @param strokeColor The paint for stroking the outside the chart, optional - * @param zoomSettings The settings related to zooming - * @param titleId The title ID - * - * TODO: Get rid of Context and inject appropriate values instead. + * @param context the context + * @param fillColor the fill color + * @param strokeColor the stroke color + * @param yAxisDimension the marking interval + * @param titleId the title id */ - public ChartValueSeries( - Context context, int fillColor, int strokeColor, ZoomSettings zoomSettings, int titleId) { - this.format = NumberFormat.getIntegerInstance(); + public ChartValueSeries(Context context, int fillColor, int strokeColor, + YAxisDimension yAxisDimension, int titleId) { + extremityMonitor = new ExtremityMonitor(); + numberFormat = NumberFormat.getIntegerInstance(); + path = new Path(); fillPaint = new Paint(); fillPaint.setStyle(Style.FILL); fillPaint.setColor(context.getResources().getColor(fillColor)); @@ -151,155 +149,137 @@ public class ChartValueSeries { strokePaint.setStyle(Style.STROKE); strokePaint.setColor(context.getResources().getColor(strokeColor)); strokePaint.setAntiAlias(true); - // Make a copy of the stroke paint with the default thickness. + // Make a copy of the stroke paint with the default thickness labelPaint = new Paint(strokePaint); - strokePaint.setStrokeWidth(2f); + strokePaint.setStrokeWidth(STROKE_WIDTH); } else { strokePaint = null; labelPaint = fillPaint; } - this.zoomSettings = zoomSettings; - this.title = context.getString(titleId); + this.markingInterval = yAxisDimension; + title = context.getString(titleId); } /** - * Draws the path of the chart + * Returns true if the series has data. */ - public void drawPath(Canvas c) { - c.drawPath(path, fillPaint); - if (strokePaint != null) { - c.drawPath(path, strokePaint); - } + public boolean hasData() { + return extremityMonitor.hasData(); } - + /** - * Resets this series + * Updates the series with a new value. + * + * @param value the new value */ - public void reset() { - monitor.reset(); + public void update(double value) { + extremityMonitor.update(value); } - + /** - * Updates this series with a new value + * Gets the number format for the series. */ - public void update(double d) { - monitor.update(d); + public NumberFormat getNumberFormat() { + return numberFormat; } - + /** - * @return The interval between markers + * Gets the path. */ - public int getInterval() { - return interval; - } - - /** - * Determines what the min and max of the chart will be. - * This will round down and up the min and max respectively. - */ - public void updateDimension() { - if (monitor.getMax() == Double.NEGATIVE_INFINITY) { - min = 0; - max = 1; - } else { - min = monitor.getMin(); - max = monitor.getMax(); - } - min = Math.min(min, zoomSettings.getAbsoluteMin()); - max = Math.max(max, zoomSettings.getAbsoluteMax()); - - this.interval = zoomSettings.calculateInterval(min, max); - // Round it up. - effectiveMax = ((int) (max / interval)) * interval + interval; - // Round it down. - effectiveMin = ((int) (min / interval)) * interval; - if (min < 0) { - effectiveMin -= interval; - } - spread = effectiveMax - effectiveMin; - } - - /** - * @return The length of the longest string from the series - */ - public int getMaxLabelLength() { - String minS = format.format(effectiveMin); - String maxS = format.format(getMax()); - return Math.max(minS.length(), maxS.length()); - } - - /** - * @return The rounded down minimum value - */ - public int getMin() { - return effectiveMin; - } - - /** - * @return The rounded up maximum value - */ - public int getMax() { - return effectiveMax; - } - - /** - * @return The difference between the min and max values in the series - */ - public double getSpread() { - return spread; - } - - /** - * @return The number format for this series - */ - NumberFormat getFormat() { - return format; - } - - /** - * @return The path for this series - */ - Path getPath() { + public Path getPath() { return path; } /** - * @return The paint for this series + * Gets the label paint. */ - Paint getPaint() { - return strokePaint == null ? fillPaint : strokePaint; - } - public Paint getLabelPaint() { return labelPaint; } - + /** - * @return The title of the series + * Gets the title. */ public String getTitle() { return title; } + /** + * Sets the title. + * + * @param title the title + */ public void setTitle(String title) { this.title = title; } /** - * @return is this series enabled + * Returns true if the series is enabled. */ public boolean isEnabled() { return enabled; } /** - * Sets the series enabled flag. + * Sets the enabled value. + * + * @param enabled true to enable */ public void setEnabled(boolean enabled) { this.enabled = enabled; } + + /** + * Draws the path on canvas. + * + * @param canvas the canvas + */ + public void drawPath(Canvas canvas) { + canvas.drawPath(path, fillPaint); + if (strokePaint != null) { + canvas.drawPath(path, strokePaint); + } + } - public boolean hasData() { - return monitor.hasData(); + /** + * Updates the y axis dimension. + */ + public void updateDimension() { + boolean hasData = extremityMonitor.hasData(); + double min = hasData ? extremityMonitor.getMin() : 0.0; + double max = hasData ? extremityMonitor.getMax() : 1.0; + int[] dimension = markingInterval.getYAxisDimension(min, max); + interval = dimension[0]; + effectiveMin = dimension[1]; + effectiveMax = dimension[2]; + } + + /** + * Gets the y axis interval value. + */ + public int getInterval() { + return interval; + } + + /** + * Gets the minimum value. + */ + public int getMin() { + return effectiveMin; + } + + /** + * Gets the maximum value. + */ + @VisibleForTesting + int getMax() { + return effectiveMax; + } + + /** + * Gets the maximum label length. + */ + public int getMaxLabelLength() { + return Math.max(numberFormat.format(getMin()).length(), numberFormat.format(getMax()).length()); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index af6c785fd..330493387 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; +import com.google.android.apps.mytracks.ChartValueSeries.YAxisDimension; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.ExtremityMonitor; import com.google.android.apps.mytracks.util.IntentUtils; @@ -208,7 +208,7 @@ public class ChartView extends View { new ChartValueSeries(context, R.color.elevation_fill, R.color.elevation_border, - new ZoomSettings(MAX_INTERVALS, + new YAxisDimension(MAX_INTERVALS, Integer.MIN_VALUE, Integer.MAX_VALUE, new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}), R.string.stats_elevation); @@ -216,27 +216,27 @@ public class ChartView extends View { new ChartValueSeries(context, R.color.speed_fill, R.color.speed_border, - new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new YAxisDimension(MAX_INTERVALS, 0, Integer.MAX_VALUE, new int[] {1, 5, 10, 20, 50}), R.string.stats_speed); series[POWER_SERIES] = new ChartValueSeries(context, R.color.power_fill, R.color.power_border, - new ZoomSettings(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}), + new YAxisDimension(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}), R.string.sensor_state_power); series[CADENCE_SERIES] = new ChartValueSeries(context, R.color.cadence_fill, R.color.cadence_border, - new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new YAxisDimension(MAX_INTERVALS, 0, Integer.MAX_VALUE, new int[] {5, 10, 25, 50}), R.string.sensor_state_cadence); series[HEART_RATE_SERIES] = new ChartValueSeries(context, R.color.heartrate_fill, R.color.heartrate_border, - new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new YAxisDimension(MAX_INTERVALS, 0, Integer.MAX_VALUE, new int[] {25, 50}), R.string.sensor_state_heart_rate); } @@ -819,7 +819,7 @@ public class ChartView extends View { desiredY = topBorder + effectiveHeight + FONT_HEIGHT / 2 - desiredY - 1; Paint p = new Paint(cvs.getLabelPaint()); p.setTextAlign(Align.RIGHT); - String text = cvs.getFormat().format(y); + String text = cvs.getNumberFormat().format(y); c.drawText(text, x, desiredY, p); return p.measureText(text); } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java index cbc5b3c71..3d0ad000c 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java @@ -15,36 +15,33 @@ */ package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; +import com.google.android.apps.mytracks.ChartValueSeries.YAxisDimension; import com.google.android.maps.mytracks.R; -import android.graphics.Paint.Style; import android.test.AndroidTestCase; /** + * Tests {@link ChartValueSeries}. + * * @author Sandor Dornbush */ public class ChartValueSeriesTest extends AndroidTestCase { private ChartValueSeries series; - + @Override protected void setUp() throws Exception { - series = new ChartValueSeries(getContext(), - R.color.elevation_fill, - R.color.elevation_border, - new ZoomSettings(5, new int[] {100}), - R.string.stats_elevation); + YAxisDimension yAxisDimension = new YAxisDimension( + 5, Integer.MIN_VALUE, Integer.MAX_VALUE, new int[] { 100 }); + series = new ChartValueSeries(getContext(), R.color.elevation_fill, R.color.elevation_border, + yAxisDimension, R.string.stats_elevation); } public void testInitialConditions() { - assertEquals(0, series.getInterval()); + assertEquals(1, series.getInterval()); assertEquals(1, series.getMaxLabelLength()); assertEquals(0, series.getMin()); - assertEquals(0, series.getMax()); - assertEquals(0.0, series.getSpread()); - assertEquals(Style.STROKE, series.getPaint().getStyle()); - assertEquals(getContext().getString(R.string.stats_elevation), - series.getTitle()); + assertEquals(1, series.getMax()); + assertEquals(getContext().getString(R.string.stats_elevation), series.getTitle()); assertTrue(series.isEnabled()); } @@ -52,7 +49,7 @@ public class ChartValueSeriesTest extends AndroidTestCase { series.setEnabled(false); assertFalse(series.isEnabled()); } - + public void testSmallUpdates() { series.update(0); series.update(10); @@ -61,7 +58,6 @@ public class ChartValueSeriesTest extends AndroidTestCase { assertEquals(3, series.getMaxLabelLength()); assertEquals(0, series.getMin()); assertEquals(100, series.getMax()); - assertEquals(100.0, series.getSpread()); } public void testBigUpdates() { @@ -72,7 +68,6 @@ public class ChartValueSeriesTest extends AndroidTestCase { assertEquals(5, series.getMaxLabelLength()); assertEquals(0, series.getMin()); assertEquals(1000, series.getMax()); - assertEquals(1000.0, series.getSpread()); } public void testNotZeroBasedUpdates() { @@ -83,56 +78,26 @@ public class ChartValueSeriesTest extends AndroidTestCase { assertEquals(5, series.getMaxLabelLength()); assertEquals(500, series.getMin()); assertEquals(1500, series.getMax()); - assertEquals(1000.0, series.getSpread()); } - public void testZoomSettings_invalidArgs() { - try { - new ZoomSettings(0, new int[] {10, 50, 100}); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // OK. - } - try { - new ZoomSettings(1, null); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // OK. - } - try { - new ZoomSettings(1, new int[] {}); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // OK. - } - try { - new ZoomSettings(1, new int[] {1, 3, 2}); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // OK. - } - } - - public void testZoomSettings_minAligned() { - ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100}); - assertEquals(10, settings.calculateInterval(0, 15)); - assertEquals(10, settings.calculateInterval(0, 50)); - assertEquals(50, settings.calculateInterval(0, 111)); - assertEquals(50, settings.calculateInterval(0, 250)); - assertEquals(100, settings.calculateInterval(0, 251)); - assertEquals(100, settings.calculateInterval(0, 10000)); + public void testYAxisDimension_minAligned() { + YAxisDimension yAxisDimension = new YAxisDimension( + 5, Integer.MIN_VALUE, Integer.MAX_VALUE, new int[] { 10, 50, 100 }); + assertEquals(10, yAxisDimension.getYAxisDimension(0, 15)[0]); + assertEquals(10, yAxisDimension.getYAxisDimension(0, 50)[0]); + assertEquals(50, yAxisDimension.getYAxisDimension(0, 111)[0]); + assertEquals(50, yAxisDimension.getYAxisDimension(0, 250)[0]); + assertEquals(100, yAxisDimension.getYAxisDimension(0, 251)[0]); + assertEquals(100, yAxisDimension.getYAxisDimension(0, 10000)[0]); } - public void testZoomSettings_minNotAligned() { - ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100}); - assertEquals(50, settings.calculateInterval(5, 55)); - assertEquals(10, settings.calculateInterval(10, 60)); - assertEquals(50, settings.calculateInterval(7, 250)); - assertEquals(100, settings.calculateInterval(7, 257)); - assertEquals(100, settings.calculateInterval(11, 10000)); - - // A regression test. - settings = new ZoomSettings(5, new int[] {5, 10, 20}); - assertEquals(10, settings.calculateInterval(-37.14, -11.89)); + public void testYAxisDimension_minNotAligned() { + YAxisDimension settings = new YAxisDimension( + 5, Integer.MIN_VALUE, Integer.MAX_VALUE, new int[] { 10, 50, 100 }); + assertEquals(50, settings.getYAxisDimension(5, 55)[0]); + assertEquals(10, settings.getYAxisDimension(10, 60)[0]); + assertEquals(50, settings.getYAxisDimension(7, 250)[0]); + assertEquals(100, settings.getYAxisDimension(7, 257)[0]); + assertEquals(100, settings.getYAxisDimension(11, 10000)[0]); } }