From 28e5b312c1fc9d7b608472f8815cdee054e46008 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Wed, 12 Jul 2017 08:39:03 -0500 Subject: [PATCH 01/57] Pie legend widget (#54) Adds legend support to PieChart and refactors legend functionality into abstract class LegendWidget. Also updates docs / unit tests. --- .../java/com/androidplot/pie/PieChart.java | 45 +++- .../com/androidplot/pie/PieLegendItem.java | 26 ++ .../com/androidplot/pie/PieLegendWidget.java | 41 +++ .../java/com/androidplot/ui/RenderStack.java | 2 +- .../com/androidplot/ui/widget/LegendItem.java | 13 + .../ui/widget/LegendItemOrganizer.java | 9 + .../androidplot/ui/widget/LegendWidget.java | 197 +++++++++++++++ .../java/com/androidplot/xy/XYLegendItem.java | 28 ++ .../com/androidplot/xy/XYLegendWidget.java | 239 ++++-------------- .../main/java/com/androidplot/xy/XYPlot.java | 6 +- .../com/androidplot/ui/RenderStackTest.java | 52 ++++ .../androidplot/xy/XYLegendWidgetTest.java | 120 ++++++--- .../demos/SimplePieChartActivity.java | 29 +-- docs/legend.md | 59 +++++ docs/release_notes.md | 20 +- docs/xyplot.md | 41 +-- gradle/wrapper/gradle-wrapper.properties | 2 +- 17 files changed, 618 insertions(+), 311 deletions(-) create mode 100644 androidplot-core/src/main/java/com/androidplot/pie/PieLegendItem.java create mode 100644 androidplot-core/src/main/java/com/androidplot/pie/PieLegendWidget.java create mode 100644 androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java create mode 100644 androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java create mode 100644 androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java create mode 100644 androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java create mode 100644 androidplot-core/src/test/java/com/androidplot/ui/RenderStackTest.java create mode 100644 docs/legend.md diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java index 615efaaa..35e5e416 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieChart.java @@ -37,15 +37,18 @@ public class PieChart extends Plot { + + private PieChart pieChart; + + public PieLegendWidget(LayoutManager layoutManager, PieChart pieChart, + Size widgetSize, + TableModel tableModel, + Size iconSize) { + super(tableModel, layoutManager, widgetSize, iconSize); + this.pieChart = pieChart; + } + + @Override + protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull PieLegendItem item) { + canvas.drawRect(iconRect, item.formatter.getFillPaint()); + } + + @Override + protected List getLegendItems() { + final List legendItems = new ArrayList<>(); + for(SeriesBundle item : pieChart.getRegistry().getLegendEnabledItems()) { + legendItems.add(new PieLegendItem(item.getSeries(), item.getFormatter())); + } + return legendItems; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java index d9b9d812..99227563 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/RenderStack.java @@ -23,7 +23,7 @@ import java.util.List; /** - * A stack of series to be rendered. The stack order is immutable but individual elements may be + * A stack of series to be rendered. The stack order is immutable but individual elements may be * manipulated via the public methods of {@link RenderStack.StackElement}. */ public class RenderStack { diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java new file mode 100644 index 00000000..5bb428ac --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItem.java @@ -0,0 +1,13 @@ +package com.androidplot.ui.widget; + +/** + * An item to be displayed by {@link LegendWidget}. + */ +public interface LegendItem { + + /** + * + * @return The user facing label for this item. + */ + String getTitle(); +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java new file mode 100644 index 00000000..536ee9d8 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java @@ -0,0 +1,9 @@ +package com.androidplot.ui.widget; + +import java.util.List; + + +public interface LegendItemOrganizer { + + void organize(List items); +} diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java new file mode 100644 index 00000000..69e41407 --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendWidget.java @@ -0,0 +1,197 @@ +package com.androidplot.ui.widget; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.androidplot.exception.PlotRenderException; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.Size; +import com.androidplot.ui.TableModel; +import com.androidplot.util.FontUtils; +import com.androidplot.util.PixelUtils; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +/** + * Provides core functionality for displaying a legend widget within a {@link com.androidplot.Plot}. + * @param + */ +public abstract class LegendWidget extends Widget { + + private static final float DEFAULT_TEXT_SIZE_DP = 20; + + private TableModel tableModel; + private Size iconSize; + + private Paint textPaint; + private Paint iconBackgroundPaint; + private Paint iconBorderPaint; + + private boolean drawIconBackgroundEnabled = true; + private boolean drawIconBorderEnabled = true; + + private Comparator legendItemComparator; + + { + textPaint = new Paint(); + textPaint.setColor(Color.LTGRAY); + textPaint.setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_DP)); + textPaint.setAntiAlias(true); + + iconBackgroundPaint = new Paint(); + iconBackgroundPaint.setColor(Color.BLACK); + + iconBorderPaint = new Paint(); + iconBorderPaint.setColor(Color.TRANSPARENT); + iconBorderPaint.setStyle(Paint.Style.STROKE); + } + + + public LegendWidget(@NonNull TableModel tableModel, @NonNull LayoutManager layoutManager, + @NonNull Size size, @NonNull Size iconSize) { + super(layoutManager, size); + setTableModel(tableModel); + this.iconSize = iconSize; + } + + @Override + protected void doOnDraw(Canvas canvas, RectF widgetRect) throws PlotRenderException { + final List items = getLegendItems(); + if(legendItemComparator != null) { + Collections.sort(items, legendItemComparator); + } + final Iterator cellRectIterator = tableModel.getIterator(widgetRect, items.size()); + for(ItemT item : items) { + final RectF cellRect = cellRectIterator.next(); + final RectF iconRect = getIconRect(cellRect); + beginDrawingCell(canvas, iconRect); + drawItem(canvas, iconRect, item); + finishDrawingCell(canvas, cellRect, iconRect, item); + } + } + + protected void drawItem(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item) { + drawIcon(canvas, iconRect, item); + } + + /** + * Draw the icon representing the legend item + * @param canvas + * @param iconRect The space to be occupied by the icon. + * @param item + */ + protected abstract void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item); + + /** + * + * @return The list of legend items to be drawn. This is used to calculate table dimensions etc. + */ + protected abstract List getLegendItems(); + + private RectF getIconRect(RectF cellRect) { + float cellRectCenterY = cellRect.top + (cellRect.height()/2); + RectF iconRect = iconSize.getRectF(cellRect); + + // center the icon rect vertically + float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2); + iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY); + return iconRect; + } + + /** + * Done at the start of rendering a new cell. Whatever is drawn here will be beneath the rest + * of the cell content; typically used to draw backgrounds. + * @param canvas + * @param iconRect + */ + protected void beginDrawingCell(Canvas canvas, RectF iconRect) { + + if(drawIconBackgroundEnabled && iconBackgroundPaint != null) { + canvas.drawRect(iconRect, iconBackgroundPaint); + } + } + + /** + * Done at the end of rendering a new cell. Whatever is drawn here will be on top of + * the rest of the cell content; typically used to draw borders and text. + * @param canvas + * @param cellRect + * @param iconRect + * @param legendItem + */ + protected void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, LegendItem legendItem) { + + if(drawIconBorderEnabled && iconBorderPaint != null) { + canvas.drawRect(iconRect, iconBorderPaint); + } + + float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2); + + if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) { + canvas.drawText(legendItem.getTitle(), iconRect.left - 2, centeredTextOriginY, textPaint); + } else { + canvas.drawText(legendItem.getTitle(), iconRect.right + 2, centeredTextOriginY, textPaint); + } + } + + protected static float getRectCenterY(RectF cellRect) { + return cellRect.top + (cellRect.height()/2); + } + + public synchronized void setTableModel(TableModel tableModel) { + this.tableModel = tableModel; + } + + public Paint getTextPaint() { + return textPaint; + } + + public void setTextPaint(Paint textPaint) { + this.textPaint = textPaint; + } + + public boolean isDrawIconBackgroundEnabled() { + return drawIconBackgroundEnabled; + } + + public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) { + this.drawIconBackgroundEnabled = drawIconBackgroundEnabled; + } + + public boolean isDrawIconBorderEnabled() { + return drawIconBorderEnabled; + } + + public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) { + this.drawIconBorderEnabled = drawIconBorderEnabled; + } + + public Size getIconSize() { + return iconSize; + } + + public void setIconSize(Size iconSize) { + this.iconSize = iconSize; + } + + public Comparator getLegendItemComparator() { + return legendItemComparator; + } + + /** + * Set a scheme for sorting the display order or legend items. By default no sorting is applied + * and {@link com.androidplot.Series} items typically appear in the order which the series was + * added to the {@link com.androidplot.Plot}. + * @param legendItemComparator + */ + public void setLegendItemComparator(Comparator legendItemComparator) { + this.legendItemComparator = legendItemComparator; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java new file mode 100644 index 00000000..1fd10ebd --- /dev/null +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendItem.java @@ -0,0 +1,28 @@ +package com.androidplot.xy; + +import android.support.annotation.NonNull; + +import com.androidplot.ui.widget.LegendItem; + +public class XYLegendItem implements LegendItem { + + public enum Type { + SERIES, + REGION + } + + public final Type type; + public final Object item; + private final String text; + + public XYLegendItem(@NonNull Type cellType, @NonNull Object item, @NonNull String text) { + this.type = cellType; + this.item = item; + this.text = text; + } + + @Override + public String getTitle() { + return this.text; + } +} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java index e5b981c5..f08783cb 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYLegendWidget.java @@ -17,233 +17,80 @@ package com.androidplot.xy; import android.graphics.*; +import android.support.annotation.NonNull; + import com.androidplot.ui.LayoutManager; import com.androidplot.ui.SeriesBundle; import com.androidplot.ui.Size; import com.androidplot.ui.TableModel; -import com.androidplot.ui.widget.Widget; -import com.androidplot.util.FontUtils; +import com.androidplot.ui.widget.LegendWidget; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Hashtable; +import java.util.List; +import java.util.Map.Entry; /** * Displays a legend for each series added to the owning {@link XYPlot}. */ -public class XYLegendWidget extends Widget { - - /** - * This class is of no use outside of XYLegendWidget. It's just used to alphabetically sort - * Region legend entries. - */ - private static class RegionEntryComparator implements Comparator> { - @Override - public int compare(Map.Entry o1, Map.Entry o2) { - return o1.getValue().compareTo(o2.getValue()); - } - } - - private enum CellType { - SERIES, - REGION - } +public class XYLegendWidget extends LegendWidget { private XYPlot plot; - //private float iconWidth = 12; - private Paint textPaint; - private Paint iconBorderPaint; - private TableModel tableModel; - private boolean drawIconBackgroundEnabled = true; - private boolean drawIconBorderEnabled = true; - - private Size iconSize; - private static final RegionEntryComparator regionEntryComparator = new RegionEntryComparator(); - //private RectF iconRect = new RectF(0, 0, ICON_WIDTH_DEFAULT, ICON_HEIGHT_DEFAULT); - - { - textPaint = new Paint(); - textPaint.setColor(Color.LTGRAY); - textPaint.setAntiAlias(true); - - iconBorderPaint = new Paint(); - iconBorderPaint.setStyle(Paint.Style.STROKE); - //regionEntryComparator = new RegionEntryComparator(); - } public XYLegendWidget(LayoutManager layoutManager, XYPlot plot, Size widgetSize, TableModel tableModel, Size iconSize) { - super(layoutManager, widgetSize); + super(tableModel, layoutManager, widgetSize, iconSize); this.plot = plot; - setTableModel(tableModel); - this.iconSize = iconSize; - } - - public synchronized void setTableModel(TableModel tableModel) { - this.tableModel = tableModel; - } - private RectF getIconRect(RectF cellRect) { - float cellRectCenterY = cellRect.top + (cellRect.height()/2); - RectF iconRect = iconSize.getRectF(cellRect); - - // center the icon rect vertically - float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2); - iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY); - return iconRect; - } - - private static float getRectCenterY(RectF cellRect) { - return cellRect.top + (cellRect.height()/2); - } - - private void beginDrawingCell(Canvas canvas, RectF iconRect) { - - Paint bgPaint = plot.getGraph().getGridBackgroundPaint(); - if(drawIconBackgroundEnabled && bgPaint != null) { - canvas.drawRect(iconRect, bgPaint); - } - } - - private void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, String text) { - - Paint bgPaint = plot.getGraph().getGridBackgroundPaint(); - if(drawIconBorderEnabled && bgPaint != null) { - iconBorderPaint.setColor(bgPaint.getColor()); - canvas.drawRect(iconRect, iconBorderPaint); - } - - float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2); - - if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) { - canvas.drawText(text, iconRect.left - 2, centeredTextOriginY, textPaint); - } else { - canvas.drawText(text, iconRect.right + 2, centeredTextOriginY, textPaint); - } + // Set a default comparator that sorts by type and then alphabetically + setLegendItemComparator(new Comparator() { + @Override + public int compare(XYLegendItem o1, XYLegendItem o2) { + if(o1.type == o2.type) { + return o1.getTitle().compareTo(o2.getTitle()); + } else { + return(o1.type.compareTo(o2.type)); + } + } + }); } protected void drawRegionLegendIcon(Canvas canvas, RectF rect, XYRegionFormatter formatter) { - canvas.drawRect(rect, formatter.getPaint()); - } - - private void drawRegionLegendCell(Canvas canvas, XYRegionFormatter formatter, RectF cellRect, String text) { - RectF iconRect = getIconRect(cellRect); - beginDrawingCell(canvas, iconRect); - - drawRegionLegendIcon( - canvas, - iconRect, - formatter - ); - finishDrawingCell(canvas, cellRect, iconRect, text); + canvas.drawRect(rect, formatter.getPaint()); } - private void drawSeriesLegendCell(Canvas canvas, XYSeriesRenderer renderer, XYSeriesFormatter formatter, RectF cellRect, String seriesTitle) { - RectF iconRect = getIconRect(cellRect); - beginDrawingCell(canvas, iconRect); - - renderer.drawSeriesLegendIcon( - canvas, - iconRect, - formatter); - finishDrawingCell(canvas, cellRect, iconRect, seriesTitle); + @Override + protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull XYLegendItem XYLegendItem) { + switch (XYLegendItem.type) { + case REGION: + drawRegionLegendIcon(canvas, iconRect, (XYRegionFormatter) XYLegendItem.item); + break; + case SERIES: + final XYSeriesFormatter formatter = (XYSeriesFormatter) XYLegendItem.item; + plot.getRenderer(formatter.getRendererClass()).drawSeriesLegendIcon(canvas, iconRect, formatter); + break; + default: + throw new UnsupportedOperationException("Unexpected item type: " + XYLegendItem.type); + } } -// protected List> getLegendEnabledSeriesAndFormatterList() { -// List> sfList = new ArrayList<>(); -// ListIterator> it = plot.getSeriesRegistry().listIterator(); -// while(it.hasNext()) { -// SeriesAndFormatter thisSf = it.next(); -// if(thisSf.getFormatter().isLegendIconEnabled()) { -// sfList.add(thisSf); -// } -// } -// return sfList; -// } - @Override - protected synchronized void doOnDraw(Canvas canvas, RectF widgetRect) { - if(plot.isEmpty()) { - return; + protected List getLegendItems() { + final ArrayList items = new ArrayList<>(); + for (SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) { + items.add(new XYLegendItem(XYLegendItem.Type.SERIES, sfPair.getFormatter(), sfPair.getSeries().getTitle())); } - // Keep an alphabetically sorted list of regions: - TreeSet> sortedRegions = new TreeSet>(new RegionEntryComparator()); - - // Calculate the number of cells needed to draw the Legend: - int seriesCount = plot.getRegistry().size(); - - for(XYSeriesRenderer renderer : plot.getRendererList()) { + for (XYSeriesRenderer renderer : plot.getRendererList()) { Hashtable urf = renderer.getUniqueRegionFormatters(); - sortedRegions.addAll(urf.entrySet()); - } - - seriesCount += sortedRegions.size(); - - // Create an iterator specially created to draw the number of cells we calculated: - Iterator it = tableModel.getIterator(widgetRect, seriesCount); - - RectF cellRect; - - // draw each series legend item: - for(SeriesBundle sfPair : plot.getRegistry().getLegendEnabledItems()) { - //for(SeriesAndFormatter sfPair : plot.getSeriesRegistry()) { - cellRect = it.next(); - XYSeriesFormatter format = sfPair.getFormatter(); - drawSeriesLegendCell(canvas, plot.getRenderer(sfPair.getFormatter().getRendererClass()), - format, cellRect, sfPair.getSeries().getTitle()); - } - - // draw each region legend item: - for(Map.Entry entry : sortedRegions) { - if(!it.hasNext()) { - break; + for (Entry entry : urf.entrySet()) { + items.add(new XYLegendItem(XYLegendItem.Type.REGION, entry.getKey(), entry.getValue())); } - cellRect = it.next(); - XYRegionFormatter formatter = entry.getKey(); - drawRegionLegendCell(canvas, formatter, cellRect, entry.getValue()); } - } - - - public Paint getTextPaint() { - return textPaint; - } - - public void setTextPaint(Paint textPaint) { - this.textPaint = textPaint; - } - - public boolean isDrawIconBackgroundEnabled() { - return drawIconBackgroundEnabled; - } - - public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) { - this.drawIconBackgroundEnabled = drawIconBackgroundEnabled; - } - - public boolean isDrawIconBorderEnabled() { - return drawIconBorderEnabled; - } - - public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) { - this.drawIconBorderEnabled = drawIconBorderEnabled; - } - - public TableModel getTableModel() { - return tableModel; - } - - public Size getIconSize() { - return iconSize; - } - /** - * Set the size of each legend's icon. Note that when using relative sizing, - * the size is calculated against the countaining cell's size, not the plot's size. - * @param iconSize - */ - public void setIconSize(Size iconSize) { - this.iconSize = iconSize; + return items; } } diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java index 15e8bbc2..67eaafa8 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYPlot.java @@ -48,9 +48,6 @@ */ public class XYPlot extends Plot { - private static final int DEFAULT_LEGEND_WIDGET_H_DP = 10; - private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 7; - private static final int DEFAULT_GRAPH_WIDGET_H_DP = 18; private static final int DEFAULT_GRAPH_WIDGET_W_DP = 10; @@ -60,8 +57,11 @@ public class XYPlot extends Plot renderStack = new RenderStack<>(plot); + + renderStack.sync(); + assertEquals(2, renderStack.getElements().size()); + for(RenderStack.StackElement element : renderStack.getElements()) { + assertTrue(element.isEnabled()); + } + + renderStack.disable(LineAndPointRenderer.class); + assertEquals(2, renderStack.getElements().size()); + for(RenderStack.StackElement element : renderStack.getElements()) { + assertFalse(element.isEnabled()); + } + } +} diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java index bbb2bec5..b50f7237 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYLegendWidgetTest.java @@ -17,61 +17,101 @@ package com.androidplot.xy; import android.graphics.*; -import com.androidplot.Plot; import com.androidplot.test.AndroidplotTest; -import org.junit.After; +import com.androidplot.ui.DynamicTableModel; +import com.androidplot.ui.LayoutManager; +import com.androidplot.ui.Size; +import com.androidplot.ui.SizeMode; +import com.google.common.collect.Lists; + +import org.junit.Before; import org.junit.Test; -import org.robolectric.RuntimeEnvironment; -import java.util.Arrays; -import static junit.framework.Assert.assertEquals; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class XYLegendWidgetTest extends AndroidplotTest { - static class MockXYPlot extends XYPlot { + @Mock LayoutManager layoutManager; + @Mock XYPlot xyPlot; + @Mock Canvas canvas; + @Mock XYRegionFormatter xyRegionFormatter; + LineAndPointRenderer lineAndPointRenderer; - public MockXYPlot() { - super(RuntimeEnvironment.application, "Test", - Plot.RenderMode.USE_MAIN_THREAD); - } + Size widgetSize = new Size(100, SizeMode.ABSOLUTE, 100, SizeMode.ABSOLUTE); + Size iconSize = new Size(10, SizeMode.ABSOLUTE, 10, SizeMode.ABSOLUTE); + XYSeriesRegistry seriesRegistry; - public void exposedOnSizeChanged(int w, int h, int oldw, int oldh) { - this.onSizeChanged(w, h, oldw, oldh); - } + XYLegendWidget legendWidget; - public void exposedOnDraw(Canvas canvas) { - this.onDraw(canvas); - } - } + @Before + public void before() { + seriesRegistry = new XYSeriesRegistry(); + legendWidget = spy(new XYLegendWidget(layoutManager, xyPlot, widgetSize, + new DynamicTableModel(4, 4), iconSize)); - @After - public void tearDown() throws Exception {} + lineAndPointRenderer = new LineAndPointRenderer(xyPlot); - @Test - public void testDoOnDraw() throws Exception { - MockXYPlot plot = new MockXYPlot(); + when(xyPlot.getRegistry()).thenReturn(seriesRegistry); + when(xyPlot.getRendererList()).thenReturn(Lists.newArrayList(lineAndPointRenderer)); + when(xyPlot.getRenderer(any(Class.class))).thenReturn(lineAndPointRenderer); + } - SimpleXYSeries s1 = new SimpleXYSeries((Arrays.asList(1, 2, 3)), - SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "s1"); + @Test + public void draw_drawsLegendIcons_forEnabledItemsOnly() throws Exception { + final XYSeries s1 = mock(XYSeries.class); + final XYSeriesFormatter f1 = new LineAndPointFormatter(); + f1.setLegendIconEnabled(true); + + final XYSeries s2 = mock(XYSeries.class); + final XYSeriesFormatter f2 = new LineAndPointFormatter(); + f2.setLegendIconEnabled(false); + + final RectRegion r1 = new RectRegion(0, 0, 10, 10, "r1"); + final RectRegion r2 = new RectRegion(0, 0, 20, 20, "r2"); + f1.addRegion(r1, new XYRegionFormatter(0)); + f2.addRegion(r2, new XYRegionFormatter(0)); + + seriesRegistry.add(s1, f1); + seriesRegistry.add(s2, f2); + legendWidget.draw(canvas); + + verify(legendWidget, times(2)) + .drawRegionLegendIcon(any(Canvas.class), any(RectF.class), any(XYRegionFormatter.class)); + verify(legendWidget, times(3)) + .drawIcon(any(Canvas.class), any(RectF.class), any(XYLegendItem.class)); + } - plot.addSeries(s1, new LineAndPointFormatter( - Color.RED, Color.GREEN, Color.BLUE, null)); + @Test + public void draw_sortsItemsAlphabeticallyByTitle() throws Exception{ + final XYLegendItem i1 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "zoo"); + final XYLegendItem i2 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "apple"); + final XYLegendItem i3 = new XYLegendItem(XYLegendItem.Type.SERIES, + new LineAndPointFormatter(), "boo"); - assertEquals(1, plot.getRegistry().size()); + final List legendItems = Lists.newArrayList(i1, i2, i3); + doReturn(legendItems).when(legendWidget).getLegendItems(); - plot.exposedOnSizeChanged(100, 100, 100, 100); - plot.redraw(); - // have to manually invoke this because the invalidate() - // invoked by redraw() is a stub and will not result in onDraw being called. - plot.exposedOnDraw(new Canvas()); + legendWidget.draw(canvas); - plot.removeSeries(s1); - assertEquals(0, plot.getRegistry().size()); - plot.addSeries(s1, new BarFormatter(Color.RED, Color.GREEN)); - plot.redraw(); + InOrder inOrder = Mockito.inOrder(legendWidget); - // throws NullPointerException before fix - // for ANDROIDPLOT-166 was applied. - plot.exposedOnDraw(new Canvas()); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i2)); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i3)); + inOrder.verify(legendWidget).drawIcon(any(Canvas.class), any(RectF.class), eq(i1)); } - } diff --git a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java index 03b5b12b..9484176f 100644 --- a/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java +++ b/demoapp/src/main/java/com/androidplot/demos/SimplePieChartActivity.java @@ -62,6 +62,9 @@ public void onCreate(Bundle savedInstanceState) // initialize our XYPlot reference: pie = (PieChart) findViewById(R.id.mySimplePieChart); + // enable the legend: + pie.getLegend().setVisible(true); + final float padding = PixelUtils.dpToPix(30); pie.getPie().setPadding(padding, padding, padding, padding); @@ -183,36 +186,10 @@ protected void setupIntroAnimation() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float scale = valueAnimator.getAnimatedFraction(); -// scalingSeries1.setScale(scale); -// scalingSeries2.setScale(scale); renderer.setExtentDegs(360 * scale); pie.redraw(); } }); -// animator.addListener(new Animator.AnimatorListener() { -// @Override -// public void onAnimationStart(Animator animator) { -// -// } -// -// @Override -// public void onAnimationEnd(Animator animator) { -// // the animation is over, so show point labels: -// series1Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); -// series2Format.getPointLabelFormatter().getTextPaint().setColor(Color.WHITE); -// plot.redraw(); -// } -// -// @Override -// public void onAnimationCancel(Animator animator) { -// -// } -// -// @Override -// public void onAnimationRepeat(Animator animator) { -// -// } -// }); // the animation will run for 1.5 seconds: animator.setDuration(1500); diff --git a/docs/legend.md b/docs/legend.md new file mode 100644 index 00000000..37c2d136 --- /dev/null +++ b/docs/legend.md @@ -0,0 +1,59 @@ +# The Legend +For `Plot` types that support it, the legend displays a list of elements in the plot along with +a color coded icon. The color coded icon is automatically generated using the colors and line styles +used to render the associated item. In the case of a `Series`, this is the `Formatter` you associated +with the `Series` when you added it to your `Plot`. + +# Showing / Hiding the Legend +Depending on the `Plot` type(s) you are using, the legend may or may not be visible by default. To +can enable / disable the legend: + +```java +plot.getLegend().setVisible(true|false); +``` + +# Hiding Series Items +You can tell Androidplot not to generate a legend item for a Series by configuring it's associated +`Formatter`: + +```java +formatter.setLegendIconEnabled(false); +``` + +## The TableModel +The `TableModel` controls how and where each item in the legend is drawn. Androidplot provides two +default implementations; `DynamicTableModel` and `FixedTableModel` (detailed below). All `TableModel` implementations +organize elements into a grid. This grid is populated with items based on the order which it's corresponding +series was added to the plot. This ordering can be further controlled by setting the `TableModel`'s +`TableOrder` param to either [ROW_MAJOR](https://en.wikipedia.org/wiki/Row-major_order) (items are added left-to-right, top-down) +or `COLUMN_MAJOR` (items are added top-down, left-to-right). + +### DynamicTableModel +The `DynamicTableModel` takes a desired of numbered rows and columns and evenly subdivides the `LegendWidget`'s +visible space into cells. For example, A 2x2 legend using `ROW_MAJOR` ordering: + +```java +plot.getLegend().setTableModel(new DynamicTableModel(2, 2, TableOrder.ROW_MAJOR)); +``` + +### FixedTableModel +The `FixedTableModel` takes a desired size of each cell in pixels and adds cells using the specified `TableOrder`. +It automatically wraps to the next row or column (based on `TableOrder`) when the cell being added +exceeds the legend's available space on a given axis. For example, A `FixedTableModel` using 300w*100h cells and +a TableOrder of `COLUMN_MAJOR`: + +```java +plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300), + PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR)); +``` + +# Sorting Legend Entries +You can control the order of Legend entries by setting a custom `Comparator` on the legend: + +```java +Comparator<...> myComparator = ... +plot.getLegend().setLegendItemComparator(myComparator); +``` + +Using a custom `Comparator` in conjunction with `ROW_MAJOR` and `COLUMN_MAJOR` properties on the `TableModel` +(show above) gives you full control over the display ordering of your legend entries. \ No newline at end of file diff --git a/docs/release_notes.md b/docs/release_notes.md index d9a0ef4a..7ea22016 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -2,17 +2,32 @@ For details on what to expect in general when updating to a new version of Androiplot, check out the [versioning doc](versioning.md). +# 1.5.0 + +_Updates to legend functionality in this version may result in changes to the display order +of legend items in some cases. A custom `Comparator` can be used to resolve this if necessary; +see the [legend doc](legend.md) for implementation details._ + +* Added [legend doc](legend.md) +* Added legend support to `PieChart` +* Added configurable legend item sorting +* (#45) Auto range boundaries calculation fix for when using a fixed domain range and a `FastXYSeries` +* Minor Performance Optimizations + # 1.4.3 + * (#39) `FastLineAndPointRenderer` now renders vertices for legend items. * Added [XML Attrs reference doc](attrs.md). # 1.4.2 + * (#32) New step mode: `INCREMENT_BY_FIT`. -* (#33) PanZoom support for 'INCREMENT_BY_FIT'. +* (#33) `PanZoom` support for `INCREMENT_BY_FIT`. * (#34) Removed examples and documentation for serializing `SeriesRegistry` to preserve state. # 1.4.1 -* (#26) Fixed an NPE issue when drawing null values with a PointLabeler. + +* (#26) Fixed an NPE issue when drawing null values with a `PointLabeler`. * Fixed a broken link in Quickstart doc. # 1.4.0 @@ -92,6 +107,7 @@ See the [pie chart documentation](piechart.md) for usage details. * Removed InteractiveXYPlot as PanZoom makes it obsolete. # 1.0.0 + This is a factor of several core elements of the Androidplot lib. The general theme was to make class and method names more intuitive and to make xml styling more powerful. diff --git a/docs/xyplot.md b/docs/xyplot.md index 1e363d12..2dab08db 100644 --- a/docs/xyplot.md +++ b/docs/xyplot.md @@ -292,44 +292,9 @@ See the [candlestick documentation](candlestick.md) Smooth lines can be created by applying the [Catmull-Rom interpolator](http://androidplot.com/smooth-curves-and-androidplot/) to your series' Format. -# The Legend -By default, Androidplot will automatically produce a legend for your Plot. You however choose to hide the legend -or you can customize it to suit your needs. - -# Hiding Legend Items -As mentioned above, Androidplot automatically produces a legend for your Plot. This "auto legend" includes -items for each series added to the plot. If you wish to omit a series from the legend: - -```java -formatter.setLegendIconEnabled(false); -``` - -## The TableModel -The `TableModel` controls how and where each item in the legend is drawn. Androidplot provides two -default implementations; `DynamicTableModel` and `FixedTableModel` (detailed below). All `TableModel` implementations -organize elements into a grid. This grid is populated with items based on the order which it's corresponding -series was added to the plot. This ordering can be further controlled by setting the `TableModel`'s -`TableOrder` param to either [ROW_MAJOR](https://en.wikipedia.org/wiki/Row-major_order) (items are added left-to-right, top-down) -or `COLUMN_MAJOR` (items are added top-down, left-to-right). - -### DynamicTableModel -The `DynamicTableModel` takes a desired of numbered rows and columns and evenly subdivides the `LegendWidget`'s -visible space into cells. For example, A 2x2 legend using `ROW_MAJOR` ordering: - -```java -plot.getLegend().setTableModel(new DynamicTableModel(2, 2, TableOrder.ROW_MAJOR)); -``` - -### FixedTableModel -The `FixedTableModel` takes a desired size of each cell in pixels and adds cells using the specified `TableOrder`. -It automatically wraps to the next row or column (based on `TableOrder`) when the cell being added -exceeds the legend's available space on a given axis. For example, A `FixedTableModel` using 300w*100h cells and -a TableOrder of `COLUMN_MAJOR`: - -```java -plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300), - PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR)); -``` +# The Legend +By default, Androidplot will automatically produce a legend for your `XYPlot`. See [the legend](legend.md) doc +for usage details. # Graph Rotation Androidplot provides the `Widget.setRotation(Widget.Rotation)` method for controlling the orientation diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0429c501..12d6c54e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip From e58d4dadd428ce81b580c3df4938014036529eb7 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sun, 28 May 2017 08:57:26 -0500 Subject: [PATCH 02/57] Uprev to 1.5.0 --- build.gradle | 2 +- docs/quickstart.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f6deec86..f7124dd4 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 25 theTargetSdkVersion = 25 theMinSdkVersion = 5 - theVersionName = '1.4.4' + theVersionName = '1.5.0' theVersionCode = 0 } diff --git a/docs/quickstart.md b/docs/quickstart.md index 4c629126..76060cd9 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -14,7 +14,7 @@ To use the library in your gradle project add the following to your build.gradle ```groovy dependencies { - compile "com.androidplot:androidplot-core:1.4.3" + compile "com.androidplot:androidplot-core:1.5.0" } ``` From a124e850eb31bdfd5447223bbe008c7cebe8b056 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Wed, 12 Jul 2017 08:14:15 -0500 Subject: [PATCH 03/57] Documentation updates --- docs/index.md | 1 + docs/legend.md | 8 ++++++-- docs/plot_composition.md | 26 +++++++++++++------------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/index.md b/docs/index.md index c42b6b06..abb8a729 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,7 @@ specific plot types explaining styling and other advanced topics. * [Quickstart](quickstart.md) :star: * [Quickstart (YouTube Video)](https://www.youtube.com/watch?v=wEFkzQY_wWI) :movie_camera: * [Plot Composition](plot_composition.md) +* [The Legend](legend.md) * [XY Plots](xyplot.md) * [Bar Charts](barchart.md) * [Candlestick Charts](candlestick.md) diff --git a/docs/legend.md b/docs/legend.md index 37c2d136..c3ea90e4 100644 --- a/docs/legend.md +++ b/docs/legend.md @@ -13,7 +13,7 @@ plot.getLegend().setVisible(true|false); ``` # Hiding Series Items -You can tell Androidplot not to generate a legend item for a Series by configuring it's associated +You can tell Androidplot not to generate a legend item for a `Series` by configuring it's associated `Formatter`: ```java @@ -56,4 +56,8 @@ plot.getLegend().setLegendItemComparator(myComparator); ``` Using a custom `Comparator` in conjunction with `ROW_MAJOR` and `COLUMN_MAJOR` properties on the `TableModel` -(show above) gives you full control over the display ordering of your legend entries. \ No newline at end of file +(show above) gives you full control over the display ordering of your legend entries. + +# Positioning and Resizing +The legend is just an implementation of a Widget and is positioned and resized in the same ways +that all Widget instances are positioned. See the [Plot Composition](plot_composition.md) doc for details. \ No newline at end of file diff --git a/docs/plot_composition.md b/docs/plot_composition.md index 20b80952..3252b9f5 100644 --- a/docs/plot_composition.md +++ b/docs/plot_composition.md @@ -1,26 +1,26 @@ # Plot Composition -All plots in Androidplot inherit from the abstract base class Plot which provides common behaviors -for all Plot implementations. +All plots in Androidplot inherit from the abstract base class `Plot` which provides common behaviors +for all `Plot` implementations. # Widgets -Plots are composed of one or more Widgets. A Widget is an abstraction of a visual +Plots are composed of one or more Widgets. A `Widget` is an abstraction of a visual component that may be positioned and scaled within the visible area of a Plot. For example, -an XY Plot is typically composed of these 5 Widgets: +an `XYPlot` is typically composed of these 5 `Widgets`: * Title * Graph * Domain Label * Range Label -* Legend +* [Legend](legend.md) -All Plot implementations will contain at least one default Widget providing the core -behavior encapsulated by that Plot. In addition to moving and scaling these Widgets, developers may -also extend them and replace the Plot's default instance with the derived implenentation in order to +All implementations of `Plot` will contain at least one default `Widget` providing the core +behavior encapsulated by that `Plot`. In addition to moving and scaling a `Widget`, developers may +also extend them and replace the `Plot` instance's default instance with the derived implementation in order to get custom behavior. # The LayoutManager -The LayoutManager provides the logic for visually positioning and scaling Widgets; all Plot implementations -contain an instance of LayoutManager that can be retrieved via `Plot.getLayoutManager()`. +The `LayoutManager` provides the logic for visually positioning and scaling Widgets; all `Plot` implementations +contain an instance of `LayoutManager` that can be retrieved via `Plot.getLayoutManager()`. ## Z-Indexing Z-indexing is a 2D drawing concept which associates each drawable entity with a value that determines @@ -28,12 +28,12 @@ which elements get drawn onto the screen first, producing the visual effect that on top of others. While Androidplot uses the term "z-index" it's implemented internally as a linked list to prevent the possibility -of duplicate index values and therefore ensuring that the drawing order of Widgets is always explicit. +of duplicate index values and therefore ensuring that `Widget` drawing order is always explicit. The [Layerable](../androidplot-core/src/main/java/com/androidplot/util/Layerable.java) interface -defines methods used for manipulating the z-index of a Widget. +defines methods used for manipulating the z-index of a `Widget`. ## Adding & Removing Widgets -New Widgets can be added either to the front or back of the z-index using these methods: +New `Widget` instances can be added either to the front or back of the z-index using these methods: * `LayoutManager.addToTop(Widget)` * `LayoutManager.addToBottom(Widget)` From e6ac20ac8952e907ffee4cc503b198131547c55c Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Wed, 12 Jul 2017 09:27:32 -0500 Subject: [PATCH 04/57] uprev to 1.5.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f7124dd4..d81cbf77 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 25 theTargetSdkVersion = 25 theMinSdkVersion = 5 - theVersionName = '1.5.0' + theVersionName = '1.5.1' theVersionCode = 0 } From d448f5c047e3e9ccdba6b768a3ee12341d54d7ef Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Fri, 4 Aug 2017 07:51:18 -0500 Subject: [PATCH 05/57] #55 Fixes PieRenderer.getContainingSegment for segments larger than 50% of the pie. (#57) --- .../java/com/androidplot/pie/PieRenderer.java | 38 ++++++++++------- .../com/androidplot/pie/PieRendererTest.java | 41 ++++++++++++++++++- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java index 8b663a82..dc83e620 100644 --- a/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/pie/PieRenderer.java @@ -30,11 +30,14 @@ */ public class PieRenderer extends SeriesRenderer { + private static final float FULL_PIE_DEGS = 360f; + private static final float HALF_PIE_DEGS = 180f; + // starting angle to use when drawing the first radial line of the first segment. private float startDegs = 0; // number of degrees to extend from startDegs; can be used to "shape" the pie chart. - private float extentDegs = 360; + private float extentDegs = FULL_PIE_DEGS; // TODO: express donut in units other than px. private float donutSize = 0.5f; @@ -240,7 +243,7 @@ protected PointF calculateLineEnd(float x, float y, float rad, float deg) { protected PointF calculateLineEnd(PointF origin, float rad, float deg) { - double radians = deg * Math.PI / 180F; + double radians = deg * Math.PI / HALF_PIE_DEGS; double x = rad * Math.cos(radians); double y = rad * Math.sin(radians); @@ -292,11 +295,10 @@ public Segment getContainingSegment(PointF point) { float dx = point.x - origin.x; float dy = point.y - origin.y; double theta = Math.atan2(dy, dx); - double angle = (theta * (180f / Math.PI)); + double angle = (theta * (HALF_PIE_DEGS / Math.PI)); if (angle < 0) { - // convert angle to 0-360 range with 0 being in the - // traditional "east" orientation: - angle += 360f; + // bring into 0-360 range + angle += FULL_PIE_DEGS; } // find the segment whose starting and ending angle (degs) contains @@ -310,10 +312,16 @@ public Segment getContainingSegment(PointF point) { float lastOffset = offset; float sweep = (float) (scale * (values[i]) * extentDegs); offset += sweep; - offset = offset % 360; + offset = offset % FULL_PIE_DEGS; final double dist = signedDistance(offset, angle); - if(dist > 0 && dist <= signedDistance(offset, lastOffset)) { + double endDist = signedDistance(offset, lastOffset); + if(endDist < 0) { + // segment accounts for more than 50% of the pie and wrapped around + // need to correct: + endDist = FULL_PIE_DEGS + endDist; + } + if(dist > 0 && dist <= endDist) { return sfPair.getSeries(); } i++; @@ -328,10 +336,10 @@ public Segment getContainingSegment(PointF point) { * @return */ protected static float degsToScreenDegs(float degs) { - degs = degs % 360; + degs = degs % FULL_PIE_DEGS; if (degs > 0) { - return 360 - degs; + return FULL_PIE_DEGS - degs; } else { return degs; } @@ -344,12 +352,12 @@ protected static float degsToScreenDegs(float degs) { * @return */ protected static double signedDistance(double angle1, double angle2) { - double d = Math.abs(angle1 - angle2) % 360; - double r = d > 180 ? 360 - d : d; + double d = Math.abs(angle1 - angle2) % FULL_PIE_DEGS; + double r = d > HALF_PIE_DEGS ? FULL_PIE_DEGS - d : d; //calculate sign - int sign = (angle1 - angle2 >= 0 && angle1 - angle2 <= 180) - || (angle1 - angle2 <= -180 && angle1 - angle2 >= -360) ? 1 : -1; + int sign = (angle1 - angle2 >= 0 && angle1 - angle2 <= HALF_PIE_DEGS) + || (angle1 - angle2 <= -HALF_PIE_DEGS && angle1 - angle2 >= -FULL_PIE_DEGS) ? 1 : -1; r *= sign; return r; } @@ -359,7 +367,7 @@ protected static double signedDistance(double angle1, double angle2) { * @param degs */ protected static void validateInputDegs(float degs) { - if(degs < 0 || degs > 360) { + if(degs < 0 || degs > FULL_PIE_DEGS) { throw new IllegalArgumentException("Degrees values must be between 0.0 and 360."); } } diff --git a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java index dba4ce43..e504582a 100644 --- a/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java +++ b/androidplot-core/src/test/java/com/androidplot/pie/PieRendererTest.java @@ -110,7 +110,7 @@ public void testOnRender() throws Exception { } @Test - public void testGetContainingSegment() throws Exception { + public void getContainingSegment_returnsCorrectSegment() throws Exception { Segment segment1 = spy(new Segment("s1", 25)); Segment segment2 = spy(new Segment("s2", 25)); Segment segment3 = spy(new Segment("s3", 25)); @@ -150,6 +150,45 @@ public void testGetContainingSegment() throws Exception { assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0))); } + @Test + public void getContainingSegment_handlesSegmentsLargerThanHalfPie() throws Exception { + Segment segment1 = spy(new Segment("s1", 25)); + Segment segment2 = spy(new Segment("s2", 24)); + Segment segment3 = spy(new Segment("s3", 51)); + SegmentFormatter formatter = spy( + new SegmentFormatter(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN)); + PieRenderer renderer = formatter.getRendererInstance(pieChart); + + pieChart.addSegment(segment1, formatter); + pieChart.addSegment(segment2, formatter); + pieChart.addSegment(segment3, formatter); + + // southeast + assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 100))); + + // southwest + assertEquals(segment2, renderer.getContainingSegment(new PointF(0, 100))); + + // northwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0))); + + // northeast + assertEquals(segment3, renderer.getContainingSegment(new PointF(100, 0))); + + renderer.setStartDegs(90); + // southeast + assertEquals(segment2, renderer.getContainingSegment(new PointF(100, 100))); + + // southwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 100))); + + // northwest + assertEquals(segment3, renderer.getContainingSegment(new PointF(0, 0))); + + // northeast + assertEquals(segment1, renderer.getContainingSegment(new PointF(100, 0))); + } + @Test public void testDegsToScreenDegs() throws Exception { assertEquals(0f, PieRenderer.degsToScreenDegs(0)); From 44c9b621cd6801b6fc368fea14932a2b50891162 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sat, 5 Aug 2017 09:29:03 -0500 Subject: [PATCH 06/57] Updates buildscript to CircleCI 2.0 --- .circleci/config.yml | 99 +++++++++++++++++++++++++++++++++++ .gitignore | 3 +- androidplot-core/build.gradle | 4 +- build.gradle | 2 +- circle.yml | 59 --------------------- demoapp-wearable/build.gradle | 2 +- 6 files changed, 104 insertions(+), 65 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 circle.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..43c50cfc --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,99 @@ +# Java Gradle CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-java/ for more details +# +version: 2 + +general: + branches: + only: + #- circleci +jobs: + build: + docker: + # specify the version you desire here + #- image: circleci/openjdk:8-jdk + + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + - image: circleci/android:api-25-alpha + + working_directory: ~/repo + + environment: + # Customize the JVM maximum heap limit + JVM_OPTS: -Xmx3200m + TERM: dumb +# KEYSTORE: ${CIRCLE_WORKING_DIRECTORY}/sigining.keystore +# PUBLISHER_ACCT_JSON_FILE: ${CIRCLE_WORKING_DIRECTORY}/publisher_profile.json + + steps: + - checkout + + - run: echo 'export KEYSTORE=${HOME}/repo/sigining.keystore' >> $BASH_ENV + - run: echo 'export PUBLISHER_ACCT_JSON_FILE=${HOME}/repo/publisher_profile.json' >> $BASH_ENV + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "build.gradle" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + # Get private keys etc + - run: ./misc/download_keystore.sh + - run: ./misc/inject_circle_build_number.sh + + - run: ./gradlew dependencies + + - save_cache: + paths: + - ~/.m2 + key: v1-dependencies-{{ checksum "build.gradle" }} + + # run tests & code coc! + - run: ./gradlew testDebug jacocoTestReportDebug + + # build release + - run: ./gradlew assembleRelease + + # javadoc + - run: ./gradlew javadoc + + # trigger codecod.io + - run: bash <(curl -s https://codecov.io/bash) + + - store_artifacts: + path: androidplot-core/build/outputs/aar/ + destination: aar + + - store_artifacts: + path: demoapp/build/outputs/apk/ + destination: apk + + - store_artifacts: + path: androidplot-core/build/reports/jacoco/debug/ + destination: coverage_report + + - store_artifacts: + path: androidplot-core/build/reports/tests/ + destination: test_results + + - store_test_results: + path: androidplot-core/build/test-results/ + + - deploy: + name: "Deploy to Bintray" + command: | + if [ "${CIRCLE_BRANCH}" == "master" ]; + then ./gradlew bintrayUpload; + fi + + - deploy: + name: "Deploy to Google Play" + command: | + if [ "${CIRCLE_BRANCH}" == "master" ]; + then + ./misc/download_google_publisher_json.sh; + ./gradlew publishApkRelease + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index 58204be4..694f2995 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,5 @@ DemoApp/.settings DemoApp/bin DemoApp/gen DemoApp/target -.idea/libraries -.idea/*.xml +.idea **/R.java diff --git a/androidplot-core/build.gradle b/androidplot-core/build.gradle index aff191ce..3e305c7d 100644 --- a/androidplot-core/build.gradle +++ b/androidplot-core/build.gradle @@ -74,7 +74,7 @@ android { lintOptions { abortOnError false } - buildToolsVersion '25.0.0' + buildToolsVersion theBuildToolsVersion } group = 'com.androidplot' @@ -86,7 +86,7 @@ def gitUrl = 'https://github.com/halfhp/androidplot.git' dependencies { compile 'com.halfhp.fig:figlib:1.0.3' - compile 'com.android.support:support-annotations:24.2.0' + compile 'com.android.support:support-annotations:25.3.1' testCompile "org.mockito:mockito-core:1.10.19" testCompile group: 'junit', name: 'junit', version: '4.12' testCompile "org.robolectric:robolectric:3.1" diff --git a/build.gradle b/build.gradle index d81cbf77..7b379558 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.3.2' + classpath 'com.android.tools.build:gradle:2.3.3' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.5.0' diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 494b4bc2..00000000 --- a/circle.yml +++ /dev/null @@ -1,59 +0,0 @@ - -machine: - environment: - KEYSTORE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/sigining.keystore - PUBLISHER_ACCT_JSON_FILE: ${HOME}/${CIRCLE_PROJECT_REPONAME}/publisher_profile.json - -dependencies: - - pre: - - if [ ! -e /usr/local/android-sdk-linux/platforms/android-25 ]; then echo y | android update sdk --all --no-ui --filter "android-25"; fi; - - if [ ! -e /usr/local/android-sdk-linux/build-tools/25.0.2 ]; then echo y | android update sdk --all --no-ui --filter "build-tools-25.0.2"; fi; - - bash ./misc/download_keystore.sh - - bash ./misc/inject_circle_build_number.sh - -test: - - override: - - (./gradlew test assembleRelease javadoc): - timeout: 360 - - post: - - # core lib: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/outputs/aar/ $CIRCLE_ARTIFACTS - - # demo app .apk: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/demoapp/build/outputs/apk/ $CIRCLE_ARTIFACTS - - # javadoc: - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/docs/javadoc/ $CIRCLE_ARTIFACTS - - - # junit xml report: - - mkdir -p $CIRCLE_TEST_REPORTS/junit-xml/ - - find . -type f -regex ".*/build/test-results/testReleaseUnitTest/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit-xml/ \; - - # junit html report: - # TODO: recursively copy subdirs etc - - mkdir -p $CIRCLE_TEST_REPORTS/junit-html/ - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/tests/testReleaseUnitTest/* $CIRCLE_TEST_REPORTS/junit-html/ - - # lint report: - - mkdir -p $CIRCLE_TEST_REPORTS/lint/ - - find . -type f -regex ".*/build/outputs/.*html" -exec cp {} $CIRCLE_TEST_REPORTS/lint/ \; - - # code coverage: - - ./gradlew jacocoTestReportDebug - - mkdir -p $CIRCLE_TEST_REPORTS/jacoco/ - - cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/androidplot-core/build/reports/jacoco/debug/. $CIRCLE_TEST_REPORTS/jacoco - - bash <(curl -s https://codecov.io/bash) - -deployment: - master: - branch: master - commands: - - (./gradlew bintrayUpload): - timeout: 360 - - bash ./misc/download_google_publisher_json.sh - - ./gradlew publishApkRelease diff --git a/demoapp-wearable/build.gradle b/demoapp-wearable/build.gradle index 37406aaa..8154801c 100644 --- a/demoapp-wearable/build.gradle +++ b/demoapp-wearable/build.gradle @@ -19,7 +19,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.2' + classpath 'com.android.tools.build:gradle:2.3.3' } } apply plugin: 'com.android.application' From a16a1df75adf417e4c18083e0f756eb746d6da36 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sat, 5 Aug 2017 09:58:01 -0500 Subject: [PATCH 07/57] #52 - Added NPE check to Plot.renderOnCanvas (#59) --- androidplot-core/src/main/java/com/androidplot/Plot.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index f2eab84a..3ee10174 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -21,6 +21,7 @@ import android.graphics.*; import android.os.Build; import android.os.Looper; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -804,7 +805,10 @@ protected void onDraw(Canvas canvas) { * "heavy lifting". * @param canvas */ - protected synchronized void renderOnCanvas(Canvas canvas) { + protected synchronized void renderOnCanvas(@Nullable Canvas canvas) { + if(canvas == null) { + return; + } try { // any series interested in synchronizing with plot should // implement PlotListener.onBeforeDraw(...) and do a read lock from within its From 9088a7b1ae536fceb9b570ec07c2eb39fa0a13a0 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sat, 5 Aug 2017 09:59:50 -0500 Subject: [PATCH 08/57] updates quickstart lib version to 1.5.1 --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 76060cd9..cf238b57 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -14,7 +14,7 @@ To use the library in your gradle project add the following to your build.gradle ```groovy dependencies { - compile "com.androidplot:androidplot-core:1.5.0" + compile "com.androidplot:androidplot-core:1.5.1" } ``` From 0cb45aef633bc64fcaf4eca17c5b1ffd454f350f Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Sat, 5 Aug 2017 21:01:15 -0500 Subject: [PATCH 09/57] uprev to 1.5.2 for development --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7b379558..dad2f2f7 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { theCompileSdkVersion = 25 theTargetSdkVersion = 25 theMinSdkVersion = 5 - theVersionName = '1.5.1' + theVersionName = '1.5.2' theVersionCode = 0 } From 4662832d5c73b7bceba53c9dd948f8e9ff1f9026 Mon Sep 17 00:00:00 2001 From: guycnicholas Date: Wed, 8 Nov 2017 09:40:24 -0800 Subject: [PATCH 10/57] For issue #61 updated screenToSeriesY to use the vertical bounds rather than horizontal (#62) --- .../com/androidplot/xy/XYGraphWidget.java | 2 +- .../com/androidplot/xy/XYGraphWidgetTest.java | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java index a6460799..d1ce96ec 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYGraphWidget.java @@ -489,7 +489,7 @@ protected float seriesToScreenX(Number x) { protected float seriesToScreenY(Number y) { return (float) plot.getBounds().getyRegion(). - transform(y.doubleValue(), gridRect.left, gridRect.right, true); + transform(y.doubleValue(), gridRect.bottom, gridRect.top, true); } @Override diff --git a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java index 4b9f156a..0bfe4d9d 100644 --- a/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java +++ b/androidplot-core/src/test/java/com/androidplot/xy/XYGraphWidgetTest.java @@ -79,7 +79,7 @@ public void setUp() throws Exception { xyPlot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1); graphWidget = spy(new XYGraphWidget(layoutManager, xyPlot, size)); - graphWidget.setGridRect(new RectF(0, 0, 100, 100)); + graphWidget.setGridRect(new RectF(0, 0, 10, 100)); graphWidget.setLabelRect(new RectF(0, 0, 100, 100)); } @@ -253,11 +253,11 @@ public void testScreenToSeries() throws Exception { assertEquals(-100, coords.x.intValue()); assertEquals(100, coords.y.intValue()); - coords = graphWidget.screenToSeries(new PointF(100, 100)); + coords = graphWidget.screenToSeries(new PointF(10, 100)); assertEquals(100, coords.x.intValue()); assertEquals(-100, coords.y.intValue()); - coords = graphWidget.screenToSeries(new PointF(50, 50)); + coords = graphWidget.screenToSeries(new PointF(5, 50)); assertEquals(0, coords.x.intValue()); assertEquals(0, coords.y.intValue()); } @@ -271,11 +271,11 @@ public void testSeriesToScreen() throws Exception { assertEquals(0f, point.y); point = graphWidget.seriesToScreen(new XYCoords(100, -100)); - assertEquals(100f, point.x); + assertEquals(10f, point.x); assertEquals(100f, point.y); point = graphWidget.seriesToScreen(new XYCoords(0, 0)); - assertEquals(50f, point.x); + assertEquals(5f, point.x); assertEquals(50f, point.y); } @@ -284,8 +284,8 @@ public void testScreenToSeriesX() throws Exception { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); assertEquals(-100, graphWidget.screenToSeriesX(new PointF(0, 0)).intValue()); - assertEquals(100, graphWidget.screenToSeriesX(new PointF(100, 100)).intValue()); - assertEquals(0, graphWidget.screenToSeriesX(new PointF(50, 50)).intValue()); + assertEquals(100, graphWidget.screenToSeriesX(new PointF(10, 100)).intValue()); + assertEquals(0, graphWidget.screenToSeriesX(new PointF(5, 50)).intValue()); } @Test @@ -302,16 +302,16 @@ public void testSeriesToScreenX() throws Exception { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); assertEquals(0f, graphWidget.seriesToScreenX(-100)); - assertEquals(100f, graphWidget.seriesToScreenX(100)); - assertEquals(50f, graphWidget.seriesToScreenX(0)); + assertEquals(10f, graphWidget.seriesToScreenX(100)); + assertEquals(5f, graphWidget.seriesToScreenX(0)); } @Test public void testSeriesToScreenY() throws Exception { when(xyPlot.getBounds()).thenReturn(new RectRegion(-100, 100, -100, 100)); - assertEquals(0f, graphWidget.seriesToScreenY(100)); - assertEquals(100f, graphWidget.seriesToScreenY(-100)); + assertEquals(100f, graphWidget.seriesToScreenY(100)); + assertEquals(0f, graphWidget.seriesToScreenY(-100)); assertEquals(50f, graphWidget.seriesToScreenY(0)); } } From 55fce04bff334e0988aa253b25eeb667456d5be8 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Fri, 8 Dec 2017 08:43:17 -0600 Subject: [PATCH 11/57] Androidplot 1.5.2 (#65) * target Android SDK 26 * update fig dependency for gradle 3.x.x support * adds custom renderer documentation * remove obsolete class / unnecessary casts * adds sizing section to plot_composition.md * adds more sizing and positioning documentation --- .circleci/config.yml | 2 +- androidplot-core/build.gradle | 6 +- .../src/main/java/com/androidplot/Plot.java | 8 +- .../java/com/androidplot/ui/Formatter.java | 6 +- .../java/com/androidplot/ui/SizeMetric.java | 1 - .../ui/widget/LegendItemOrganizer.java | 9 -- .../java/com/androidplot/xy/BarRenderer.java | 2 +- .../com/androidplot/xy/XYRegionFormatter.java | 6 +- .../src/main/res/values/attrs.xml | 51 ++++++- build.gradle | 13 +- demoapp-wearable/build.gradle | 2 +- demoapp/build.gradle | 11 +- .../demos/SimpleXYPlotActivity.java | 6 +- .../demos/TouchZoomExampleActivity.java | 8 +- .../demos/XYRegionExampleActivity.java | 12 +- .../src/main/res/layout/bar_plot_example.xml | 54 ++++---- .../src/main/res/layout/demo_app_widget.xml | 17 ++- .../res/layout/dynamic_xyplot_example.xml | 22 +-- demoapp/src/main/res/layout/main.xml | 54 +++++--- demoapp/src/main/res/layout/pie_chart.xml | 3 +- .../main/res/layout/step_chart_example.xml | 25 ++-- .../main/res/layout/time_series_example.xml | 42 +++--- .../main/res/layout/touch_zoom_example.xml | 49 +++---- demoapp/src/main/res/values-hdpi/dimens.xml | 18 --- demoapp/src/main/res/values-ldpi/dimens.xml | 20 --- demoapp/src/main/res/values/dimens.xml | 18 --- demoapp/src/main/res/values/style.xml | 28 ---- docs/attrs.md | 50 ++++++- docs/custom_renderer.md | 101 ++++++++++++++ docs/grouprenderer.md | 8 +- docs/images/rounded_bar_renderer.png | Bin 0 -> 70585 bytes docs/images/sizing/abs100x-abs100y.png | Bin 0 -> 5852 bytes docs/images/sizing/abs100x-abs150y.png | Bin 0 -> 6725 bytes docs/images/sizing/abs100x-rel1y.png | Bin 0 -> 6049 bytes docs/images/sizing/fil50x-fil50y.png | Bin 0 -> 7882 bytes docs/images/sizing/rel075x-abs100y.png | Bin 0 -> 5907 bytes docs/index.md | 1 + docs/plot_composition.md | 131 +++++++++++++++++- docs/quickstart.md | 2 +- docs/release_notes.md | 12 ++ gradle/wrapper/gradle-wrapper.properties | 4 +- 41 files changed, 527 insertions(+), 275 deletions(-) delete mode 100644 androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java delete mode 100644 demoapp/src/main/res/values-ldpi/dimens.xml delete mode 100644 demoapp/src/main/res/values/style.xml create mode 100644 docs/custom_renderer.md create mode 100644 docs/images/rounded_bar_renderer.png create mode 100644 docs/images/sizing/abs100x-abs100y.png create mode 100644 docs/images/sizing/abs100x-abs150y.png create mode 100644 docs/images/sizing/abs100x-rel1y.png create mode 100644 docs/images/sizing/fil50x-fil50y.png create mode 100644 docs/images/sizing/rel075x-abs100y.png diff --git a/.circleci/config.yml b/.circleci/config.yml index 43c50cfc..6a24c3f8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,7 +16,7 @@ jobs: # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ - - image: circleci/android:api-25-alpha + - image: circleci/android:api-26-alpha working_directory: ~/repo diff --git a/androidplot-core/build.gradle b/androidplot-core/build.gradle index 3e305c7d..a814a4c1 100644 --- a/androidplot-core/build.gradle +++ b/androidplot-core/build.gradle @@ -58,7 +58,6 @@ task generateAttrsMarkdown(type: AttrMarkdown) { android { compileSdkVersion theCompileSdkVersion - buildToolsVersion theBuildToolsVersion defaultConfig { versionCode theVersionCode @@ -74,7 +73,6 @@ android { lintOptions { abortOnError false } - buildToolsVersion theBuildToolsVersion } group = 'com.androidplot' @@ -85,8 +83,8 @@ def gitUrl = 'https://github.com/halfhp/androidplot.git' dependencies { - compile 'com.halfhp.fig:figlib:1.0.3' - compile 'com.android.support:support-annotations:25.3.1' + compile 'com.halfhp.fig:figlib:1.0.7' + compile 'com.android.support:support-annotations:27.0.2' testCompile "org.mockito:mockito-core:1.10.19" testCompile group: 'junit', name: 'junit', version: '4.12' testCompile "org.robolectric:robolectric:3.1" diff --git a/androidplot-core/src/main/java/com/androidplot/Plot.java b/androidplot-core/src/main/java/com/androidplot/Plot.java index 3ee10174..8b436199 100644 --- a/androidplot-core/src/main/java/com/androidplot/Plot.java +++ b/androidplot-core/src/main/java/com/androidplot/Plot.java @@ -520,7 +520,7 @@ private void loadAttrs(AttributeSet attrs, int defStyle) { // apply "configurator" attrs: (overrides any previously applied styleable attrs) // filter out androidplot prefixed attrs: - HashMap attrHash = new HashMap(); + HashMap attrHash = new HashMap<>(); for (int i = 0; i < attrs.getAttributeCount(); i++) { String attrName = attrs.getAttributeName(i); @@ -529,7 +529,11 @@ private void loadAttrs(AttributeSet attrs, int defStyle) { attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i)); } } - Fig.configure(getContext(), this, attrHash); + try { + Fig.configure(getContext(), this, attrHash); + } catch (FigException e) { + throw new RuntimeException(e); + } } } diff --git a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java index 8f2a354b..d920ab36 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/Formatter.java @@ -48,7 +48,11 @@ public Formatter(Context ctx, int xmlCfgId) { } public void configure(Context ctx, int xmlCfgId) { - Fig.configure(ctx, this, xmlCfgId); + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } } /** diff --git a/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java b/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java index e3819acd..d7f3e58b 100644 --- a/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java +++ b/androidplot-core/src/main/java/com/androidplot/ui/SizeMetric.java @@ -43,7 +43,6 @@ protected void validatePair(float value, SizeMode layoutType) { @Override public float getPixelValue(float size) { - //switch(layoutType) switch(getLayoutType()) { case ABSOLUTE: return getValue(); diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java deleted file mode 100644 index 536ee9d8..00000000 --- a/androidplot-core/src/main/java/com/androidplot/ui/widget/LegendItemOrganizer.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.androidplot.ui.widget; - -import java.util.List; - - -public interface LegendItemOrganizer { - - void organize(List items); -} diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java index c696bbf2..bb8c4c7f 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java @@ -238,7 +238,7 @@ protected RectF createBarRect(float w1, float h1, float w2, float h2, BarFormatt return result; } - protected void drawBar(Canvas canvas, Bar bar, RectF rect) { + protected void drawBar(Canvas canvas, Bar bar, RectF rect) { // null yVals are skipped: if(bar.getY() == null) { diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java index b0a3186f..0492c6c2 100644 --- a/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java +++ b/androidplot-core/src/main/java/com/androidplot/xy/XYRegionFormatter.java @@ -43,7 +43,11 @@ public class XYRegionFormatter { public XYRegionFormatter(Context ctx, int xmlCfgId) { // prevent configuration of classes derived from this one: if (getClass().equals(XYRegionFormatter.class)) { - Fig.configure(ctx, this, xmlCfgId); + try { + Fig.configure(ctx, this, xmlCfgId); + } catch (FigException e) { + throw new RuntimeException(e); + } } } diff --git a/androidplot-core/src/main/res/values/attrs.xml b/androidplot-core/src/main/res/values/attrs.xml index e640706e..c9dc65a0 100644 --- a/androidplot-core/src/main/res/values/attrs.xml +++ b/androidplot-core/src/main/res/values/attrs.xml @@ -16,12 +16,17 @@ --> + @@ -442,6 +447,10 @@ __dimension|float|integer__ * relative_from_left * relative_from_right * relative_from_center + +`HorizontalPositioning` component of the `HorizontalPosition` of the `TextLabelWidget` +that displays the domain title. +See [Positioning Widgets](plot_composition.md#positioning-widgets) documentation. --> + xmlns:ap="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + ap:title="Growth" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:progress="10" /> + android:progress="1" /> + android:checked="true" + android:text="Series 1" /> + android:checked="true" + android:text="Series 2" /> \ No newline at end of file diff --git a/demoapp/src/main/res/layout/demo_app_widget.xml b/demoapp/src/main/res/layout/demo_app_widget.xml index 85b5fe6b..81977453 100644 --- a/demoapp/src/main/res/layout/demo_app_widget.xml +++ b/demoapp/src/main/res/layout/demo_app_widget.xml @@ -17,14 +17,13 @@ --> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - - + \ No newline at end of file diff --git a/demoapp/src/main/res/layout/dynamic_xyplot_example.xml b/demoapp/src/main/res/layout/dynamic_xyplot_example.xml index ddfd0cba..5b0c5644 100644 --- a/demoapp/src/main/res/layout/dynamic_xyplot_example.xml +++ b/demoapp/src/main/res/layout/dynamic_xyplot_example.xml @@ -1,5 +1,4 @@ - - + xmlns:ap="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + ap:legendTextSize="15sp" + ap:rangeTitle="Range" + ap:title="A Dynamic XY Plot" /> diff --git a/demoapp/src/main/res/layout/main.xml b/demoapp/src/main/res/layout/main.xml index 65a43ef9..590087de 100644 --- a/demoapp/src/main/res/layout/main.xml +++ b/demoapp/src/main/res/layout/main.xml @@ -32,99 +32,117 @@