legendItemComparator) {
+ this.legendItemComparator = legendItemComparator;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java
index 8e86f8cf..06bf54ff 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/TextLabelWidget.java
@@ -21,13 +21,9 @@
import com.androidplot.util.FontUtils;
public class TextLabelWidget extends Widget {
- private static final String TAG = TextLabelWidget.class.getName();
-
private String text;
private Paint labelPaint;
-
- private TextOrientationType orientation;
-
+ private TextOrientation orientation;
private boolean autoPackEnabled = true;
{
@@ -35,19 +31,20 @@ public class TextLabelWidget extends Widget {
labelPaint.setColor(Color.WHITE);
labelPaint.setAntiAlias(true);
labelPaint.setTextAlign(Paint.Align.CENTER);
+ setClippingEnabled(false);
}
public TextLabelWidget(LayoutManager layoutManager, Size size) {
- this(layoutManager, size, TextOrientationType.HORIZONTAL);
+ this(layoutManager, size, TextOrientation.HORIZONTAL);
}
- public TextLabelWidget(LayoutManager layoutManager, String title, Size size, TextOrientationType orientation) {
+ public TextLabelWidget(LayoutManager layoutManager, String title, Size size, TextOrientation orientation) {
this(layoutManager, size, orientation);
setText(title);
}
- public TextLabelWidget(LayoutManager layoutManager, Size size, TextOrientationType orientation) {
- super(layoutManager, new Size(0, SizeLayoutType.ABSOLUTE, 0, SizeLayoutType.ABSOLUTE));
+ public TextLabelWidget(LayoutManager layoutManager, Size size, TextOrientation orientation) {
+ super(layoutManager, new Size(0, SizeMode.ABSOLUTE, 0, SizeMode.ABSOLUTE));
setSize(size);
this.orientation = orientation;
}
@@ -66,25 +63,21 @@ public void onPostInit() {
}
}
- //protected abstract String getText();
-
/**
* Sets the dimensions of the widget to exactly contain the text contents
*/
public void pack() {
- //Log.d(TAG, "Packing...");
Rect size = FontUtils.getStringDimensions(text, getLabelPaint());
if(size == null) {
- //Log.w(TAG, "Attempt to pack empty text.");
return;
}
switch(orientation) {
case HORIZONTAL:
- setSize(new Size(size.height(), SizeLayoutType.ABSOLUTE, size.width()+2, SizeLayoutType.ABSOLUTE));
+ setSize(new Size(size.height(), SizeMode.ABSOLUTE, size.width()+2, SizeMode.ABSOLUTE));
break;
case VERTICAL_ASCENDING:
case VERTICAL_DESCENDING:
- setSize(new Size(size.width(), SizeLayoutType.ABSOLUTE, size.height()+2, SizeLayoutType.ABSOLUTE));
+ setSize(new Size(size.width(), SizeMode.ABSOLUTE, size.height()+2, SizeMode.ABSOLUTE));
break;
}
refreshLayout();
@@ -102,16 +95,13 @@ public void doOnDraw(Canvas canvas, RectF widgetRect) {
if(text == null || text.length() == 0) {
return;
}
- //FontUtils.getStringDimensions(text, labelPaint);
+
float vOffset = labelPaint.getFontMetrics().descent;
PointF start = getAnchorCoordinates(widgetRect,
- AnchorPosition.CENTER);
-
- // BEGIN ROTATION CALCULATION
- //int canvasState = canvas.save(Canvas.ALL_SAVE_FLAG);
+ Anchor.CENTER);
try {
- canvas.save(Canvas.ALL_SAVE_FLAG);
+ canvas.save();
canvas.translate(start.x, start.y);
switch (orientation) {
case HORIZONTAL:
@@ -128,11 +118,8 @@ public void doOnDraw(Canvas canvas, RectF widgetRect) {
}
canvas.drawText(text, 0, vOffset, labelPaint);
} finally {
- //canvas.restoreToCount(canvasState);
canvas.restore();
}
-
- // END ROTATION CALCULATION
}
public Paint getLabelPaint() {
@@ -142,18 +129,18 @@ public Paint getLabelPaint() {
public void setLabelPaint(Paint labelPaint) {
this.labelPaint = labelPaint;
- // when paint changes, packing params change too so check
+ // when paint changes, packing params change too so run
// to see if we need to resize:
if(autoPackEnabled) {
pack();
}
}
- public TextOrientationType getOrientation() {
+ public TextOrientation getOrientation() {
return orientation;
}
- public void setOrientation(TextOrientationType orientation) {
+ public void setOrientation(TextOrientation orientation) {
this.orientation = orientation;
if(autoPackEnabled) {
pack();
@@ -172,7 +159,6 @@ public void setAutoPackEnabled(boolean autoPackEnabled) {
}
public void setText(String text) {
- //Log.d(TAG, "Setting textLabel to: " + text);
this.text = text;
if(autoPackEnabled) {
pack();
diff --git a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java
index bda3f47c..6d21767b 100644
--- a/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java
+++ b/androidplot-core/src/main/java/com/androidplot/ui/widget/Widget.java
@@ -17,11 +17,13 @@
package com.androidplot.ui.widget;
import android.graphics.*;
-import com.androidplot.exception.PlotRenderException;
+import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+
import com.androidplot.ui.*;
import com.androidplot.util.DisplayDimensions;
-import com.androidplot.ui.XLayoutStyle;
-import com.androidplot.ui.YLayoutStyle;
+import com.androidplot.ui.HorizontalPositioning;
+import com.androidplot.ui.VerticalPositioning;
import com.androidplot.util.PixelUtils;
/**
@@ -32,7 +34,7 @@ public abstract class Widget implements BoxModelable, Resizable {
private Paint borderPaint;
private Paint backgroundPaint;
- private boolean clippingEnabled = true;
+ private boolean clippingEnabled = false;
private BoxModel boxModel = new BoxModel();
private Size size;
private DisplayDimensions plotDimensions = new DisplayDimensions();
@@ -41,6 +43,16 @@ public abstract class Widget implements BoxModelable, Resizable {
private PositionMetrics positionMetrics;
private LayoutManager layoutManager;
+ private Rotation rotation = Rotation.NONE;
+ private RectF lastWidgetRect = null;
+
+ public enum Rotation {
+ NINETY_DEGREES,
+ NEGATIVE_NINETY_DEGREES,
+ ONE_HUNDRED_EIGHTY_DEGREES,
+ NONE,
+ }
+
public Widget(LayoutManager layoutManager, SizeMetric heightMetric, SizeMetric widthMetric) {
this(layoutManager, new Size(heightMetric, widthMetric));
}
@@ -56,37 +68,38 @@ public DisplayDimensions getWidgetDimensions() {
return widgetDimensions;
}
- public AnchorPosition getAnchor() {
+ public Anchor getAnchor() {
return getPositionMetrics().getAnchor();
}
- public void setAnchor(AnchorPosition anchor) {
+ public void setAnchor(Anchor anchor) {
getPositionMetrics().setAnchor(anchor);
}
/**
- * Same as {@link #position(float, com.androidplot.ui.XLayoutStyle, float, com.androidplot.ui.YLayoutStyle, com.androidplot.ui.AnchorPosition)}
+ * Same as {@link #position(float, HorizontalPositioning, float, VerticalPositioning, Anchor)}
* but with the anchor parameter defaulted to the upper left corner.
+ *
* @param x
- * @param xLayoutStyle
+ * @param horizontalPositioning
* @param y
- * @param yLayoutStyle
+ * @param verticalPositioning
*/
- public void position(float x, XLayoutStyle xLayoutStyle, float y, YLayoutStyle yLayoutStyle) {
- position(x, xLayoutStyle, y, yLayoutStyle, AnchorPosition.LEFT_TOP);
+ public void position(float x, HorizontalPositioning horizontalPositioning, float y, VerticalPositioning verticalPositioning) {
+ position(x, horizontalPositioning, y, verticalPositioning, Anchor.LEFT_TOP);
}
/**
- * @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1.
- * @param xLayoutStyle LayoutType to use when orienting this element's X-Coordinate.
- * @param y Y_VALS_ONLY-Coordinate of the top-left corner of element. When using RELATIVE, must be a value between 0 and 1.
- * @param yLayoutStyle LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate.
- * @param anchor The point of reference used by this positioning call.
+ * @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1.
+ * @param horizontalPositioning LayoutType to use when orienting this element's X-Coordinate.
+ * @param y Y_VALS_ONLY-Coordinate of the top-left corner of element. When using RELATIVE, must be a value between 0 and 1.
+ * @param verticalPositioning LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate.
+ * @param anchor The point of reference used by this positioning call.
*/
- public void position(float x, XLayoutStyle xLayoutStyle, float y,
- YLayoutStyle yLayoutStyle, AnchorPosition anchor) {
- setPositionMetrics(new PositionMetrics(x, xLayoutStyle, y, yLayoutStyle, anchor));
+ public void position(float x, HorizontalPositioning horizontalPositioning, float y,
+ VerticalPositioning verticalPositioning, Anchor anchor) {
+ setPositionMetrics(new PositionMetrics(x, horizontalPositioning, y, verticalPositioning, anchor));
layoutManager.addToTop(this);
}
@@ -113,7 +126,6 @@ public void onPostInit() {
* @return
*/
public boolean containsPoint(PointF point) {
- //return outlineRect != null && outlineRect.contains(point.x, point.y);
return widgetDimensions.canvasRect.contains(point.x, point.y);
}
@@ -130,7 +142,7 @@ public void setWidth(float width) {
size.getWidth().setValue(width);
}
- public void setWidth(float width, SizeLayoutType layoutType) {
+ public void setWidth(float width, SizeMode layoutType) {
size.getWidth().set(width, layoutType);
}
@@ -138,7 +150,7 @@ public void setHeight(float height) {
size.getHeight().setValue(height);
}
- public void setHeight(float height, SizeLayoutType layoutType) {
+ public void setHeight(float height, SizeMode layoutType) {
size.getHeight().set(height, layoutType);
}
@@ -263,7 +275,7 @@ public float getMarginRight() {
* into this Widget's size or position is altered.
*/
public synchronized void refreshLayout() {
- if(positionMetrics == null) {
+ if (positionMetrics == null) {
// make sure positionMetrics have been set. this method can be
// automatically called during xml configuration of certain params
// before the widget is fully configured.
@@ -271,7 +283,7 @@ public synchronized void refreshLayout() {
}
float elementWidth = getWidthPix(plotDimensions.paddedRect.width());
float elementHeight = getHeightPix(plotDimensions.paddedRect.height());
- PointF coords = getElementCoordinates(elementHeight,
+ PointF coords = calculateCoordinates(elementHeight,
elementWidth, plotDimensions.paddedRect, positionMetrics);
RectF widgetRect = new RectF(coords.x, coords.y,
@@ -288,79 +300,132 @@ public synchronized void layout(final DisplayDimensions plotDimensions) {
refreshLayout();
}
- public PointF getElementCoordinates(float height, float width, RectF viewRect, PositionMetrics metrics) {
- float x = metrics.getXPositionMetric().getPixelValue(viewRect.width()) + viewRect.left;
- float y = metrics.getYPositionMetric().getPixelValue(viewRect.height()) + viewRect.top;
- PointF point = new PointF(x, y);
- return PixelUtils.sub(point, getAnchorOffset(width, height, metrics.getAnchor()));
- }
- public static PointF getAnchorOffset(float width, float height, AnchorPosition anchorPosition) {
- PointF point = new PointF();
- switch (anchorPosition) {
- case LEFT_TOP:
- break;
- case LEFT_MIDDLE:
- point.set(0, height / 2);
- break;
- case LEFT_BOTTOM:
- point.set(0, height);
- break;
- case RIGHT_TOP:
- point.set(width, 0);
- break;
- case RIGHT_BOTTOM:
- point.set(width, height);
- break;
- case RIGHT_MIDDLE:
- point.set(width, height / 2);
- break;
- case TOP_MIDDLE:
- point.set(width / 2, 0);
- break;
- case BOTTOM_MIDDLE:
- point.set(width / 2, height);
- break;
- case CENTER:
- point.set(width / 2, height / 2);
- break;
- default:
- throw new IllegalArgumentException("Unsupported anchor location: " + anchorPosition);
- }
- return point;
+ public static PointF calculateCoordinates(float height, float width, RectF viewRect, PositionMetrics metrics) {
+ float x = metrics.getXPositionMetric().getPixelValue(viewRect.width()) + viewRect.left;
+ float y = metrics.getYPositionMetric().getPixelValue(viewRect.height()) + viewRect.top;
+ PointF point = new PointF(x, y);
+ return PixelUtils.sub(point, getAnchorOffset(width, height, metrics.getAnchor()));
+ }
+
+ public static PointF getAnchorOffset(float width, float height, Anchor anchor) {
+ PointF point = new PointF();
+ switch (anchor) {
+ case LEFT_TOP:
+ break;
+ case LEFT_MIDDLE:
+ point.set(0, height / 2);
+ break;
+ case LEFT_BOTTOM:
+ point.set(0, height);
+ break;
+ case RIGHT_TOP:
+ point.set(width, 0);
+ break;
+ case RIGHT_BOTTOM:
+ point.set(width, height);
+ break;
+ case RIGHT_MIDDLE:
+ point.set(width, height / 2);
+ break;
+ case TOP_MIDDLE:
+ point.set(width / 2, 0);
+ break;
+ case BOTTOM_MIDDLE:
+ point.set(width / 2, height);
+ break;
+ case CENTER:
+ point.set(width / 2, height / 2);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported anchor location: " + anchor);
}
+ return point;
+ }
- public static PointF getAnchorCoordinates(RectF widgetRect, AnchorPosition anchorPosition) {
- return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top),
- getAnchorOffset(widgetRect.width(), widgetRect.height(), anchorPosition));
- }
+ public static PointF getAnchorCoordinates(RectF widgetRect, Anchor anchor) {
+ return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top),
+ getAnchorOffset(widgetRect.width(), widgetRect.height(), anchor));
+ }
+
+ public static PointF getAnchorCoordinates(float x, float y, float width, float height, Anchor anchor) {
+ return getAnchorCoordinates(new RectF(x, y, x + width, y + height), anchor);
+ }
- public static PointF getAnchorCoordinates(float x, float y, float width, float height, AnchorPosition anchorPosition) {
- return getAnchorCoordinates(new RectF(x, y, x+width, y+height), anchorPosition);
+ private void checkSize(@NonNull RectF widgetRect) {
+ if (lastWidgetRect == null || !lastWidgetRect.equals(widgetRect)) {
+ onResize(lastWidgetRect, widgetRect);
}
+ lastWidgetRect = widgetRect;
+ }
- public void draw(Canvas canvas, RectF widgetRect) throws PlotRenderException {
- //outlineRect = widgetRect;
+ /**
+ * Called whenever the height or width of the Widget's reserved space has changed,
+ * immediately before {@link #doOnDraw(Canvas, RectF)}.
+ * May be used to efficiently carry out expensive operations only when necessary.
+ *
+ * @param oldRect
+ * @param newRect
+ */
+ protected void onResize(@Nullable RectF oldRect, @NonNull RectF newRect) {
+ // do nothing by default
+ }
+
+ public void draw(Canvas canvas) {
if (isVisible()) {
if (backgroundPaint != null) {
drawBackground(canvas, widgetDimensions.canvasRect);
}
-
- /* RectF marginatedRect = new RectF(outlineRect.left + marginLeft,
- outlineRect.top + marginTop,
- outlineRect.right - marginRight,
- outlineRect.bottom - marginBottom);*/
-
- /*RectF marginatedRect = boxModel.getMarginatedRect(widgetRect);
- RectF paddedRect = boxModel.getPaddedRect(marginatedRect);*/
- doOnDraw(canvas, widgetDimensions.paddedRect);
+ canvas.save();
+ final RectF widgetRect = applyRotation(canvas, widgetDimensions.paddedRect);
+ checkSize(widgetRect);
+ doOnDraw(canvas, widgetRect);
+ canvas.restore();
if (borderPaint != null) {
- drawBorder(canvas, widgetDimensions.paddedRect);
+ drawBorder(canvas, widgetRect);
}
}
}
+ protected RectF applyRotation(Canvas canvas, RectF rect) {
+ float rotationDegs = 0;
+ final float cx = widgetDimensions.paddedRect.centerX();
+ final float cy = widgetDimensions.paddedRect.centerY();
+ final float halfHeight = widgetDimensions.paddedRect.height() / 2;
+ final float halfWidth = widgetDimensions.paddedRect.width() / 2;
+ switch (rotation) {
+ case NINETY_DEGREES:
+ rotationDegs = 90;
+ rect = new RectF(
+ cx - halfHeight,
+ cy - halfWidth,
+ cx + halfHeight,
+ cy + halfWidth);
+ break;
+ case NEGATIVE_NINETY_DEGREES:
+ rotationDegs = -90;
+ rect = new RectF(
+ cx - halfHeight,
+ cy - halfWidth,
+ cx + halfHeight,
+ cy + halfWidth);
+ break;
+ case ONE_HUNDRED_EIGHTY_DEGREES:
+ rotationDegs = 180;
+ // fall through
+ case NONE:
+ break;
+ default:
+ throw new UnsupportedOperationException("Not yet implemented.");
+
+ }
+ if (rotation != Rotation.NONE) {
+ canvas.rotate(rotationDegs, cx, cy);
+ }
+ return rect;
+ }
+
protected void drawBorder(Canvas canvas, RectF paddedRect) {
canvas.drawRect(paddedRect, borderPaint);
}
@@ -373,7 +438,7 @@ protected void drawBackground(Canvas canvas, RectF widgetRect) {
* @param canvas The Canvas to draw onto
* @param widgetRect the size and coordinates of this widget
*/
- protected abstract void doOnDraw(Canvas canvas, RectF widgetRect) throws PlotRenderException;
+ protected abstract void doOnDraw(Canvas canvas, RectF widgetRect);
public Paint getBorderPaint() {
return borderPaint;
@@ -414,4 +479,12 @@ public PositionMetrics getPositionMetrics() {
public void setPositionMetrics(PositionMetrics positionMetrics) {
this.positionMetrics = positionMetrics;
}
+
+ public Rotation getRotation() {
+ return rotation;
+ }
+
+ public void setRotation(Rotation rotation) {
+ this.rotation = rotation;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/APTrace.java b/androidplot-core/src/main/java/com/androidplot/util/APTrace.java
new file mode 100644
index 00000000..88e6317a
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/util/APTrace.java
@@ -0,0 +1,26 @@
+package com.androidplot.util;
+
+import android.os.*;
+
+/**
+ * Wraps {@link Trace} to provide API-safe methods as well as an easy target for runtime removal
+ * via obfuscation.
+ */
+public abstract class APTrace {
+
+ public static void begin(final String sectionName) {
+ if(Build.VERSION.SDK_INT >= 18) {
+ Trace.beginSection(sectionName);
+ } else {
+ // TODO: alternate impl?
+ }
+ }
+
+ public static void end() {
+ if(Build.VERSION.SDK_INT >= 18) {
+ Trace.endSection();
+ } else {
+ // TODO: alternate impl?
+ }
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java
index 318687e0..853ae25a 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/AttrUtils.java
@@ -18,11 +18,13 @@
import android.content.res.TypedArray;
import android.graphics.Paint;
-import android.util.TypedValue;
+import android.util.*;
+
import com.androidplot.ui.*;
+import com.androidplot.ui.Size;
import com.androidplot.ui.widget.Widget;
-import com.androidplot.xy.XYStepMode;
-import com.androidplot.xy.XYStepModel;
+import com.androidplot.xy.StepMode;
+import com.androidplot.xy.StepModel;
/**
* Methods for applying styleable attributes.
@@ -30,6 +32,16 @@
*/
public class AttrUtils {
+ private static final String TAG = AttrUtils.class.getName();
+
+ public static void configureInsets(TypedArray attrs, Insets insets,
+ int topAttr, int bottomAttr, int leftAttr, int rightAttr) {
+ insets.setTop(attrs.getDimension(topAttr, insets.getTop()));
+ insets.setBottom(attrs.getDimension(bottomAttr, insets.getBottom()));
+ insets.setLeft(attrs.getDimension(leftAttr, insets.getLeft()));
+ insets.setRight(attrs.getDimension(rightAttr, insets.getRight()));
+ }
+
/**
* Configure a {@link Paint} instance used for drawing text from xml attrs.
* @param attrs
@@ -37,10 +49,44 @@ public class AttrUtils {
* @param colorAttr
* @param textSizeAttr
*/
- public static void configureTextPaint(TypedArray attrs, Paint paint, int colorAttr, int textSizeAttr) {
+ public static void configureTextPaint(TypedArray attrs, Paint paint,
+ int colorAttr, int textSizeAttr) {
+ configureTextPaint(attrs, paint, colorAttr, textSizeAttr, null);
+ }
+
+ /**
+ * Configure a {@link Paint} instance used for drawing text from xml attrs.
+ * @param attrs
+ * @param paint
+ * @param colorAttr
+ * @param textSizeAttr
+ * @param alignAttr
+ */
+ public static void configureTextPaint(TypedArray attrs, Paint paint, int colorAttr,
+ int textSizeAttr, Integer alignAttr) {
if(attrs != null) {
setColor(attrs, paint, colorAttr);
setTextSize(attrs, paint, textSizeAttr);
+
+ if(alignAttr != null && attrs.hasValue(alignAttr)) {
+ configureTextAlign(attrs, paint, alignAttr);
+ }
+ }
+ }
+
+ /**
+ * Configure {@link Paint} text alignment from xml attrs.
+ * @param attrs
+ * @param paint
+ * @param alignAttr
+ */
+ public static void configureTextAlign(TypedArray attrs, Paint paint, int alignAttr) {
+ if (attrs != null) {
+ //if(attrs.hasValue(alignAttr)) {
+ final Paint.Align alignment = Paint.Align.values()
+ [attrs.getInt(alignAttr, paint.getTextAlign().ordinal())];
+ paint.setTextAlign(alignment);
+ //}
}
}
@@ -58,11 +104,15 @@ public static void configureLinePaint(TypedArray attrs, Paint paint, int colorAt
}
}
- private static void setColor(TypedArray attrs, Paint paint, int attrId) {
- paint.setColor(attrs.getColor(attrId, paint.getColor()));
+ public static void setColor(TypedArray attrs, Paint paint, int attrId) {
+ if(paint == null) {
+ Log.w(TAG, "Attempt to configure null Paint property for attrId: " + attrId);
+ } else {
+ paint.setColor(attrs.getColor(attrId, paint.getColor()));
+ }
}
- private static void setTextSize(TypedArray attrs, Paint paint, int attrId) {
+ public static void setTextSize(TypedArray attrs, Paint paint, int attrId) {
paint.setTextSize(attrs.getDimension(attrId, paint.getTextSize()));
}
@@ -115,14 +165,14 @@ public static void configureSize(TypedArray attrs, Size model, int heightSizeLay
private static void configureSizeMetric(TypedArray attrs, SizeMetric model, int typeAttr, int valueAttr) {
final float value = getIntFloatDimenValue(attrs, valueAttr, model.getValue()).floatValue();
- final SizeLayoutType sizeLayoutType =
+ final SizeMode sizeMode =
getSizeLayoutType(attrs, typeAttr, model.getLayoutType());
- model.set(value, sizeLayoutType);
+ model.set(value, sizeMode);
}
- private static SizeLayoutType getSizeLayoutType(TypedArray attrs, int attr, SizeLayoutType defaultValue) {
- return SizeLayoutType.values()[attrs.getInt(attr, defaultValue.ordinal())];
+ private static SizeMode getSizeLayoutType(TypedArray attrs, int attr, SizeMode defaultValue) {
+ return SizeMode.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
public static void configureWidget(TypedArray attrs, Widget widget, int heightSizeLayoutTypeAttr, int heightAttr,
@@ -138,6 +188,12 @@ public static void configureWidget(TypedArray attrs, Widget widget, int heightSi
}
}
+ public static void configureWidgetRotation(TypedArray attrs, Widget widget, int rotationAttr) {
+ if(attrs != null) {
+ widget.setRotation(getWidgetRotation(attrs, rotationAttr, Widget.Rotation.NONE));
+ }
+ }
+
/**
* Configure a {@link Widget} from xml attrs.
* @param attrs
@@ -151,7 +207,7 @@ public static void configureWidget(TypedArray attrs, Widget widget, int heightSi
public static void configurePositionMetrics(TypedArray attrs, PositionMetrics metrics, int xLayoutStyleAttr,
int xLayoutValueAttr, int yLayoutStyleAttr, int yLayoutValueAttr,
int anchorPositionAttr) {
- if(attrs != null) {
+ if(attrs != null && metrics != null) {
metrics.getXPositionMetric().set(
getIntFloatDimenValue(attrs, xLayoutValueAttr, metrics.getXPositionMetric().getValue()).floatValue(),
getXLayoutStyle(attrs, xLayoutStyleAttr, metrics.getXPositionMetric().getLayoutType()));
@@ -181,27 +237,31 @@ private static Number getIntFloatDimenValue(TypedArray attrs, int valueAttr, Num
} else if (valueType == TypedValue.TYPE_FLOAT) {
result = attrs.getFloat(valueAttr, defaultValue.floatValue());
} else {
- throw new IllegalArgumentException("Invalid value type - must be float or dimension.");
+ throw new IllegalArgumentException("Invalid value type - must be int, float or dimension.");
}
}
return result;
}
- private static XLayoutStyle getXLayoutStyle(TypedArray attrs, int attr, XLayoutStyle defaultValue) {
- return XLayoutStyle.values()[attrs.getInt(attr, defaultValue.ordinal())];
+ private static HorizontalPositioning getXLayoutStyle(TypedArray attrs, int attr, HorizontalPositioning defaultValue) {
+ return HorizontalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())];
+ }
+
+ private static VerticalPositioning getYLayoutStyle(TypedArray attrs, int attr, VerticalPositioning defaultValue) {
+ return VerticalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
- private static YLayoutStyle getYLayoutStyle(TypedArray attrs, int attr, YLayoutStyle defaultValue) {
- return YLayoutStyle.values()[attrs.getInt(attr, defaultValue.ordinal())];
+ private static Widget.Rotation getWidgetRotation(TypedArray attrs, int attr, Widget.Rotation defaultValue) {
+ return Widget.Rotation.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
- private static AnchorPosition getAnchorPosition(TypedArray attrs, int attr, AnchorPosition defaultValue) {
- return AnchorPosition.values()[attrs.getInt(attr, defaultValue.ordinal())];
+ private static Anchor getAnchorPosition(TypedArray attrs, int attr, Anchor defaultValue) {
+ return Anchor.values()[attrs.getInt(attr, defaultValue.ordinal())];
}
- public static void configureStep(TypedArray attrs, XYStepModel model, int stepModeAttr, int stepValueAttr) {
+ public static void configureStep(TypedArray attrs, StepModel model, int stepModeAttr, int stepValueAttr) {
if(attrs != null) {
- model.setMode(XYStepMode.values()[attrs.getInt(stepModeAttr, model.getMode().ordinal())]);
+ model.setMode(StepMode.values()[attrs.getInt(stepModeAttr, model.getMode().ordinal())]);
model.setValue(getIntFloatDimenValue(attrs, stepValueAttr, model.getValue()).doubleValue());
}
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/Configurator.java b/androidplot-core/src/main/java/com/androidplot/util/Configurator.java
deleted file mode 100644
index ac2eb063..00000000
--- a/androidplot-core/src/main/java/com/androidplot/util/Configurator.java
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.androidplot.util;
-
-import android.content.Context;
-import android.content.res.XmlResourceParser;
-import android.graphics.Color;
-import android.util.Log;
-import android.util.TypedValue;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Type;
-import java.util.HashMap;
-
-/**
- * Utility class for "configuring" objects via XML config files. Supports the following field types:
- * String
- * Enum
- * int
- * float
- * boolean
- *
- * Config files should be stored in /res/xml. Given the XML configuration /res/xml/myConfig.xml, one can apply the
- * configuration to an Object instance as follows:
- *
- * MyObject obj = new MyObject();
- * Configurator.configure(obj, R.xml.myConfig);
- *
- * WHAT IT DOES:
- * Given a series of parameters stored in an XML file, Configurator iterates through each parameter, using the name
- * as a map to the field within a given object. For example:
- *
- *
- * {@code
- *
- * }
- *
- *
- * Given a Car instance car and assuming the method setCondition(String) exists within the SparkPlug class,
- * Configurator does the following:
- *
- *
- * {@code
- * car.getEngine().getSparkPlug().setCondition("poor");
- * }
- *
- *
- * Now let's pretend that setCondition takes an instance of the Condition enum as it's argument.
- * Configurator then does the following:
- *
- * car.getEngine().getSparkPlug().setCondition(Condition.valueOf("poor");
- *
- * Now let's look at how ints are handled. Given the following xml:
- *
- *
- *
- * would result in:
- * car.getEngine.setMiles(Integer.ParseInt("100000");
- *
- * That's pretty straight forward. But colors are expressed as ints too in Android
- * but can be defined using hex values or even names of colors. When Configurator
- * attempts to parse a parameter for a method that it knows takes an int as it's argument,
- * Configurator will first attempt to parse the parameter as a color. Only after this
- * attempt fails will Configurator resort to Integer.ParseInt. So:
- *
- *
- *
- * would result in:
- * car.getHood().getPaint().setColor(Color.parseColor("Red");
- *
- * Next lets talk about float. Floats can appear in XML a few different ways in Android,
- * especially when it comes to defining dimensions:
- *
- *
- *
- * Configurator will correctly parse each of these into their corresponding real pixel value expressed as a float.
- *
- * One last thing to keep in mind when using Configurator:
- * Values for Strings and ints can be assigned to localized values, allowing
- * a cleaner solution for those developing apps to run on multiple form factors
- * or in multiple languages:
- *
- *
- */
-@SuppressWarnings("WeakerAccess")
-public abstract class Configurator {
-
- private static final String TAG = Configurator.class.getName();
- protected static final String CFG_ELEMENT_NAME = "config";
-
- protected static int parseResId(Context ctx, String prefix, String value) {
- String[] split = value.split("/");
- // is this a localized resource?
- if (split.length > 1 && split[0].equalsIgnoreCase(prefix)) {
- String pack = split[0].replace("@", "");
- String name = split[1];
- return ctx.getResources().getIdentifier(name, pack, ctx.getPackageName());
- } else {
- throw new IllegalArgumentException();
- }
- }
-
- protected static int parseIntAttr(Context ctx, String value) {
- try {
- return ctx.getResources().getColor(parseResId(ctx, "@color", value));
- } catch (IllegalArgumentException e1) {
- try {
- return Color.parseColor(value);
- } catch (IllegalArgumentException e2) {
- // wasn't a color so try parsing as a plain old int:
- return Integer.parseInt(value);
- }
- }
- }
-
- /**
- * Treats value as a float parameter. First value is tested to see whether
- * it contains a resource identifier. Failing that, it is tested to see whether
- * a dimension suffix (dp, em, mm etc.) exists. Failing that, it is evaluated as
- * a plain old float.
- * @param ctx
- * @param value
- * @return
- */
- protected static float parseFloatAttr(Context ctx, String value) {
- try {
- return ctx.getResources().getDimension(parseResId(ctx, "@dimen", value));
- } catch (IllegalArgumentException e1) {
- try {
- return PixelUtils.stringToDimension(value);
- } catch (Exception e2) {
- return Float.parseFloat(value);
- }
- }
- }
-
- protected static String parseStringAttr(Context ctx, String value) {
- try {
- return ctx.getResources().getString(parseResId(ctx, "@string", value));
- } catch (IllegalArgumentException e1) {
- return value;
- }
- }
-
-
- protected static Method getSetter(Class clazz, final String fieldId) throws NoSuchMethodException {
- Method[] methods = clazz.getMethods();
-
- String methodName = "set" + fieldId;
- for (Method method : methods) {
- if (method.getName().equalsIgnoreCase(methodName)) {
- return method;
- }
- }
- throw new NoSuchMethodException("No such public method (case insensitive): " +
- methodName + " in " + clazz);
- }
-
- @SuppressWarnings("unchecked")
- protected static Method getGetter(Class clazz, final String fieldId) throws NoSuchMethodException {
- Log.d(TAG, "Attempting to find getter for " + fieldId + " in class " + clazz.getName());
- String firstLetter = fieldId.substring(0, 1);
- String methodName = "get" + firstLetter.toUpperCase() + fieldId.substring(1, fieldId.length());
- return clazz.getMethod(methodName);
- }
-
- /**
- * Returns the object containing the field specified by path.
- * @param obj
- * @param path Path through member hierarchy to the destination field.
- * @return null if the object at path cannot be found.
- * @throws java.lang.reflect.InvocationTargetException
- *
- * @throws IllegalAccessException
- */
- protected static Object getObjectContaining(Object obj, String path) throws
- InvocationTargetException, IllegalAccessException, NoSuchMethodException {
- if(obj == null) {
- throw new NullPointerException("Attempt to call getObjectContaining(Object obj, String path) " +
- "on a null Object instance. Path was: " + path);
- }
- Log.d(TAG, "Looking up object containing: " + path);
- int separatorIndex = path.indexOf(".");
-
- // not there yet, descend deeper:
- if (separatorIndex > 0) {
- String lhs = path.substring(0, separatorIndex);
- String rhs = path.substring(separatorIndex + 1, path.length());
-
- // use getter to retrieve the instance
- Method m = getGetter(obj.getClass(), lhs);
- if(m == null) {
- throw new NullPointerException("No getter found for field: " + lhs + " within " + obj.getClass());
- }
- Log.d(TAG, "Invoking " + m.getName() + " on instance of " + obj.getClass().getName());
- Object o = m.invoke(obj);
- // delve into o
- return getObjectContaining(o, rhs);
- //} catch (NoSuchMethodException e) {
- // TODO: log a warning
- // return null;
- //}
- } else {
- // found it!
- return obj;
- }
- }
-
- @SuppressWarnings("unchecked")
- private static Object[] inflateParams(Context ctx, Class[] params, String[] vals) throws NoSuchMethodException,
- InvocationTargetException, IllegalAccessException {
- Object[] out = new Object[params.length];
- int i = 0;
- for (Class param : params) {
- if (Enum.class.isAssignableFrom(param)) {
- out[i] = param.getMethod("valueOf", String.class).invoke(null, vals[i].toUpperCase());
- } else if (param.equals(Float.TYPE)) {
- out[i] = parseFloatAttr(ctx, vals[i]);
- } else if (param.equals(Integer.TYPE)) {
- out[i] = parseIntAttr(ctx, vals[i]);
- } else if (param.equals(Boolean.TYPE)) {
- out[i] = Boolean.valueOf(vals[i]);
- } else if (param.equals(String.class)) {
- out[i] = parseStringAttr(ctx, vals[i]);
- } else {
- throw new IllegalArgumentException(
- "Error inflating XML: Setter requires param of unsupported type: " + param);
- }
- i++;
- }
- return out;
- }
-
- /**
- *
- * @param ctx
- * @param obj
- * @param xmlFileId ID of the XML config file within /res/xml
- */
- public static void configure(Context ctx, Object obj, int xmlFileId) {
- XmlResourceParser xrp = ctx.getResources().getXml(xmlFileId);
- try {
- HashMap params = new HashMap();
- while (xrp.getEventType() != XmlResourceParser.END_DOCUMENT) {
- xrp.next();
- String name = xrp.getName();
- if (xrp.getEventType() == XmlResourceParser.START_TAG) {
- if (name.equalsIgnoreCase(CFG_ELEMENT_NAME))
- for (int i = 0; i < xrp.getAttributeCount(); i++) {
- params.put(xrp.getAttributeName(i), xrp.getAttributeValue(i));
- }
- break;
- }
- }
- configure(ctx, obj, params);
- } catch (XmlPullParserException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- xrp.close();
- }
- }
-
- public static void configure(Context ctx, Object obj, HashMap params) {
- for (String key : params.keySet()) {
- try {
- configure(ctx, obj, key, params.get(key));
- } catch (InvocationTargetException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- } catch (NoSuchMethodException e) {
- Log.w(TAG, "Error inflating XML: Setter for field \"" + key + "\" does not exist. ");
- e.printStackTrace();
- }
- }
- }
-
- /**
- * Recursively descend into an object using key as the pathway and invoking the corresponding setter
- * if one exists.
- *
- * @param key
- * @param value
- */
- protected static void configure(Context ctx, Object obj, String key, String value)
- throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
- Object o = getObjectContaining(obj, key);
- if (o != null) {
- int idx = key.lastIndexOf(".");
- String fieldId = idx > 0 ? key.substring(idx + 1, key.length()) : key;
-
- Method m = getSetter(o.getClass(), fieldId);
- Class[] paramTypes = m.getParameterTypes();
- // TODO: add support for generic type params
- if (paramTypes.length >= 1) {
-
- // split on "|"
- // TODO: add support for String args containing a |
- String[] paramStrs = value.split("\\|");
- if (paramStrs.length == paramTypes.length) {
-
- Object[] oa = inflateParams(ctx, paramTypes, paramStrs);
- Log.d(TAG, "Invoking " + m.getName() + " with arg(s) " + argArrToString(oa));
- m.invoke(o, oa);
- } else {
- throw new IllegalArgumentException("Error inflating XML: Unexpected number of argments passed to \""
- + m.getName() + "\". Expected: " + paramTypes.length + " Got: " + paramStrs.length);
- }
- } else {
- // Obvious this is not a setter
- throw new IllegalArgumentException("Error inflating XML: no setter method found for param \"" +
- fieldId + "\".");
- }
- }
- }
-
- protected static String argArrToString(Object[] args) {
- String out = "";
- for(Object obj : args) {
- out += (obj == null ? (out += "[null] ") :
- ("[" + obj.getClass() + ": " + obj + "] "));
- }
- return out;
- }
-}
-
diff --git a/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java
new file mode 100644
index 00000000..4fa11d8a
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/util/FastNumber.java
@@ -0,0 +1,119 @@
+package com.androidplot.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * An extension of {@link Number} optimized for speed at the cost of memory.
+ */
+public class FastNumber extends Number {
+
+ @NonNull private final Number number;
+ private boolean hasDoublePrimitive;
+ private boolean hasFloatPrimitive;
+ private boolean hasIntPrimitive;
+
+ private double doublePrimitive;
+ private float floatPrimitive;
+ private int intPrimitive;
+
+ /**
+ * Safe-instantiator of FastNumber; returns a null result if the input Number is also null.
+ * @param number
+ * @return
+ */
+ public static FastNumber orNull(@NonNull Number number) {
+ if(number == null) {
+ return null;
+ } else {
+ return new FastNumber(number);
+ }
+ }
+
+ private FastNumber(@NonNull Number number) {
+
+ //noinspection ConstantConditions //in case someone ignores the @NonNull annotation
+ if (number == null) {
+ throw new IllegalArgumentException("number parameter cannot be null");
+ }
+
+ // avoid nested instances of FastNumber :
+ if(number instanceof FastNumber) {
+ FastNumber rhs = (FastNumber) number;
+ this.number = rhs.number;
+ this.hasDoublePrimitive = rhs.hasDoublePrimitive;
+ this.hasFloatPrimitive = rhs.hasFloatPrimitive;
+ this.hasIntPrimitive = rhs.hasIntPrimitive;
+ this.doublePrimitive = rhs.doublePrimitive;
+ this.floatPrimitive = rhs.floatPrimitive;
+ this.intPrimitive = rhs.intPrimitive;
+ } else {
+ this.number = number;
+ }
+ }
+
+ @Override
+ public int intValue() {
+ if(!hasIntPrimitive) {
+ intPrimitive = number.intValue();
+ hasIntPrimitive = true;
+ }
+ return intPrimitive;
+ }
+
+ @Override
+ public long longValue() {
+ // TODO: optimize me!
+ return number.longValue();
+ }
+
+ @Override
+ public float floatValue() {
+ if(!hasFloatPrimitive) {
+ floatPrimitive = number.floatValue();
+ hasFloatPrimitive = true;
+ }
+ return floatPrimitive;
+ }
+
+ @Override
+ public double doubleValue() {
+ if(!hasDoublePrimitive) {
+ doublePrimitive = number.doubleValue();
+ hasDoublePrimitive = true;
+ }
+ return doublePrimitive;
+ }
+
+ /**
+ * To be equal, two instances must both be instances of {@link FastNumber}. The inner {@link
+ * #number} field must also be a common type. Numbers which are mathematically equal are not
+ * necessarily equal. This keeps with the java implementation of common Number classes where for
+ * instance {@code new Integer(0).equals(new Double(0))} returns {@code false}
+ */
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ FastNumber that = (FastNumber) o;
+
+ return number.equals(that.number);
+
+ }
+
+ @Override
+ public int hashCode() {
+ return number.hashCode();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.valueOf(doubleValue());
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java b/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java
index b790dc81..edae4d30 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/FontUtils.java
@@ -16,8 +16,7 @@
package com.androidplot.util;
-import android.graphics.Paint;
-import android.graphics.Rect;
+import android.graphics.*;
public class FontUtils {
@@ -64,4 +63,18 @@ public static Rect getStringDimensions(String text, Paint paint) {
return size;
}
+ /**
+ * Draws text vertically centered on the specified coordinates
+ * @param canvas
+ * @param paint
+ * @param text
+ * @param cx
+ * @param cy
+ */
+ public static void drawTextVerticallyCentered(Canvas canvas, String text, float cx, float cy, Paint paint) {
+ Rect textBounds = new Rect();
+ paint.getTextBounds(text, 0, text.length(), textBounds);
+ canvas.drawText(text, cx, cy - textBounds.exactCenterY(), paint);
+ }
+
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/ZHash.java b/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java
similarity index 82%
rename from androidplot-core/src/main/java/com/androidplot/util/ZHash.java
rename to androidplot-core/src/main/java/com/androidplot/util/LayerHash.java
index 81ad5e53..f11737a6 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/ZHash.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/LayerHash.java
@@ -1,156 +1,180 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.androidplot.util;
-
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * An implementation of {@link ZIndexable}. Provides fast element retrieval via hash key in addition to
- * mutable ordering (z indexing) of elements.
- */
-public class ZHash implements ZIndexable {
-
- private HashMap hash;
- private ZLinkedList zlist;
-
- {
- hash = new HashMap<>();
- zlist = new ZLinkedList<>();
- }
-
- public int size() {
- return zlist.size();
- }
-
-
- public ValueType get(KeyType key) {
- return hash.get(key);
- }
-
- public List getKeysAsList() {
- return zlist;
- }
-
- /**
- * If key already exists within the structure, it's value is replaced with the new value and
- * it's existing order is maintained.
- * @param key
- * @param value
- */
- public synchronized void addToTop(KeyType key, ValueType value) {
- if(hash.containsKey(key)) {
- hash.put(key, value);
- } else {
- hash.put(key, value);
- zlist.addToTop(key);
- }
- }
-
- /**
- * If key already exists within the structure, it's value is replaced with the new value and
- * it's existing order is maintained.
- * @param key
- * @param value
- */
- public synchronized void addToBottom(KeyType key, ValueType value) {
- if(hash.containsKey(key)) {
- hash.put(key, value);
- } else {
- hash.put(key, value);
- zlist.addToBottom(key);
- }
- }
-
- public synchronized boolean moveToTop(KeyType element) {
- if(!hash.containsKey(element)) {
- return false;
- } else {
- return zlist.moveToTop(element);
- }
- }
-
- public synchronized boolean moveAbove(KeyType objectToMove, KeyType reference) {
- if(objectToMove == reference) {
- throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B.");
- }
- if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) {
- return false;
- } else {
- return zlist.moveAbove(objectToMove, reference);
- }
- }
-
- public synchronized boolean moveBeneath(KeyType objectToMove, KeyType reference) {
- if(objectToMove == reference) {
- throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B.");
- }
- if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) {
- return false;
- } else {
- return zlist.moveBeneath(objectToMove, reference);
- }
- }
-
- public synchronized boolean moveToBottom(KeyType key) {
- if(!hash.containsKey(key)) {
- return false;
- } else {
- return zlist.moveToBottom(key);
- }
- }
-
- public synchronized boolean moveUp(KeyType key) {
- if (!hash.containsKey(key)) {
- return false;
- } else {
- return zlist.moveUp(key);
- }
- }
-
- public synchronized boolean moveDown(KeyType key) {
- if (!hash.containsKey(key)) {
- return false;
- } else {
- return zlist.moveDown(key);
- }
- }
-
- @Override
- public List elements() {
- return zlist;
- }
-
- /**
- *
- * @return Ordered list of keys.
- */
- public List keys() {
- return elements();
- }
-
-
- public synchronized boolean remove(KeyType key) {
- if(hash.containsKey(key)) {
- hash.remove(key);
- zlist.remove(key);
- return true;
- } else {
- return false;
- }
- }
-}
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.util;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * An implementation of {@link Layerable}. Provides fast element retrieval via hash key in addition to
+ * mutable ordering (z indexing) of elements.
+ */
+public class LayerHash implements Layerable {
+
+ private HashMap hash;
+ private LinkedLayerList zlist;
+
+ {
+ hash = new HashMap<>();
+ zlist = new LinkedLayerList<>();
+ }
+
+ public int size() {
+ return zlist.size();
+ }
+
+
+ public ValueType get(KeyType key) {
+ return hash.get(key);
+ }
+
+ public List getKeysAsList() {
+ return zlist;
+ }
+
+ /**
+ * If key already exists within the structure, it's value is replaced with the new value and
+ * it's existing order is maintained.
+ * @param key
+ * @param value
+ */
+ public synchronized void addToTop(KeyType key, ValueType value) {
+ if(hash.containsKey(key)) {
+ hash.put(key, value);
+ } else {
+ hash.put(key, value);
+ zlist.addToTop(key);
+ }
+ }
+
+ /**
+ * If key already exists within the structure, it's value is replaced with the new value and
+ * it's existing order is maintained.
+ * @param key
+ * @param value
+ */
+ public synchronized void addToBottom(KeyType key, ValueType value) {
+ if(hash.containsKey(key)) {
+ hash.put(key, value);
+ } else {
+ hash.put(key, value);
+ zlist.addToBottom(key);
+ }
+ }
+
+ public synchronized boolean moveToTop(KeyType element) {
+ if(!hash.containsKey(element)) {
+ return false;
+ } else {
+ return zlist.moveToTop(element);
+ }
+ }
+
+ public synchronized boolean moveAbove(KeyType objectToMove, KeyType reference) {
+ if(objectToMove == reference) {
+ throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B.");
+ }
+ if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) {
+ return false;
+ } else {
+ return zlist.moveAbove(objectToMove, reference);
+ }
+ }
+
+ public synchronized boolean moveBeneath(KeyType objectToMove, KeyType reference) {
+ if(objectToMove == reference) {
+ throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B.");
+ }
+ if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) {
+ return false;
+ } else {
+ return zlist.moveBeneath(objectToMove, reference);
+ }
+ }
+
+ public synchronized boolean moveToBottom(KeyType key) {
+ if(!hash.containsKey(key)) {
+ return false;
+ } else {
+ return zlist.moveToBottom(key);
+ }
+ }
+
+ public synchronized boolean moveUp(KeyType key) {
+ if (!hash.containsKey(key)) {
+ return false;
+ } else {
+ return zlist.moveUp(key);
+ }
+ }
+
+ public synchronized boolean moveDown(KeyType key) {
+ if (!hash.containsKey(key)) {
+ return false;
+ } else {
+ return zlist.moveDown(key);
+ }
+ }
+
+ @Override
+ public List elements() {
+ return zlist;
+ }
+
+ /**
+ *
+ * @return Ordered list of keys.
+ */
+ public List keys() {
+ return elements();
+ }
+
+
+ public synchronized boolean remove(KeyType key) {
+ if(hash.containsKey(key)) {
+ hash.remove(key);
+ zlist.remove(key);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public ValueType getTop() {
+ return hash.get(zlist.getLast());
+ }
+
+ public ValueType getBottom() {
+ return hash.get(zlist.getFirst());
+ }
+
+ public ValueType getAbove(KeyType key) {
+ final int index = zlist.indexOf(key);
+ if(index >= 0 && index < size() - 1) {
+ return hash.get(zlist.get(index + 1));
+ }
+ return null;
+ }
+
+ public ValueType getBeneath(KeyType key) {
+ final int index = zlist.indexOf(key);
+ if(index > 0) {
+ return hash.get(zlist.get(index - 1));
+ }
+ return null;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/ListOrganizer.java b/androidplot-core/src/main/java/com/androidplot/util/LayerListOrganizer.java
similarity index 92%
rename from androidplot-core/src/main/java/com/androidplot/util/ListOrganizer.java
rename to androidplot-core/src/main/java/com/androidplot/util/LayerListOrganizer.java
index 67bb2a71..1080a223 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/ListOrganizer.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/LayerListOrganizer.java
@@ -1,118 +1,118 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.androidplot.util;
-
-import java.util.List;
-
-/**
- * Utility class providing additional element organization operations.
- * @param
- */
-public class ListOrganizer implements ZIndexable {
-
- private static final int ZERO = 0;
- private static final int ONE = 1;
-
- private List list;
-
- public ListOrganizer(List list) {
- this.list = list;
- }
-
-
- public boolean moveToTop(ElementType element) {
- if(list.remove(element)) {
- list.add(list.size(), element);
- return true;
- } else {
- return false;
- }
- }
-
- public boolean moveAbove(ElementType objectToMove, ElementType reference) {
- if(objectToMove == reference) {
- throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B.");
- }
-
-
- list.remove(objectToMove);
- int refIndex = list.indexOf(reference);
- list.add(refIndex + ONE, objectToMove);
- return true;
- }
-
- public boolean moveBeneath(ElementType objectToMove, ElementType reference) {
- if (objectToMove == reference) {
- throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B.");
- }
-
- list.remove(objectToMove);
- int refIndex = list.indexOf(reference);
- list.add(refIndex, objectToMove);
- return true;
-
- }
-
- public boolean moveToBottom(ElementType key) {
- list.remove(key);
- list.add(ZERO, key);
- return true;
- }
-
- public boolean moveUp(ElementType key) {
- int widgetIndex = list.indexOf(key);
- if(widgetIndex == - ONE) {
- // key not found:
- return false;
- }
- if(widgetIndex >= list.size() - ONE) {
- // already at the top:
- return true;
- }
-
- ElementType widgetAbove = list.get(widgetIndex + ONE);
- return moveAbove(key, widgetAbove);
- }
-
- public boolean moveDown(ElementType key) {
- int widgetIndex = list.indexOf(key);
- if(widgetIndex == - ONE) {
- // key not found:
- return false;
- }
- if(widgetIndex <= ZERO) {
- // already at the bottom:
- return true;
- }
-
- ElementType widgetBeneath = list.get(widgetIndex - ONE);
- return moveBeneath(key, widgetBeneath);
- }
-
- @Override
- public List elements() {
- return list;
- }
-
- public void addToBottom(ElementType element) {
- list.add(ZERO, element);
- }
-
- public void addToTop(ElementType element) {
- list.add(list.size(), element);
- }
-}
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.util;
+
+import java.util.List;
+
+/**
+ * Utility class providing additional element organization operations.
+ * @param
+ */
+public class LayerListOrganizer implements Layerable {
+
+ private static final int ZERO = 0;
+ private static final int ONE = 1;
+
+ private List list;
+
+ public LayerListOrganizer(List list) {
+ this.list = list;
+ }
+
+
+ public boolean moveToTop(ElementType element) {
+ if(list.remove(element)) {
+ list.add(list.size(), element);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean moveAbove(ElementType objectToMove, ElementType reference) {
+ if(objectToMove == reference) {
+ throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B.");
+ }
+
+
+ list.remove(objectToMove);
+ int refIndex = list.indexOf(reference);
+ list.add(refIndex + ONE, objectToMove);
+ return true;
+ }
+
+ public boolean moveBeneath(ElementType objectToMove, ElementType reference) {
+ if (objectToMove == reference) {
+ throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B.");
+ }
+
+ list.remove(objectToMove);
+ int refIndex = list.indexOf(reference);
+ list.add(refIndex, objectToMove);
+ return true;
+
+ }
+
+ public boolean moveToBottom(ElementType key) {
+ list.remove(key);
+ list.add(ZERO, key);
+ return true;
+ }
+
+ public boolean moveUp(ElementType key) {
+ int widgetIndex = list.indexOf(key);
+ if(widgetIndex == - ONE) {
+ // key not found:
+ return false;
+ }
+ if(widgetIndex >= list.size() - ONE) {
+ // already at the top:
+ return true;
+ }
+
+ ElementType widgetAbove = list.get(widgetIndex + ONE);
+ return moveAbove(key, widgetAbove);
+ }
+
+ public boolean moveDown(ElementType key) {
+ int widgetIndex = list.indexOf(key);
+ if(widgetIndex == - ONE) {
+ // key not found:
+ return false;
+ }
+ if(widgetIndex <= ZERO) {
+ // already at the bottom:
+ return true;
+ }
+
+ ElementType widgetBeneath = list.get(widgetIndex - ONE);
+ return moveBeneath(key, widgetBeneath);
+ }
+
+ @Override
+ public List elements() {
+ return list;
+ }
+
+ public void addToBottom(ElementType element) {
+ list.add(ZERO, element);
+ }
+
+ public void addToTop(ElementType element) {
+ list.add(list.size(), element);
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/ZIndexable.java b/androidplot-core/src/main/java/com/androidplot/util/Layerable.java
similarity index 88%
rename from androidplot-core/src/main/java/com/androidplot/util/ZIndexable.java
rename to androidplot-core/src/main/java/com/androidplot/util/Layerable.java
index 19c0d6b6..f1af65e6 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/ZIndexable.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/Layerable.java
@@ -1,80 +1,80 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.androidplot.util;
-
-import java.util.List;
-
-/**
- * Encapsulates the concept of z-indexable objects; Each object is stored above or below each other object and may
- * be moved up and down in the queue relative to other elements in the hash or absolutely to the front or back of the queue.
- *
- * Note that the method names correspond to the order of items drawn directly on top of one another using an iterator;
- * the first element drawn (lowest z-index) is effectively the "bottom" element.
- * @param
- */
-public interface ZIndexable {
-
- /**
- * Move above all other elements
- * @param element
- * @return
- */
- boolean moveToTop(ElementType element);
-
-
- /**
- * Move above the specified element
- * @param objectToMove
- * @param reference
- * @return
- */
- boolean moveAbove(ElementType objectToMove, ElementType reference);
-
-
- /**
- * Move beneath the specified element
- *
- * @param objectToMove
- * @param reference
- * @return
- */
- boolean moveBeneath(ElementType objectToMove, ElementType reference);
-
- /**
- * Move beneath all other elements
- * @param key
- * @return
- */
- boolean moveToBottom(ElementType key);
-
-
- /**
- * Move up by one element
- * @param key
- * @return
- */
- boolean moveUp(ElementType key);
-
- /**
- * Move down by one element
- * @param key
- * @return
- */
- boolean moveDown(ElementType key);
-
- List elements();
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.util;
+
+import java.util.List;
+
+/**
+ * Encapsulates the concept of "layerable" objects; Each object is stored above or below each other object and may
+ * be moved up and down in the queue relative to other elements in the hash or absolutely to the front or back of the queue.
+ *
+ * Note that the method names correspond to the order of items drawn directly on top of one another using an iterator;
+ * the first element drawn (lowest layer) is effectively the "bottom" element.
+ * @param
+ */
+public interface Layerable {
+
+ /**
+ * Move above all other elements
+ * @param element
+ * @return
+ */
+ boolean moveToTop(ElementType element);
+
+
+ /**
+ * Move above the specified element
+ * @param objectToMove
+ * @param reference
+ * @return
+ */
+ boolean moveAbove(ElementType objectToMove, ElementType reference);
+
+
+ /**
+ * Move beneath the specified element
+ *
+ * @param objectToMove
+ * @param reference
+ * @return
+ */
+ boolean moveBeneath(ElementType objectToMove, ElementType reference);
+
+ /**
+ * Move beneath all other elements
+ * @param key
+ * @return
+ */
+ boolean moveToBottom(ElementType key);
+
+
+ /**
+ * Move up by one element
+ * @param key
+ * @return
+ */
+ boolean moveUp(ElementType key);
+
+ /**
+ * Move down by one element
+ * @param key
+ * @return
+ */
+ boolean moveDown(ElementType key);
+
+ List elements();
}
\ No newline at end of file
diff --git a/androidplot-core/src/main/java/com/androidplot/util/ZLinkedList.java b/androidplot-core/src/main/java/com/androidplot/util/LinkedLayerList.java
similarity index 86%
rename from androidplot-core/src/main/java/com/androidplot/util/ZLinkedList.java
rename to androidplot-core/src/main/java/com/androidplot/util/LinkedLayerList.java
index 2cc3d774..2f02603c 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/ZLinkedList.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/LinkedLayerList.java
@@ -1,71 +1,75 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.androidplot.util;
-
-import java.util.LinkedList;
-import java.util.List;
-
-public class ZLinkedList extends LinkedList implements ZIndexable {
-
- private ListOrganizer organizer = new ListOrganizer<>(this);
-
- @Override
- public boolean moveToTop(Type element) {
- return organizer.moveToTop(element);
- }
-
- @Override
- public boolean moveAbove(Type objectToMove, Type reference) {
- return organizer.moveAbove(objectToMove, reference);
- }
-
- @Override
- public boolean moveBeneath(Type objectToMove, Type reference) {
- return organizer.moveBeneath(objectToMove, reference);
- }
-
- @Override
- public boolean moveToBottom(Type key) {
- return organizer.moveToBottom(key);
- }
-
- @Override
- public boolean moveUp(Type key) {
- return organizer.moveUp(key);
- }
-
- @Override
- public boolean moveDown(Type key) {
- return organizer.moveDown(key);
- }
-
- @Override
- public List elements() {
- return organizer.elements();
- }
-
- public void addToBottom(Type element) {
- organizer.addToBottom(element);
- }
-
- public void addToTop(Type element) {
- organizer.addToTop(element);
- }
-
-
-
-}
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.util;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A implementation of {@link Layerable} backed by a {@link LinkedList}.
+ * @param
+ */
+public class LinkedLayerList extends LinkedList implements Layerable {
+
+ private LayerListOrganizer organizer = new LayerListOrganizer<>(this);
+
+ @Override
+ public boolean moveToTop(Type element) {
+ return organizer.moveToTop(element);
+ }
+
+ @Override
+ public boolean moveAbove(Type objectToMove, Type reference) {
+ return organizer.moveAbove(objectToMove, reference);
+ }
+
+ @Override
+ public boolean moveBeneath(Type objectToMove, Type reference) {
+ return organizer.moveBeneath(objectToMove, reference);
+ }
+
+ @Override
+ public boolean moveToBottom(Type key) {
+ return organizer.moveToBottom(key);
+ }
+
+ @Override
+ public boolean moveUp(Type key) {
+ return organizer.moveUp(key);
+ }
+
+ @Override
+ public boolean moveDown(Type key) {
+ return organizer.moveDown(key);
+ }
+
+ @Override
+ public List elements() {
+ return organizer.elements();
+ }
+
+ public void addToBottom(Type element) {
+ organizer.addToBottom(element);
+ }
+
+ public void addToTop(Type element) {
+ organizer.addToTop(element);
+ }
+
+
+
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java b/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java
index 1686583b..8e3260b7 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/PixelUtils.java
@@ -30,7 +30,6 @@
public class PixelUtils {
private static DisplayMetrics metrics;
- private static final float FLOAT_INT_AVG_NUDGE = 0.5f;
/**
* Recalculates scale value etc. Should be called when an application starts or
@@ -48,26 +47,6 @@ public static PointF sub(PointF lhs, PointF rhs) {
return new PointF(lhs.x - rhs.x, lhs.y - rhs.y);
}
- /**
- * Converts a sub-pixel accurate RectF to
- * a single pixel accurate rect. This is helpful
- * for clipping operations which dont do a good job with
- * subpixel vals.
- * @param in
- * @return
- */
- public static RectF sink(RectF in) {
- return nearestPixRect(in.left, in.top, in.right, in.bottom);
- }
-
- public static RectF nearestPixRect(float left, float top, float right, float bottom) {
- return new RectF(
- (int) (left + FLOAT_INT_AVG_NUDGE),
- (int) (top + FLOAT_INT_AVG_NUDGE),
- (int) (right + FLOAT_INT_AVG_NUDGE),
- (int) (bottom + FLOAT_INT_AVG_NUDGE));
- }
-
/**
* Converts a dp value to pixels.
* @param dp
@@ -90,29 +69,6 @@ public static float spToPix(float sp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
}
-
- /**
- *
- * @param fraction A float value between 0 and 1.
- * @return Number of pixels fraction represents on the current device's display.
- */
- public static float fractionToPixH(float fraction) {
- checkMetrics();
- return metrics.heightPixels * fraction;
-
- }
-
- /**
- *
- * @param fraction A float value between 0 and 1.
- * @return Number of pixels fraction represents on the current device's display.
- */
- public static float fractionToPixW(float fraction) {
- checkMetrics();
- return metrics.widthPixels * fraction;
- }
-
-
/**
*
* CODE BELOW IS ADAPTED IN PART FROM MINDRIOT'S SAMPLE CODE HERE:
@@ -178,7 +134,7 @@ public InternalDimension(float value, int unit) {
}
/**
- * Safety check to hopefully help clarify what could otherwise be a confusing NPE.
+ * Safety run to hopefully help clarify what could otherwise be a confusing NPE.
*/
private static void checkMetrics() {
if(metrics == null) {
diff --git a/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java b/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java
index 1d597b9e..b56337ed 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/PlotStatistics.java
@@ -39,6 +39,7 @@ public class PlotStatistics implements PlotListener {
long latencySamples = 0;
long latencySum = 0;
String annotationString = "";
+ private boolean annotatePlotEnabled;
private Paint paint;
{
@@ -49,11 +50,6 @@ public class PlotStatistics implements PlotListener {
resetCounters();
}
-
- private boolean annotatePlotEnabled;
-
-
-
public PlotStatistics(long updateDelayMs, boolean annotatePlotEnabled) {
this.updateDelayMs = updateDelayMs;
this.annotatePlotEnabled = annotatePlotEnabled;
@@ -109,4 +105,8 @@ public void onAfterDraw(Plot source, Canvas canvas) {
latencySamples++;
annotatePlot(source, canvas);
}
+
+ public void setEnabled(boolean isEnabled) {
+ this.annotatePlotEnabled = isEnabled;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java b/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java
index 016f2f32..3915e2ad 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/RectFUtils.java
@@ -18,6 +18,8 @@
import android.graphics.RectF;
+import com.androidplot.ui.*;
+
/**
* Convenience methods for dealing with {@link android.graphics.RectF}
*/
@@ -38,4 +40,43 @@ public static boolean areIdentical(RectF r1, RectF r2) {
r1.right == r2.right &&
r1.bottom == r2.bottom;
}
+
+ /**
+ * Calculates a new {@link RectF} by applying insets to rect.
+ * @param rect
+ * @param insets
+ * @return The {@link RectF} created as a result of applying insets, or the passed in
+ * instance, if the insets were null.
+ */
+ public static RectF applyInsets(RectF rect, Insets insets) {
+ if (insets != null) {
+ return new RectF(
+ rect.left + insets.getLeft(),
+ rect.top + insets.getTop(),
+ rect.right - insets.getRight(),
+ rect.bottom - insets.getBottom());
+ } else {
+ return rect;
+ }
+ }
+
+ /**
+ * Generates a RectF from two height and two width values; the h and w values will
+ * be passed into the RectF constructor such that RectF.left <= RectF.right and
+ * RectF.top <= RectF.bottom.
+ * @param w1 width1
+ * @param h1 height1
+ * @param w2 width2
+ * @param h2 height2
+ * @return
+ */
+ public static RectF createFromEdges(float w1, float h1, float w2, float h2) {
+ final boolean w1IsLeft = w1 <= w2;
+ final boolean h1IsTop = h1 <= h2;
+ return new RectF(
+ w1IsLeft ? w1 : w2,
+ h1IsTop ? h1 : h2,
+ w1IsLeft ? w2 : w1,
+ h1IsTop ? h2 : h1);
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java
index 38db8107..4bc118e0 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/Redrawer.java
@@ -17,10 +17,12 @@
package com.androidplot.util;
import android.util.Log;
+
import com.androidplot.Plot;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
/**
@@ -33,11 +35,17 @@ public class Redrawer implements Runnable {
private static final String TAG = Redrawer.class.getName();
- private List plots;
+ private List> plots;
private long sleepTime;
+
+ // used to temporarily pause rendering without disposing of the run thread
private boolean keepRunning;
+
+ // when set to false, run thread will be allowed to exit the main run loop
private boolean keepAlive;
+ private Thread thread;
+
/**
*
* @param plots List of Plot instances to be redrawn
@@ -45,16 +53,20 @@ public class Redrawer implements Runnable {
* @param startImmediately If true, invokes run() immediately after construction.
*/
public Redrawer(List plots, float maxRefreshRate, boolean startImmediately) {
- this.plots = plots;
+ this.plots = new ArrayList<>(plots.size());
+ for(Plot plot : plots) {
+ this.plots.add(new WeakReference<>(plot));
+ }
setMaxRefreshRate(maxRefreshRate);
- new Thread(this).start();
+ thread = new Thread(this, "Androidplot Redrawer");
+ thread.start();
if(startImmediately) {
- run();
+ start();
}
}
public Redrawer(Plot plot, float maxRefreshRate, boolean startImmediately) {
- this(Arrays.asList(new Plot[]{plot}), maxRefreshRate, startImmediately);
+ this(Collections.singletonList(plot), maxRefreshRate, startImmediately);
}
/**
@@ -97,8 +109,8 @@ public void run() {
// TODO: record start and end timestamps and
// TODO: calculate sleepTime from that, in order to more accurately
// TODO: meet desired refresh rate.
- for(Plot plot : plots) {
- plot.redraw();
+ for(WeakReference plotRef : plots) {
+ plotRef.get().redraw();
}
synchronized (this) {
wait(sleepTime);
@@ -110,7 +122,7 @@ public void run() {
}
}
}
- } catch(InterruptedException e) {
+ } catch (InterruptedException ignored) {
} finally {
Log.d(TAG, "Redrawer thread exited.");
diff --git a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java
index 9e51b6f0..b9526a24 100644
--- a/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java
+++ b/androidplot-core/src/main/java/com/androidplot/util/SeriesUtils.java
@@ -16,26 +16,256 @@
package com.androidplot.util;
+import com.androidplot.Region;
+import com.androidplot.xy.FastXYSeries;
+import com.androidplot.xy.OrderedXYSeries;
+import com.androidplot.xy.RectRegion;
+import com.androidplot.xy.XYConstraints;
import com.androidplot.xy.XYSeries;
+import java.util.List;
+
/**
- * Created by nick_f on 7/24/14.
+ * Utilities for dealing with Series data.
*/
public class SeriesUtils {
+ public static RectRegion minMax(List seriesList) {
+ return minMax(null, seriesList);
+ }
+
+ public static RectRegion minMax(XYSeries... seriesList) {
+ return minMax(null, seriesList);
+ }
+
+ public static Region minMaxX(XYSeries... seriesList) {
+ final Region bounds = new Region();
+ for (XYSeries series : seriesList) {
+ for (int i = 0; i < series.size(); i++) {
+ bounds.union(series.getX(i));
+ }
+ }
+ return bounds;
+ }
+
+ public static Region minMaxY(XYSeries... seriesList) {
+ final Region bounds = new Region();
+ for (XYSeries series : seriesList) {
+ for (int i = 0; i < series.size(); i++) {
+ bounds.union(series.getY(i));
+ }
+ }
+ return bounds;
+ }
+
+ /**
+ * @param constraints may be null.
+ * @param seriesList
+ * @return
+ * @since 0.9.7
+ */
+ public static RectRegion minMax(XYConstraints constraints, List seriesList) {
+ return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()]));
+ }
+
+ /**
+ * @param constraints May be null.
+ * @param seriesArray
+ * @return
+ * @since 0.9.7
+ */
+ public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArray) {
+
+ final RectRegion bounds = new RectRegion();
+
+ // make sure there is series data to iterate over:
+ if (seriesArray != null && seriesArray.length > 0) {
+
+ // iterate over each series
+ for (XYSeries series : seriesArray) {
+
+ // if this is an advanced xy series then minMax have already been calculated for us:
+ if (series instanceof FastXYSeries) {
+ final RectRegion b = ((FastXYSeries) series).minMax();
+ if(b == null) {
+ //this series doesn't currently have min or max region (might be empty)
+ continue;
+ }
+ if(constraints == null || constraints.contains(b)) {
+ bounds.union(b);
+ continue;
+ }
+ }
+ for (int i = 0; i < series.size(); i++) {
+ final Number xi = series.getX(i);
+ final Number yi = series.getY(i);
+
+ // if constraints have been set, make sure this xy coordinate exists within them:
+ if (constraints == null || constraints.contains(xi, yi)) {
+ bounds.union(xi, yi);
+ }
+ }
+ }
+ }
+ return bounds;
+ }
+
/**
*
+ * @param bounds Starting minMax values to work from; only lists values that are greater than or less
+ * than those in bounds will be be used.
+ * @param lists lists to be evaluated for min/max values.
+ * @return the original bounds instance passed in
+ */
+ public static Region minMax(Region bounds, List... lists) {
+ for (final List list : lists) {
+ for (final Number i : list) {
+ bounds.union(i);
+ }
+ }
+ return bounds;
+ }
+
+ /**
+ * Compute the range of visible i-vals in the specified series. Assumes that x-vals are
+ * in strict ascending order; behavior is undefined otherwise.
* @param series
- * @return The largest yVal in the series or null if the series contains no non-null yVals.
+ * @param visibleBounds The visible constraints of the plot
+ * @return
*/
- public static Number getMaxY(XYSeries series) {
- Number max = null;
- for(int i = 0; i < series.size(); i++) {
- Number thisNumber = series.getY(i);
- if(max == null || thisNumber != null && thisNumber.doubleValue() > max.doubleValue()) {
- max = thisNumber;
+ public static Region iBounds(XYSeries series, RectRegion visibleBounds) {
+ final float step = series.size() >= 200 ? 50 : 1;
+ final int iBoundsMin = iBoundsMin(series, visibleBounds.getMinX().doubleValue(), step);
+ final int iBoundsMax = iBoundsMax(series, visibleBounds.getMaxX().doubleValue(), step);
+ return new Region(iBoundsMin, iBoundsMax);
+ }
+
+ /**
+ * TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing
+ * TODO a binary search algorithm that also supports nulls is not trivial and would not likely
+ * TODO result in any noticeable performance increase here. It's a task for another day!
+ * @param series
+ * @param visibleMax
+ * @param step
+ * @return The index of the smallest non-null value that is greater than visibleMax, or the index
+ * of the last element if no such value exists.
+ */
+ protected static int iBoundsMax(XYSeries series, double visibleMax, float step) {
+ int max = series.size() - 1;
+ final int seriesSize = series.size();
+ final int steps = (int) Math.ceil(seriesSize / step);
+ for (int stepIndex = steps; stepIndex >= 0; stepIndex--) {
+ final int i = stepIndex * (int) step;
+ for (int ii = 0; ii < step; ii++) {
+ final int iii = i + ii;
+ if(iii < seriesSize) {
+ final Number thisX = series.getX(iii);
+ if (thisX != null) {
+ final double thisDouble = thisX.doubleValue();
+ if (thisDouble > visibleMax) {
+ // this is the smallest non-null value in this block, so skip
+ // to the next block:
+ max = iii;
+ break;
+ } else if (thisDouble == visibleMax) {
+ return iii;
+ } else {
+ return max;
+ }
+ }
+ }
}
}
return max;
}
+
+ /**
+ * TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing
+ * TODO a binary search algorithm that also supports nulls is not trivial and would not likely
+ * TODO result in any noticeable performance increase here. It's a task for another day!
+ * @param series
+ * @param visibleMin
+ * @param step
+ * @return The index of the largest non-null value that is less than visible, or 0
+ * (the first element index) if no such value exists.
+ */
+ protected static int iBoundsMin(XYSeries series, double visibleMin, float step) {
+ int min = 0;
+ final int steps = (int) Math.ceil(series.size() / step);
+ for (int stepIndex = 1; stepIndex <= steps; stepIndex++) {
+ final int i = stepIndex * (int) step;
+ for (int ii = 1; ii <= step; ii++) {
+ final int iii = i - ii;
+ if(iii < 0) {
+ break;
+ }
+ if(iii < series.size()) {
+ final Number thisX = series.getX(iii);
+ if (thisX != null) {
+ if (thisX.doubleValue() < visibleMin) {
+ // this is the largest non-null value in this block, so skip
+ // to the next block:
+ min = iii;
+ break;
+ } else if (thisX.doubleValue() == visibleMin) {
+ return iii;
+ } else {
+ return min;
+ }
+ }
+ }
+ }
+ }
+ return min;
+ }
+
+ /**
+ * Determine the minMax iVals of the xVals surrounding a range of one or more null values.
+ * @param series
+ * @param index index of the null value in question
+ * @return The iVals of the non-null values surrounding the null range. If the null range is unbounded on
+ * either side then either or both min and max values will also be null.
+ */
+ protected static Region getNullRegion(XYSeries series, int index) {
+ Region region = new Region();
+ if(series.getX(index) != null) {
+ throw new IllegalArgumentException("Attempt to find null region for non null index: " + index);
+ }
+ for(int i = index - 1; i >= 0; i--) {
+ Number val = series.getX(i);
+ if(val != null) {
+ region.setMin(i);
+ break;
+ }
+ }
+
+ for(int i = index + 1; i < series.size(); i++) {
+ Number val = series.getX(i);
+ if(val != null) {
+ region.setMax(i);
+ break;
+ }
+ }
+ return region;
+ }
+
+ /**
+ * @param lists
+ * @return
+ * @since 0.9.7
+ */
+ public static Region minMax(List... lists) {
+ return minMax(new Region(), lists);
+ }
+
+ /**
+ * Determine the XVal order of an XYSeries. If series does not implement {@link OrderedXYSeries}
+ * then {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE} is assumed.
+ * @param series
+ * @return The {@link com.androidplot.xy.OrderedXYSeries.XOrder} of the series.
+ */
+ public static OrderedXYSeries.XOrder getXYOrder(XYSeries series) {
+ return series instanceof OrderedXYSeries ?
+ ((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java b/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java
deleted file mode 100644
index 22ac88df..00000000
--- a/androidplot-core/src/main/java/com/androidplot/util/ValPixConverter.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.androidplot.util;
-
-import android.graphics.PointF;
-import android.graphics.RectF;
-
-/**
- * Utility methods for converting pixel coordinates into real values and vice versa.
- */
-public class ValPixConverter {
- private static final int ZERO = 0;
-
-
- public static float valToPix(double val, double min, double max, float lengthPix, boolean flip) {
- if(lengthPix <= ZERO) {
- throw new IllegalArgumentException("Length in pixels must be greater than 0.");
- }
- double range = range(min, max);
- double scale = lengthPix / range;
- double raw = val - min;
- float pix = (float)(raw * scale);
-
- if(flip) {
- pix = (lengthPix - pix);
- }
- return pix;
- }
-
- public static double range(double min, double max) {
- return (max-min);
- }
-
-
- public static double valPerPix(double min, double max, float lengthPix) {
- double valRange = range(min, max);
- return valRange/lengthPix;
- }
-
- /**
- * Convert a value in pixels to the type passed into min/max
- * @param pix
- * @param min
- * @param max
- * @param lengthPix
- * @param flip True if the axis should be reversed before calculated. This is the case
- * with the y axis for screen coords.
- * @return
- */
- public static double pixToVal(float pix, double min, double max, float lengthPix, boolean flip) {
- if(pix < ZERO) {
- throw new IllegalArgumentException("pixel values cannot be negative.");
- }
-
- if(lengthPix <= ZERO) {
- throw new IllegalArgumentException("Length in pixels must be greater than 0.");
- }
- float pMult = pix;
- if(flip) {
- pMult = lengthPix - pix;
- }
- double range = range(min, max);
- return ((range / lengthPix) * pMult) + min;
- }
-
- /**
- * Converts a real value into a pixel value.
- * @param x Real d (domain) component of the point to convert.
- * @param y Real y (range) component of the point to convert.
- * @param plotArea
- * @param minX Minimum visible real value on the d (domain) axis.
- * @param maxX Maximum visible real value on the y (domain) axis.
- * @param minY Minimum visible real value on the y (range) axis.
- * @param maxY Maximum visible real value on the y (range axis.
- * @return
- */
- public static PointF valToPix(Number x, Number y, RectF plotArea, Number minX, Number maxX, Number minY, Number maxY) {
- float pixX = ValPixConverter.valToPix(x.doubleValue(), minX.doubleValue(), maxX.doubleValue(), plotArea.width(), false) + (plotArea.left);
- float pixY = ValPixConverter.valToPix(y.doubleValue(), minY.doubleValue(), maxY.doubleValue(), plotArea.height(), true) + plotArea.top;
- return new PointF(pixX, pixY);
- }
-}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java
new file mode 100644
index 00000000..11b9e8e8
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/AdvancedLineAndPointRenderer.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.xy;
+
+import android.content.*;
+import android.graphics.*;
+import com.androidplot.ui.RenderStack;
+import com.androidplot.ui.SeriesRenderer;
+
+/**
+ * This is an experimental (but stable) implementation of an {@link XYSeriesRenderer} that provides instrumentation
+ * allowing advanced behaviors like dynamically coloring / styling individual segments of a series, etc. This class
+ * may be removed or renamed in future releases.
+ * Currently has the following constraints:
+ * - Interpolation is not supported
+ * - Only draws lines; no points or fill
+ * - Draws series lines using simple Canvas.drawLine(...) invocations.
+ * @since 0.9.9
+ */
+public class AdvancedLineAndPointRenderer extends XYSeriesRenderer {
+
+ private int latestIndex;
+
+ public AdvancedLineAndPointRenderer(XYPlot plot) {
+ super(plot);
+ }
+
+ @Override
+ protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) {
+ PointF thisPoint;
+ PointF lastPoint = null;
+ for (int i = 0; i < series.size(); i++) {
+ Number y = series.getY(i);
+ Number x = series.getX(i);
+
+ if (y != null && x != null) {
+ thisPoint = getPlot().getBounds()
+ .transformScreen(x, y, plotArea);
+ } else {
+ thisPoint = null;
+ }
+
+ // don't need to do any of this if the line isnt going to be drawn:
+ if(formatter.getLinePaint() != null) {
+ if (thisPoint != null && lastPoint != null) {
+ canvas.drawLine(lastPoint.x, lastPoint.y, thisPoint.x, thisPoint.y, formatter.getLinePaint(i, latestIndex, series.size()));
+ }
+ }
+ lastPoint = thisPoint;
+ }
+ }
+
+ @Override
+ protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
+ if(formatter.getLinePaint() != null) {
+ canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
+ }
+ }
+
+ public void setLatestIndex(int latestIndex) {
+ this.latestIndex = latestIndex;
+ }
+
+
+ /**
+ * Formatter designed to work in tandem with {@link AdvancedLineAndPointRenderer}.
+ * @since 0.9.9
+ */
+ public static class Formatter extends XYSeriesFormatter {
+
+ private static final int DEFAULT_STROKE_WIDTH = 3;
+
+ private Paint linePaint;
+
+ public Formatter() {
+ linePaint = new Paint();
+ linePaint.setStrokeWidth(DEFAULT_STROKE_WIDTH);
+ linePaint.setColor(Color.RED);
+ }
+
+ public Formatter(Context context, int xmlConfigId) {
+ this();
+ configure(context, xmlConfigId);
+ }
+
+ @Override
+ public Class extends SeriesRenderer> getRendererClass() {
+ return AdvancedLineAndPointRenderer.class;
+ }
+
+ @Override
+ public AdvancedLineAndPointRenderer doGetRendererInstance(XYPlot plot) {
+ return new AdvancedLineAndPointRenderer(plot);
+ }
+
+ public Paint getLinePaint() {
+ return linePaint;
+ }
+
+ /**
+ * By default, simply returns the line paint as-is. May be overridden to provide custom behavior based
+ * on input params.
+ * @param thisIndex
+ * @param latestIndex
+ * @param seriesSize
+ * @return
+ */
+ public Paint getLinePaint(int thisIndex, int latestIndex, int seriesSize) {
+ return getLinePaint();
+ }
+
+ public void setLinePaint(Paint linePaint) {
+ this.linePaint = linePaint;
+ }
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/XYAxisType.java b/androidplot-core/src/main/java/com/androidplot/xy/Axis.java
similarity index 93%
rename from androidplot-core/src/main/java/com/androidplot/xy/XYAxisType.java
rename to androidplot-core/src/main/java/com/androidplot/xy/Axis.java
index f01c669d..18f2cdf9 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/XYAxisType.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/Axis.java
@@ -1,22 +1,22 @@
-/*
- * Copyright 2015 AndroidPlot.com
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.androidplot.xy;
-
-public enum XYAxisType {
- DOMAIN,
- RANGE
-}
+/*
+ * Copyright 2015 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.xy;
+
+public enum Axis {
+ DOMAIN,
+ RANGE
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java
index fa072c04..3272748d 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BarFormatter.java
@@ -15,6 +15,7 @@
*/
package com.androidplot.xy;
+import android.content.*;
import android.graphics.Paint;
import com.androidplot.ui.SeriesRenderer;
@@ -39,9 +40,16 @@ public void setBorderPaint(Paint borderPaint) {
private Paint fillPaint;
private Paint borderPaint;
- {
+ private float marginTop;
+ private float marginBottom;
+ private float marginLeft;
+ private float marginRight;
+
+ /**
+ * Should only be used in conjunction with calls to configure()...
+ */
+ public BarFormatter() {
fillPaint = new Paint();
- //fillPaint.setColor(Color.RED);
fillPaint.setStyle(Paint.Style.FILL);
fillPaint.setAlpha(100);
borderPaint = new Paint();
@@ -49,24 +57,56 @@ public void setBorderPaint(Paint borderPaint) {
borderPaint.setAlpha(100);
}
- /**
- * Should only be used in conjunction with calls to configure()...
- */
- public BarFormatter() {
- }
-
public BarFormatter(int fillColor, int borderColor) {
+ this();
fillPaint.setColor(fillColor);
borderPaint.setColor(borderColor);
}
+ public BarFormatter(Context context, int xmlCfgId) {
+ this();
+ configure(context, xmlCfgId);
+ }
+
@Override
public Class extends SeriesRenderer> getRendererClass() {
return BarRenderer.class;
}
@Override
- public SeriesRenderer getRendererInstance(XYPlot plot) {
+ public SeriesRenderer doGetRendererInstance(XYPlot plot) {
return new BarRenderer(plot);
}
+
+ public float getMarginTop() {
+ return marginTop;
+ }
+
+ public void setMarginTop(float marginTop) {
+ this.marginTop = marginTop;
+ }
+
+ public float getMarginBottom() {
+ return marginBottom;
+ }
+
+ public void setMarginBottom(float marginBottom) {
+ this.marginBottom = marginBottom;
+ }
+
+ public float getMarginLeft() {
+ return marginLeft;
+ }
+
+ public void setMarginLeft(float marginLeft) {
+ this.marginLeft = marginLeft;
+ }
+
+ public float getMarginRight() {
+ return marginRight;
+ }
+
+ public void setMarginRight(float marginRight) {
+ this.marginRight = marginRight;
+ }
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java
index 5279a1a6..ced83885 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BarRenderer.java
@@ -20,91 +20,102 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-import java.util.Map.Entry;
-import java.util.TreeMap;
import android.graphics.Canvas;
import android.graphics.RectF;
-import com.androidplot.exception.PlotRenderException;
import com.androidplot.ui.RenderStack;
-import com.androidplot.ui.SeriesAndFormatter;
-import com.androidplot.util.ValPixConverter;
+import com.androidplot.ui.SeriesBundle;
+import com.androidplot.util.PixelUtils;
+import com.androidplot.util.RectFUtils;
/**
- * Renders the points in an XYSeries as bars.
+ * Renders the points in an XYSeries as bars. See {@link BarOrientation} javadoc for details on supported
+ * presentation styles.
+ *
*/
-public class BarRenderer extends XYSeriesRenderer {
+public class BarRenderer extends GroupRenderer {
+
+ private BarOrientation barOrientation = BarOrientation.OVERLAID; // default Render Style
+ private BarGroupWidthMode barGroupWidthMode = BarGroupWidthMode.FIXED_WIDTH; // default Width Style
+
+ /**
+ * Represents the size in pixels of either bar width or bar gap width, depending on the current
+ * value of barWidthMode.
+ */
+ private float width = PixelUtils.dpToPix(3);
- private BarRenderStyle renderStyle = BarRenderStyle.OVERLAID; // default Render Style
- private BarWidthStyle widthStyle = BarWidthStyle.FIXED_WIDTH; // default Width Style
- private float barWidth = 5;
- private float barGap = 1;
- private Comparator barComparator = new BarComparator();
+ /**
+ * How bars should be laid out when in a group of 2 or more series.
+ */
+ public enum BarOrientation {
+
+ /**
+ * Bars are drawn overlapping one another, in the order their respective series
+ * was added to the plot.
+ */
+ IN_ORDER,
- public enum BarRenderStyle {
+ /**
+ * Bars are drawn overlapping one another, with taller bars being drawn behind
+ * the shorter ones.
+ */
OVERLAID, // bars are overlaid in descending y-val order (largest val in back)
+
+ /**
+ * Bars are stacked on top of one another so that the sum of their yVals produces the final
+ * height of that bar.
+ */
STACKED, // bars are drawn stacked vertically on top of each other
+
+ /**
+ * Bars are drawn next to one another, grouped by iVal
+ */
SIDE_BY_SIDE // bars are drawn horizontally next to each-other
}
- public enum BarWidthStyle {
- FIXED_WIDTH, // bar width is always barWidth
- VARIABLE_WIDTH // bar width is calculated so that there is only barGap between each bar
+ /**
+ * Mode with which to calculate the width of each bar.
+ */
+ public enum BarGroupWidthMode {
+ FIXED_WIDTH, // bar width is always barWidth
+ FIXED_GAP // bar width is calculated relative to a fixed gap width between each bar
}
public BarRenderer(XYPlot plot) {
super(plot);
}
- /**
- * Sets the width of the bars when using the FIXED_WIDTH render style
- * @param barWidth
- */
- public void setBarWidth(float barWidth) {
- this.barWidth = barWidth;
+ public void setBarOrientation(BarOrientation renderBarOrientation) {
+ this.barOrientation = renderBarOrientation;
}
- /**
- * Sets the size of the gap between the bar (or bar groups) when using the VARIABLE_WIDTH render style
- * @param barGap
- */
- public void setBarGap(float barGap) {
- this.barGap = barGap;
+ public BarOrientation getBarOrientation() {
+ return this.barOrientation;
}
- public void setBarRenderStyle(BarRenderStyle renderStyle) {
- this.renderStyle = renderStyle;
+ public BarGroupWidthMode getBarGroupWidthMode() {
+ return this.barGroupWidthMode;
}
-
- public void setBarWidthStyle(BarWidthStyle widthStyle) {
- this.widthStyle = widthStyle;
+
+ public float getBarGroupWidth() {
+ return this.width;
}
-
- public void setBarWidthStyle(BarWidthStyle style, float value) {
- setBarWidthStyle(style);
- switch (style) {
- case FIXED_WIDTH:
- setBarWidth(value);
- break;
- case VARIABLE_WIDTH:
- setBarGap(value);
- break;
- default:
- break;
- }
+
+ public void setBarGroupWidth(BarGroupWidthMode mode, float width) {
+ this.barGroupWidthMode = mode;
+ this.width = width;
}
-
- /**
- * Sets a {@link Comparator} used for sorting bars.
- */
- public void setBarComparator(Comparator barComparator) {
- this.barComparator = barComparator;
+
+ protected BarComparator getBarComparator(float rangeOriginPx) {
+ return new BarComparator(getBarOrientation(), rangeOriginPx);
}
@Override
public void doDrawLegendIcon(Canvas canvas, RectF rect, BarFormatter formatter) {
- canvas.drawRect(rect, formatter.getFillPaint());
+ if (formatter.hasFillPaint()) {
+ canvas.drawRect(rect, formatter.getFillPaint());
+ }
canvas.drawRect(rect, formatter.getBorderPaint());
}
@@ -121,281 +132,254 @@ public FormatterType getFormatter(int index, XYSeries series) {
}
@Override
- public void onRender(Canvas canvas, RectF plotArea, XYSeries series,
- FormatterType barFormatter, RenderStack stack) throws PlotRenderException {
-
- // get all the series associated with this renderer:
- List> sfPairList = getSeriesList();
-
- // this renderer uses special element-by-element z-indexing that results in
- // all series associated with the renderer being rendered in a single pass, so
- // we need to exclude the rest of the series on the render stack from being redrawn later:
- stack.disable(getClass());
-
- TreeMap axisMap = new TreeMap();
-
- // dont try to render anything if there's nothing to render.
- if(sfPairList == null) return;
-
- /*
+ public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) {
+
+ List barGroups = new ArrayList<>();
+
+ /*
* Build the axisMap (yVal,BarGroup)... a TreeMap of BarGroups
* BarGroups represent a point on the X axis where a single or group of bars need to be drawn.
*/
- for(SeriesAndFormatter thisPair : sfPairList) {
- BarGroup barGroup;
-
- // For each value in the series
- for(int i = 0; i < thisPair.getSeries().size(); i++) {
-
- if (thisPair.getSeries().getX(i) != null) {
-
- // get a new bar object
- Bar bar = new Bar(thisPair.getSeries(), thisPair.getFormatter(),i,plotArea);
-
- // Find or create the barGroup
- if (axisMap.containsKey(bar.intX)) {
- barGroup = axisMap.get(bar.intX);
- } else {
- barGroup = new BarGroup(bar.intX,plotArea);
- axisMap.put(bar.intX, barGroup);
- }
- barGroup.addBar(bar);
- }
+ for(int i = 0; i < seriesSize; i++) {
+ final BarGroup group = new BarGroup(i, 0, plotArea);
+ int seriesOrder = 0;
+ for(SeriesBundle bundle : sfList) {
+ // TODO: is this null check really necessary?
+ if(bundle.getSeries().getX(i) != null) {
+ Bar bar = new Bar(getPlot(), bundle.getSeries(),
+ bundle.getFormatter(), seriesOrder, i, plotArea);
+ group.addBar(bar);
+ group.centerPix = bar.xPix;
+ }
+ seriesOrder++;
}
+ barGroups.add(group);
}
- // Loop through the axisMap linking up prev pointers
- BarGroup prev, current;
- prev = null;
- for(Entry mapEntry : axisMap.entrySet()) {
- current = mapEntry.getValue();
- current.prev = prev;
- prev = current;
- }
-
-
- // The default gap between each bar section
- int gap = (int) barGap;
-
- // Determine roughly how wide (rough_width) this bar should be. This is then used as a default width
- // when there are gaps in the data or for the first/last bars.
- float f_rough_width = ((plotArea.width() - ((axisMap.size() - 1) * gap)) / (axisMap.size() - 1));
- int rough_width = (int) f_rough_width;
- if (rough_width < 0) rough_width = 0;
- if (gap > rough_width) {
- gap = rough_width / 2;
- }
-
- /*
- * Calculate the dimensions of each barGroup and then draw each bar within it according to
- * the Render Style and Width Style.
+ /*
+ * Calculate the dimensions of each barGroup and then draw each bar within it according to
+ * the Render Style and Width Style.
*/
- for(Number key : axisMap.keySet()) {
-
- BarGroup barGroup = axisMap.get(key);
-
- // Determine the exact left and right X for the Bar Group
- switch (widthStyle) {
- case FIXED_WIDTH:
- // use intX and go halfwidth either side.
- barGroup.leftX = barGroup.intX - (int) (barWidth / 2);
- barGroup.width = (int) barWidth;
- barGroup.rightX = barGroup.leftX + barGroup.width;
- break;
- case VARIABLE_WIDTH:
- if (barGroup.prev != null) {
- if (barGroup.intX - barGroup.prev.intX - gap - 1 > (int)(rough_width * 1.5)) {
- // use intX and go halfwidth either side.
- barGroup.leftX = barGroup.intX - (rough_width / 2);
- barGroup.width = rough_width;
- barGroup.rightX = barGroup.leftX + barGroup.width;
- } else {
- // base left off prev right to get the gap correct.
- barGroup.leftX = barGroup.prev.rightX + gap + 1;
- if (barGroup.leftX > barGroup.intX) barGroup.leftX = barGroup.intX;
- // base right off intX + halfwidth.
- barGroup.rightX = barGroup.intX + (rough_width / 2);
- // calculate the width
- barGroup.width = barGroup.rightX - barGroup.leftX;
- }
- } else {
- // use intX and go halfwidth either side.
- barGroup.leftX = barGroup.intX - (rough_width / 2);
- barGroup.width = rough_width;
- barGroup.rightX = barGroup.leftX + barGroup.width;
- }
- break;
- default:
- break;
- }
-
+ final int groupCount = barGroups.size();
+ for(BarGroup barGroup : barGroups) {
+
+ // Determine the exact left and right X for the Bar Group
+ switch (barGroupWidthMode) {
+ case FIXED_WIDTH:
+ barGroup.leftPix = barGroup.centerPix - (width / 2);
+ barGroup.rightPix = barGroup.leftPix + width;
+ break;
+ case FIXED_GAP:
+ float barWidth = plotArea.width();
+ if(groupCount > 1) {
+ barWidth = (barGroups.get(1).centerPix - barGroups.get(0).centerPix) - width;
+ }
+
+ final float halfWidth = barWidth / 2;
+ barGroup.leftPix = barGroup.centerPix - halfWidth;
+ barGroup.rightPix = barGroup.centerPix + halfWidth;
+ break;
+ default:
+ break;
+ }
+
/*
* Draw the bars within the barGroup area.
*/
double rangeOrigin = getPlot().getRangeOrigin().doubleValue();
- float basePositionY = ValPixConverter.valToPix(rangeOrigin,
- getPlot().getCalculatedMinY().doubleValue(),
- getPlot().getCalculatedMaxY().doubleValue(),
- plotArea.height(), true) + plotArea.top;
-
- switch (renderStyle) {
- case OVERLAID:
- Collections.sort(barGroup.bars, barComparator);
- for (Bar bar : barGroup.bars) {
- BarFormatter formatter = bar.getFormatter();
- PointLabelFormatter plf = formatter.getPointLabelFormatter();
- PointLabeler pointLabeler = null;
- if (formatter != null) {
- pointLabeler = formatter.getPointLabeler();
- }
-
- if (bar.yVal= 2) {
- canvas.drawRect(bar.barGroup.leftX, basePositionY, bar.barGroup.rightX, bar.intY, formatter.getFillPaint());
- }
- canvas.drawRect(bar.barGroup.leftX, basePositionY, bar.barGroup.rightX, bar.intY, formatter.getBorderPaint());
- } else { // rising bar
- if (bar.barGroup.width >= 2) {
- canvas.drawRect(bar.barGroup.leftX, bar.intY, bar.barGroup.rightX, basePositionY, formatter.getFillPaint());
- }
- canvas.drawRect(bar.barGroup.leftX, bar.intY, bar.barGroup.rightX, basePositionY, formatter.getBorderPaint());
- }
- if(plf != null && pointLabeler != null) {
- canvas.drawText(pointLabeler.getLabel(bar.series, bar.seriesIndex), bar.intX + plf.hOffset, bar.intY + plf.vOffset, plf.getTextPaint());
- }
- }
- break;
- case SIDE_BY_SIDE:
- int width = barGroup.width / barGroup.bars.size();
- int leftX = barGroup.leftX;
- Collections.sort(barGroup.bars, barComparator);
- for (Bar bar : barGroup.bars) {
- BarFormatter formatter = bar.getFormatter();
- PointLabelFormatter plf = formatter.getPointLabelFormatter();
- PointLabeler pointLabeler = null;
- if (formatter != null) {
- pointLabeler = formatter.getPointLabeler();
- }
-
- if (bar.yVal= 2) {
- canvas.drawRect(leftX, basePositionY, leftX + width, bar.intY, formatter.getFillPaint());
- }
- canvas.drawRect(leftX, basePositionY, leftX + width, bar.intY, formatter.getBorderPaint());
- } else { // rising bar
- if (bar.barGroup.width >= 2) {
- canvas.drawRect(leftX, bar.intY, leftX + width, basePositionY, formatter.getFillPaint());
- }
- canvas.drawRect(leftX, bar.intY, leftX + width, basePositionY, formatter.getBorderPaint());
- }
- if(plf != null && pointLabeler != null) {
- canvas.drawText(pointLabeler.getLabel(bar.series, bar.seriesIndex), leftX + width/2 + plf.hOffset, bar.intY + plf.vOffset, plf.getTextPaint());
- }
- leftX = leftX + width;
- }
- break;
- case STACKED:
- int bottom = (int) barGroup.plotArea.bottom;
- Collections.sort(barGroup.bars, barComparator);
- for (Bar b : barGroup.bars) {
- BarFormatter formatter = b.getFormatter();
- PointLabelFormatter plf = formatter.getPointLabelFormatter();
- PointLabeler pointLabeler = null;
- if (formatter != null) {
- pointLabeler = formatter.getPointLabeler();
- }
- int height = (int) b.barGroup.plotArea.bottom - b.intY;
- int top = bottom - height;
- if (b.barGroup.width >= 2) {
- canvas.drawRect(b.barGroup.leftX, top, b.barGroup.rightX, bottom, formatter.getFillPaint());
- }
-
- canvas.drawRect(b.barGroup.leftX, top, b.barGroup.rightX, bottom, formatter.getBorderPaint());
- if(plf != null && pointLabeler != null) {
- //canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), b.intX + plf.hOffset, b.intY + plf.vOffset, plf.getTextPaint());
- // b.intY should be replaced by top as Text label should be drawn on top of each bar
- canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), b.intX + plf.hOffset, top + plf.vOffset, plf.getTextPaint());
- }
- bottom = top;
- }
- break;
- default:
- break;
- }
- }
+ float rangeOriginPx = (float) getPlot().getBounds().yRegion
+ .transform(rangeOrigin, plotArea.top, plotArea.bottom, true);
+
+ final BarComparator comparator = getBarComparator(rangeOriginPx);
+ switch (barOrientation) {
+ case IN_ORDER:
+ case OVERLAID:
+ Collections.sort(barGroup.bars, comparator);
+ for (Bar bar : barGroup.bars) {
+ drawBar(canvas, bar, createBarRect(
+ bar.barGroup.leftPix,
+ bar.yPix,
+ bar.barGroup.rightPix,
+ rangeOriginPx, bar.formatter));
+ }
+ break;
+ case SIDE_BY_SIDE:
+ final float width = barGroup.getWidth() / barGroup.bars.size();
+ float leftX = barGroup.leftPix;
+ Collections.sort(barGroup.bars, comparator);
+ for (Bar bar : barGroup.bars) {
+ drawBar(canvas, bar, createBarRect(
+ leftX, bar.yPix,
+ leftX + width, rangeOriginPx,
+ bar.formatter));
+ leftX = leftX + width;
+ }
+ break;
+ case STACKED:
+ float bottom = (int) barGroup.plotArea.bottom;
+ Collections.sort(barGroup.bars, comparator);
+ for (Bar bar : barGroup.bars) {
+ // TODO: handling sub range-origin values for the purpose of labeling
+ final float height = (int) bar.barGroup.plotArea.bottom - bar.yPix;
+ final float top = bottom - height;
+ drawBar(canvas, bar, createBarRect(
+ bar.barGroup.leftPix, top,
+ bar.barGroup.rightPix, bottom,
+ bar.formatter));
+ bottom = top;
+ }
+ break;
+ default:
+ throw new UnsupportedOperationException("Unexpected BarOrientation: " + barOrientation);
+ }
+ }
+ }
+
+ protected RectF createBarRect(float w1, float h1, float w2, float h2, BarFormatter formatter) {
+ final RectF result = RectFUtils.createFromEdges(w1, h1,w2, h2);
+ result.left += formatter.getMarginLeft();
+ result.right -= formatter.getMarginRight();
+ result.top += formatter.getMarginTop();
+ result.bottom -= formatter.getMarginBottom();
+ return result;
+ }
+
+ protected void drawBar(Canvas canvas, Bar bar, RectF rect) {
+
+ // null yVals are skipped:
+ if(bar.getY() == null) {
+ return;
+ }
+
+ BarFormatter formatter = getFormatter(bar.i, bar.series);
+ if(formatter == null) {
+ formatter = bar.formatter;
+ }
+ if(rect.height() > 0 && rect.width() > 0) {
+ if (formatter.hasFillPaint()) {
+ canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom,
+ formatter.getFillPaint());
+ }
+
+ if (formatter.hasLinePaint()) {
+ canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom,
+ formatter.getBorderPaint());
+ }
+ }
+
+ PointLabelFormatter plf =
+ formatter.hasPointLabelFormatter() ? formatter.getPointLabelFormatter() : null;
+
+ PointLabeler pointLabeler =
+ formatter != null ? formatter.getPointLabeler() : null;
+
+ if (plf != null && plf.hasTextPaint() && pointLabeler != null) {
+ canvas.drawText(pointLabeler.getLabel(bar.series, bar.i),
+ rect.centerX() + plf.hOffset, bar.yPix + plf.vOffset,
+ plf.getTextPaint());
+ }
}
-
- public class Bar {
+
+ /**
+ *
+ * @param
+ */
+ public static class Bar {
+
public final XYSeries series;
- private final FormatterType formatter;
- public final int seriesIndex;
- public final double yVal, xVal;
- public final int intX, intY;
- public final float pixX, pixY;
- protected BarGroup barGroup;
-
- public Bar(XYSeries series, FormatterType formatter, int seriesIndex, RectF plotArea) {
- this.series = series;
+ public final FormatterType formatter;
+ public final int i;
+ public final int seriesOrder;
+ public final float xPix;
+ public final float yPix;
+ protected BarGroup barGroup;
+
+ // TODO: factor out plot param
+ public Bar(XYPlot plot, XYSeries series, FormatterType formatter, int seriesOrder, int i, RectF plotArea) {
+ this.series = series;
this.formatter = formatter;
- this.seriesIndex = seriesIndex;
-
- this.xVal = series.getX(seriesIndex).doubleValue();
- this.pixX = ValPixConverter.valToPix(xVal, getPlot().getCalculatedMinX().doubleValue(), getPlot().getCalculatedMaxX().doubleValue(), plotArea.width(), false) + (plotArea.left);
- this.intX = (int) pixX;
-
- if (series.getY(seriesIndex) != null) {
- this.yVal = series.getY(seriesIndex).doubleValue();
- this.pixY = ValPixConverter.valToPix(yVal, getPlot().getCalculatedMinY().doubleValue(), getPlot().getCalculatedMaxY().doubleValue(), plotArea.height(), true) + plotArea.top;
- this.intY = (int) pixY;
- } else {
- this.yVal = 0;
- this.pixY = plotArea.bottom;
- this.intY = (int) pixY;
- }
- }
-
- public FormatterType getFormatter() {
- FormatterType f = BarRenderer.this.getFormatter(seriesIndex, series);
- return f != null ? f : formatter;
+ this.i = i;
+ this.seriesOrder = seriesOrder;
+
+ final double xVal = series.getX(i).doubleValue();
+ xPix = (float) plot.getBounds().getxRegion()
+ .transform(xVal, plotArea.left, plotArea.right, false);
+
+ if (series.getY(i) != null) {
+ final double yVal = series.getY(i).doubleValue();
+ this.yPix = (float) plot.getBounds().yRegion
+ .transform(yVal, plotArea.top, plotArea.bottom, true);
+ } else {
+ this.yPix = 0;
+ }
+ }
+
+ public Number getY() {
+ return series.getY(i);
}
}
-
- private class BarGroup {
- public ArrayList bars;
- public int intX;
- public int width, leftX, rightX;
- public RectF plotArea;
- public BarGroup prev;
-
- public BarGroup(int intX, RectF plotArea) {
- // Setup the TreeMap with the required comparator
- this.bars = new ArrayList(); // create a comparator that compares series title given the index.
- this.intX = intX;
- this.plotArea = plotArea;
- }
-
- public void addBar(Bar bar) {
- bar.barGroup = this;
- this.bars.add(bar);
- }
+
+ /**
+ * A collection of one or more {@Bar} instances sharing a common iVal.
+ */
+ private static class BarGroup {
+
+ public ArrayList bars;
+ public int i;
+ public float centerPix;
+ public float leftPix;
+ public float rightPix;
+ public RectF plotArea;
+
+ public BarGroup(int i, float centerPix, RectF plotArea) {
+ // Setup the TreeMap with the required comparator
+ this.bars = new ArrayList<>(); // create a comparator that compares series title given the index.
+ this.centerPix = centerPix;
+ this.plotArea = plotArea;
+ this.i = i;
+ }
+
+ public void addBar(Bar bar) {
+ bar.barGroup = this;
+ this.bars.add(bar);
+ }
+
+ protected float getWidth() {
+ return rightPix - leftPix;
+ }
}
+ /**
+ * Used to determine the order in which bars of the same group will be drawn.
+ */
@SuppressWarnings("WeakerAccess")
- public class BarComparator implements Comparator{
+ public static class BarComparator implements Comparator {
+
+ private final BarOrientation barOrientation;
+ private final float rangeOriginPx;
+
+ public BarComparator(BarOrientation barOrientation, float rangeOriginPx) {
+ this.rangeOriginPx = rangeOriginPx;
+ this.barOrientation = barOrientation;
+ }
@Override
public int compare(Bar bar1, Bar bar2) {
- switch (renderStyle) {
- case OVERLAID:
- return Integer.valueOf(bar1.intY).compareTo(bar2.intY);
- case SIDE_BY_SIDE:
- return bar1.series.getTitle().compareToIgnoreCase(bar2.series.getTitle());
- case STACKED:
- return bar1.series.getTitle().compareToIgnoreCase(bar2.series.getTitle());
- default:
- return 0;
- }
+ switch (barOrientation) {
+ case OVERLAID:
+ if(bar1.yPix > rangeOriginPx && bar2.yPix > rangeOriginPx) {
+ return Float.valueOf(bar2.yPix).compareTo(bar1.yPix);
+ } else {
+ return Float.valueOf(bar1.yPix).compareTo(bar2.yPix);
+ }
+ case IN_ORDER:
+ case SIDE_BY_SIDE:
+ case STACKED:
+ default:
+ return Integer.valueOf(bar1.seriesOrder).compareTo(bar2.seriesOrder);
+ }
}
- }
-
+ }
+
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BoundaryMode.java b/androidplot-core/src/main/java/com/androidplot/xy/BoundaryMode.java
index 47c4fe51..c0b791a6 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/BoundaryMode.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BoundaryMode.java
@@ -20,7 +20,7 @@ public enum BoundaryMode {
FIXED,
AUTO,
GROW,
- SHRINNK
+ SHRINK
}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java
new file mode 100644
index 00000000..3a794b5c
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleFormatter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.xy;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Paint;
+
+import com.androidplot.ui.SeriesRenderer;
+import com.androidplot.util.PixelUtils;
+
+/**
+ * Format for drawing a value using {@link BubbleRenderer}.
+ * @since 1.2.2
+ */
+public class BubbleFormatter extends XYSeriesFormatter {
+
+ private static final float DEFAULT_STROKE_PIX = 1;
+ private static final int DEFAULT_STROKE_COLOR = Color.BLACK;
+ private static final int DEFAULT_FILL_COLOR = Color.YELLOW;
+
+
+ private Paint strokePaint;
+ private Paint fillPaint;
+
+ {
+ strokePaint = new Paint();
+ strokePaint.setAntiAlias(true);
+ strokePaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_STROKE_PIX));
+ strokePaint.setStyle(Paint.Style.STROKE);
+ strokePaint.setColor(DEFAULT_STROKE_COLOR);
+
+ fillPaint = new Paint();
+ fillPaint.setAntiAlias(true);
+ fillPaint.setColor(DEFAULT_FILL_COLOR);
+
+ // default point labeler should draw z for bubbles:
+ setPointLabeler(new PointLabeler() {
+ @Override
+ public String getLabel(BubbleSeries series, int index) {
+ return String.valueOf(series.getZ(index));
+ }
+ });
+ }
+
+ public BubbleFormatter() {}
+
+ public BubbleFormatter(Context context, int xmlCfgId) {
+ this();
+ configure(context, xmlCfgId);
+ }
+
+ public BubbleFormatter(int fillColor, int strokeColor) {
+ fillPaint.setColor(fillColor);
+ strokePaint.setColor(strokeColor);
+ }
+
+ @Override
+ public Class extends SeriesRenderer> getRendererClass() {
+ return BubbleRenderer.class;
+ }
+
+ @Override
+ public BubbleRenderer doGetRendererInstance(XYPlot plot) {
+ return new BubbleRenderer(plot);
+ }
+
+ public Paint getStrokePaint() {
+ return strokePaint;
+ }
+
+ public void setStrokePaint(Paint strokePaint) {
+ this.strokePaint = strokePaint;
+ }
+
+ public Paint getFillPaint() {
+ return fillPaint;
+ }
+
+ public void setFillPaint(Paint fillPaint) {
+ this.fillPaint = fillPaint;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java
new file mode 100644
index 00000000..dcebf6cb
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleRenderer.java
@@ -0,0 +1,151 @@
+package com.androidplot.xy;
+
+import android.graphics.*;
+
+import com.androidplot.Region;
+import com.androidplot.ui.*;
+import com.androidplot.util.*;
+
+/**
+ * Renders three dimensional data onto an {@link XYPlot} as bubbles; the x/y values define the position
+ * of the bubble and z is uses as a scaling value for the bubble's radius.
+ * @since 1.2.2
+ */
+public class BubbleRenderer extends XYSeriesRenderer {
+
+ protected static final float MIN_BUBBLE_RADIUS_DEFAULT_DP = 9;
+ protected static final float MAX_BUBBLE_RADIUS_DEFAULT_DP = 25;
+
+ private Region bubbleBounds;
+
+ private BubbleScaleMode bubbleScaleMode = BubbleScaleMode.SQUARE_ROOT;
+
+ public enum BubbleScaleMode {
+
+ /**
+ * Bubble radius is scaled directly by {@link BubbleSeries} z-vals
+ */
+ LINEAR,
+
+ /**
+ * Bubble radius is scaled by the square root of {@link BubbleSeries} z-vals.
+ * This is the default scaling used.
+ */
+ SQUARE_ROOT
+ }
+
+ public BubbleRenderer(XYPlot plot) {
+ super(plot);
+
+ bubbleBounds = new Region(
+ PixelUtils.dpToPix(MIN_BUBBLE_RADIUS_DEFAULT_DP),
+ PixelUtils.dpToPix(MAX_BUBBLE_RADIUS_DEFAULT_DP));
+ }
+
+ @Override
+ protected void onRender(Canvas canvas, RectF plotArea, BubbleSeries series,
+ FormatterType formatter, RenderStack stack) {
+
+ Region magnitudeBounds = calculateBounds();
+ for(int i = 0; i < series.size(); i++) {
+
+ // only render non-null values greater than zero:
+ if(series.getY(i) != null && series.getZ(i).doubleValue() > 0) {
+
+ final PointF centerPoint = getPlot().getBounds().
+ transform(series.getX(i), series.getY(i), plotArea, false, true);
+
+ // calculate bubble radius:
+ float bubbleRadius = magnitudeBounds.
+ transform(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT ?
+ Math.sqrt(series.getZ(i).doubleValue()) :
+ series.getZ(i).doubleValue(), bubbleBounds).floatValue();
+ drawBubble(canvas, formatter, series, i, centerPoint, bubbleRadius);
+ }
+ }
+ }
+
+ /**
+ * Render a bubble onto the canvas
+ * @param canvas
+ * @param formatter
+ * @param series
+ * @param index
+ * @param centerPoint the x/y coords of the center of the bubble
+ * @param radius size of the bubble
+ */
+ protected void drawBubble(Canvas canvas, FormatterType formatter, BubbleSeries series,
+ int index, PointF centerPoint, float radius) {
+ canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getFillPaint());
+ canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getStrokePaint());
+ if(series != null && formatter.hasPointLabelFormatter() && formatter.getPointLabeler() != null) {
+ FontUtils.drawTextVerticallyCentered(
+ canvas,
+ formatter.getPointLabeler().getLabel(series, index),
+ centerPoint.x,
+ centerPoint.y,
+ formatter.getPointLabelFormatter().getTextPaint());
+ }
+ }
+
+ @Override
+ protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) {
+ drawBubble(canvas, formatter, null, 0,
+ new PointF(rect.centerX(), rect.centerY()), (rect.width()/2.5f));
+ }
+
+ public float getMinBubbleRadius() {
+ return bubbleBounds.getMin().floatValue();
+ }
+
+ public void setMinBubbleRadius(float minBubbleRadius) {
+ bubbleBounds.setMin(minBubbleRadius);
+ }
+
+ public float getMaxBubbleRadius() {
+ return bubbleBounds.getMax().floatValue();
+ }
+
+ public void setMaxBubbleRadius(float maxBubbleRadius) {
+ bubbleBounds.setMax(maxBubbleRadius);
+ }
+
+ public BubbleScaleMode getBubbleScaleMode() {
+ return bubbleScaleMode;
+ }
+
+ public void setBubbleScaleMode(BubbleScaleMode bubbleScaleMode) {
+ this.bubbleScaleMode = bubbleScaleMode;
+ }
+
+ protected Region calculateBounds() {
+ Region bounds = new Region();
+ for(SeriesBundle f : getSeriesAndFormatterList()) {
+ SeriesUtils.minMax(bounds, f.getSeries().getZVals());
+ }
+
+ if(bounds.getMax() != null && bounds.getMax().doubleValue() > 0) {
+ if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) {
+ // scale for easier visual interpretation. see:
+ // https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly
+ bounds.setMax(Math.sqrt(bounds.getMax().doubleValue()));
+ }
+ } else {
+ // no non-null, greater than zero vals so bounds are undefined
+ return null;
+ }
+
+ if(bounds.getMin().doubleValue() > 0) {
+
+ if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) {
+ // scale for easier visual interpretation. see:
+ // https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly
+ bounds.setMin(Math.sqrt(bounds.getMin().doubleValue()));
+ }
+ } else {
+ // if the smallest value is negative, use zero instead since those vals arent visible:
+ bounds.setMax(0);
+ }
+ return bounds;
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java
new file mode 100644
index 00000000..ac36dc46
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/BubbleSeries.java
@@ -0,0 +1,81 @@
+package com.androidplot.xy;
+
+import java.util.*;
+
+/**
+ * Created by halfhp on 9/17/16.
+ */
+public class BubbleSeries implements XYSeries {
+
+ private List xVals;
+ private List yVals;
+ private List zVals;
+ private String title;
+
+ /**
+ *
+ * @param interleavedValues Interleaved values ordered as x, y, z; total size must be a multiple of 3.
+ */
+ public BubbleSeries(Number... interleavedValues) {
+ if(interleavedValues == null || interleavedValues.length % 3 > 0) {
+ throw new RuntimeException("BubbleSeries interleave array length must be a non-zero multiple of 3.");
+ }
+
+ xVals = new ArrayList<>();
+ yVals = new ArrayList<>();
+ zVals = new ArrayList<>();
+ for(int i = 0; i < interleavedValues.length; i+=3) {
+ xVals.add(interleavedValues[i]);
+ yVals.add(interleavedValues[i+1]);
+ zVals.add(interleavedValues[i+2]);
+ }
+ }
+
+ public BubbleSeries(List yVals, List zVals, String title) {
+ this.yVals = yVals;
+ this.zVals = zVals;
+ this.title = title;
+ // populate x with iVals:
+ this.xVals = new ArrayList<>(zVals.size());
+ for(int i = 0; i < zVals.size(); i++) {
+ this.xVals.add(i);
+ }
+ }
+
+ public BubbleSeries(List xVals, List yVals, List zVals, String title) {
+ this.xVals = xVals;
+ this.yVals = yVals;
+ this.zVals = zVals;
+ this.title = title;
+ }
+
+ @Override
+ public String getTitle() {
+ return title;
+ }
+
+ @Override
+ public int size() {
+ return xVals.size();
+ }
+
+ @Override
+ public Number getX(int index) {
+ return xVals.get(index);
+ }
+
+ @Override
+ public Number getY(int index) {
+ return yVals.get(index);
+ }
+
+ public Number getZ(int index) {
+ return zVals.get(index);
+ }
+
+ public List getZVals() {
+ return zVals;
+ }
+
+
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java
new file mode 100644
index 00000000..84fce88f
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickFormatter.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2016 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.xy;
+
+import android.content.*;
+import android.graphics.Color;
+import android.graphics.Paint;
+import com.androidplot.ui.SeriesRenderer;
+import com.androidplot.util.PixelUtils;
+
+/**
+ * Format for drawing a value using {@link CandlestickRenderer}.
+ * @since 0.9.7
+ */
+public class CandlestickFormatter extends XYSeriesFormatter {
+
+ private static final float DEFAULT_WIDTH_PIX = PixelUtils.dpToPix(10);
+ private static final float DEFAULT_STROKE_PIX = PixelUtils.dpToPix(4);
+
+ private Paint wickPaint;
+ private Paint risingBodyFillPaint;
+ private Paint fallingBodyFillPaint;
+ private Paint risingBodyStrokePaint;
+ private Paint fallingBodyStrokePaint;
+ private Paint upperCapPaint;
+ private Paint lowerCapPaint;
+
+ private float bodyWidth = DEFAULT_WIDTH_PIX;
+ private float upperCapWidth = DEFAULT_WIDTH_PIX;
+ private float lowerCapWidth = DEFAULT_WIDTH_PIX;
+
+ private BodyStyle bodyStyle;
+
+ public enum BodyStyle {
+ SQUARE,
+ TRIANGULAR
+ }
+
+ protected static Paint getDefaultFillPaint(int color) {
+ Paint p = new Paint();
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(color);
+ return p;
+ }
+
+ protected static Paint getDefaultStrokePaint(int color) {
+ Paint p = new Paint();
+ p.setStyle(Paint.Style.STROKE);
+ p.setStrokeWidth(DEFAULT_STROKE_PIX);
+ p.setColor(color);
+ p.setAntiAlias(true);
+ return p;
+ }
+
+ public CandlestickFormatter(Context context, int xmlCfgId) {
+ this();
+ configure(context, xmlCfgId);
+ }
+
+ public CandlestickFormatter() {
+ this(getDefaultStrokePaint(Color.YELLOW),
+ getDefaultFillPaint(Color.GREEN),
+ getDefaultFillPaint(Color.RED),
+ getDefaultStrokePaint(Color.GREEN),
+ getDefaultStrokePaint(Color.RED),
+ getDefaultStrokePaint(Color.YELLOW),
+ getDefaultStrokePaint(Color.YELLOW),
+ BodyStyle.SQUARE);
+ }
+
+ public CandlestickFormatter(Paint wickPaint, Paint risingBodyFillPaint, Paint fallingBodyFillPaint,
+ Paint risingBodyStrokePaint, Paint fallingBodyStrokePaint,
+ Paint upperCapPaint, Paint lowerCapPaint, BodyStyle bodyStyle) {
+ setWickPaint(wickPaint);
+ setRisingBodyFillPaint(risingBodyFillPaint);
+ setFallingBodyFillPaint(fallingBodyFillPaint);
+ setRisingBodyStrokePaint(risingBodyStrokePaint);
+ setFallingBodyStrokePaint(fallingBodyStrokePaint);
+ setUpperCapPaint(upperCapPaint);
+ setLowerCapPaint(lowerCapPaint);
+ setBodyStyle(bodyStyle);
+ }
+
+ @Override
+ public Class extends SeriesRenderer> getRendererClass() {
+ return CandlestickRenderer.class;
+ }
+
+ @Override
+ public SeriesRenderer doGetRendererInstance(XYPlot plot) {
+ return new CandlestickRenderer(plot);
+ }
+
+ public Paint getWickPaint() {
+ return wickPaint;
+ }
+
+ public void setWickPaint(Paint wickPaint) {
+ this.wickPaint = wickPaint;
+ }
+
+ public Paint getRisingBodyFillPaint() {
+ return risingBodyFillPaint;
+ }
+
+ public void setRisingBodyFillPaint(Paint risingBodyFillPaint) {
+ this.risingBodyFillPaint = risingBodyFillPaint;
+ }
+
+ public Paint getRisingBodyStrokePaint() {
+ return risingBodyStrokePaint;
+ }
+
+ public void setRisingBodyStrokePaint(Paint risingBodyStrokePaint) {
+ this.risingBodyStrokePaint = risingBodyStrokePaint;
+ }
+
+ public Paint getUpperCapPaint() {
+ return upperCapPaint;
+ }
+
+ public void setUpperCapPaint(Paint upperCapPaint) {
+ this.upperCapPaint = upperCapPaint;
+ }
+
+ public Paint getLowerCapPaint() {
+ return lowerCapPaint;
+ }
+
+ public void setLowerCapPaint(Paint lowerCapPaint) {
+ this.lowerCapPaint = lowerCapPaint;
+ }
+
+ public float getBodyWidth() {
+ return bodyWidth;
+ }
+
+ public void setBodyWidth(float bodyWidth) {
+ this.bodyWidth = bodyWidth;
+ }
+
+ public float getLowerCapWidth() {
+ return lowerCapWidth;
+ }
+
+ public void setLowerCapWidth(float lowerCapWidth) {
+ this.lowerCapWidth = lowerCapWidth;
+ }
+
+ public float getUpperCapWidth() {
+ return upperCapWidth;
+ }
+
+ public void setUpperCapWidth(float upperCapWidth) {
+ this.upperCapWidth = upperCapWidth;
+ }
+
+ public Paint getFallingBodyFillPaint() {
+ return fallingBodyFillPaint;
+ }
+
+ public void setFallingBodyFillPaint(Paint fallingBodyFillPaint) {
+ this.fallingBodyFillPaint = fallingBodyFillPaint;
+ }
+
+ public Paint getFallingBodyStrokePaint() {
+ return fallingBodyStrokePaint;
+ }
+
+ public void setFallingBodyStrokePaint(Paint fallingBodyStrokePaint) {
+ this.fallingBodyStrokePaint = fallingBodyStrokePaint;
+ }
+
+ public BodyStyle getBodyStyle() {
+ return bodyStyle;
+ }
+
+ public void setBodyStyle(BodyStyle bodyStyle) {
+ this.bodyStyle = bodyStyle;
+ }
+
+ /**
+ * Convenience method to set caps and wick to a single color in one call.
+ * @param paint
+ */
+ public void setCapAndWickPaint(Paint paint) {
+ setUpperCapPaint(paint);
+ setLowerCapPaint(paint);
+ setWickPaint(paint);
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickMaker.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickMaker.java
new file mode 100644
index 00000000..8834cfbb
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickMaker.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.xy;
+
+/**
+ * Helper utility to simplify the creation of of candlestick charts
+ * @since 0.9.7
+ */
+public abstract class CandlestickMaker {
+
+ /**
+ * Adds a candlestick chart to the specified plot using the specified
+ * high, low, open and close values.
+ * @param plot
+ * @param formatter
+ * @param openVals
+ * @param closeVals
+ * @param highVals
+ * @param lowVals
+ */
+ public static void make(XYPlot plot, CandlestickFormatter formatter,
+ XYSeries openVals, XYSeries closeVals, XYSeries highVals, XYSeries lowVals) {
+ plot.addSeries(formatter, highVals, lowVals, openVals, closeVals);
+ }
+
+ /**
+ * Add a candlestick chart to the specified plot using the specified {@link CandlestickSeries}.
+ * @param plot
+ * @param formatter
+ * @param series
+ * @since 0.9.8
+ */
+ public static void make(XYPlot plot, CandlestickFormatter formatter, CandlestickSeries series) {
+ make(plot, formatter, series.getOpenSeries(), series.getCloseSeries(),
+ series.getHighSeries(), series.getLowSeries());
+ }
+
+ /**
+ * Check the validity of series data comprising a {@link CandlestickSeries}.
+ * This is a development aid; be sure to remove any usage of this method in production code.
+ * @param series
+ * @since 0.9.8
+ */
+ public static void check(CandlestickSeries series) {
+ check(series.getOpenSeries(), series.getCloseSeries(), series.getHighSeries(), series.getLowSeries());
+ }
+
+ /**
+ * Check the validity of series data comprising a candlestick chart.
+ * This is a development aid; be sure to remove any usage of this method in production code.
+ * @param openVals
+ * @param closeVals
+ * @param highVals
+ * @param lowVals
+ * @since 0.9.8
+ */
+ public static void check(XYSeries openVals, XYSeries closeVals, XYSeries highVals, XYSeries lowVals) {
+ final int size = openVals.size();
+ assert closeVals.size() == size : "closeVals has irregular size.";
+ assert highVals.size() == size : "highVals has irregular size.";
+ assert lowVals.size() == size : "lowVals has irregular size.";
+
+ for(int i = 0; i < size; i++) {
+
+ final double highVal = highVals.getY(i).doubleValue();
+ final double lowVal = lowVals.getY(i).doubleValue();
+ final double openVal = openVals.getY(i).doubleValue();
+ final double closeVal = closeVals.getY(i).doubleValue();
+
+ assert openVal <= highVal : "Detected openVal > highVal at index " + i;
+ assert openVal >= lowVal : "Detected openVal < lowVal at index " + i;
+ assert closeVal <= highVal : "Detected closeVal > highVal at index " + i;
+ assert closeVal >= lowVal : "Detected closeVal < lowVal at index " + i;
+ assert lowVal <= highVal : "Detected lowVal > highVal at index " + i;
+ }
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java
new file mode 100644
index 00000000..81c4363c
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickRenderer.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.xy;
+
+import android.graphics.*;
+import com.androidplot.ui.RenderStack;
+import com.androidplot.ui.SeriesBundle;
+
+import java.util.List;
+
+/**
+ * Renders a group of {@link com.androidplot.xy.XYSeries} as a candlestick chart
+ * into an {@link com.androidplot.xy.XYPlot}.
+ *
+ * Constraints:
+ * - Exactly four series must be added using the same {@link CandlestickFormatter}.
+ * - Each of the four series has the same x(i) value.
+ * - Expects that series are added in the order of:
+ * high, low, open, close
+ *
+ * {@link CandlestickSeries} and {@link CandlestickMaker} provide simplified classes and methods
+ * for setting up a candlestick chart.
+ * @since 0.9.7
+ */
+public class CandlestickRenderer extends GroupRenderer {
+
+ protected static final int HIGH_INDEX = 0;
+ protected static final int LOW_INDEX = 1;
+ protected static final int OPEN_INDEX = 2;
+ protected static final int CLOSE_INDEX = 3;
+
+ public CandlestickRenderer(XYPlot plot) {
+ super(plot);
+ }
+
+
+ @Override
+ public void onRender(Canvas canvas, RectF plotArea, List> sfList, int seriesSize, RenderStack stack) {
+
+ final FormatterType formatter = sfList.get(0).getFormatter();
+ for(int i = 0; i < seriesSize; i++) {
+
+ final XYSeries highSeries = sfList.get(HIGH_INDEX).getSeries();
+ final XYSeries lowSeries = sfList.get(LOW_INDEX).getSeries();
+ final XYSeries openSeries = sfList.get(OPEN_INDEX).getSeries();
+ final XYSeries closeSeries = sfList.get(CLOSE_INDEX).getSeries();
+
+ // x-val for all series should be identical so just grab x from the first series:
+ Number x = highSeries.getX(i);
+
+ Number high = highSeries.getY(i);
+ Number low = lowSeries.getY(i);
+ Number open = openSeries.getY(i);
+ Number close = closeSeries.getY(i);
+
+ // draw the candlestick:
+ final PointF highPix = getPlot().getBounds().transformScreen(x, high, plotArea);
+ final PointF lowPix = getPlot().getBounds().transformScreen(x, low, plotArea);
+ final PointF openPix = getPlot().getBounds().transformScreen(x, open, plotArea);
+ final PointF closePix = getPlot().getBounds().transformScreen(x, close, plotArea);
+
+ drawWick(canvas, highPix, lowPix, formatter);
+ drawBody(canvas, openPix, closePix, formatter);
+ drawUpperCap(canvas, highPix, formatter);
+ drawLowerCap(canvas, lowPix, formatter);
+
+ // draw labels, if any:
+ final PointLabelFormatter plf = formatter.hasPointLabelFormatter()
+ ? formatter.getPointLabelFormatter() : null;
+ final PointLabeler pointLabeler = formatter.getPointLabeler();
+ if(plf != null && pointLabeler != null) {
+ drawTextLabel(canvas, highPix, pointLabeler.getLabel(highSeries, i), plf);
+ drawTextLabel(canvas, lowPix, pointLabeler.getLabel(lowSeries, i), plf);
+ drawTextLabel(canvas, openPix, pointLabeler.getLabel(openSeries, i), plf);
+ drawTextLabel(canvas, closePix, pointLabeler.getLabel(closeSeries, i), plf);
+ }
+ }
+ }
+
+ protected void drawTextLabel(Canvas canvas, PointF coords, String text, PointLabelFormatter plf) {
+ if(text != null) {
+ canvas.drawText(text, coords.x + plf.hOffset, coords.y + plf.vOffset, plf.getTextPaint());
+ }
+ }
+
+ protected void drawWick(Canvas canvas, PointF min, PointF max, FormatterType formatter) {
+ canvas.drawLine(min.x, min.y, max.x, max.y, formatter.getWickPaint());
+ }
+
+ protected void drawBody(Canvas canvas, PointF open, PointF close, FormatterType formatter) {
+ final float halfWidth = formatter.getBodyWidth() / 2;
+ final RectF rect = new RectF(open.x - halfWidth, open.y, close.x + halfWidth, close.y);
+
+ Paint bodyFillPaint = open.y >= close.y ?
+ formatter.getRisingBodyFillPaint() : formatter.getFallingBodyFillPaint();
+
+ Paint bodyStrokePaint = open.y >= close.y ?
+ formatter.getRisingBodyStrokePaint() : formatter.getFallingBodyStrokePaint();
+
+ switch(formatter.getBodyStyle()) {
+ case SQUARE:
+ canvas.drawRect(rect, bodyFillPaint);
+ canvas.drawRect(rect, bodyStrokePaint);
+ break;
+ case TRIANGULAR:
+ drawTriangle(canvas, rect, bodyFillPaint, bodyStrokePaint);
+ }
+ }
+
+ protected void drawUpperCap(Canvas canvas, PointF val, FormatterType formatter) {
+ final float halfWidth = formatter.getUpperCapWidth();
+ canvas.drawLine(val.x - halfWidth, val.y, val.x + halfWidth, val.y, formatter.getUpperCapPaint());
+ }
+
+ protected void drawLowerCap(Canvas canvas, PointF val, FormatterType formatter) {
+ final float halfWidth = formatter.getLowerCapWidth();
+ canvas.drawLine(val.x - halfWidth, val.y, val.x + halfWidth, val.y, formatter.getLowerCapPaint());
+ }
+
+ @Override
+ protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) {
+ // TODO
+ }
+
+ protected void drawTriangle(Canvas canvas, RectF rect,
+ Paint fillPaint, Paint strokePaint) {
+ Path path = new Path();
+ path.moveTo(rect.centerX(), rect.bottom);
+ path.lineTo(rect.left,rect.top);
+ path.lineTo(rect.right, rect.top);
+ path.close();
+ canvas.drawPath(path, fillPaint);
+ canvas.drawPath(path, strokePaint);
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CandlestickSeries.java b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickSeries.java
new file mode 100644
index 00000000..40ec09ae
--- /dev/null
+++ b/androidplot-core/src/main/java/com/androidplot/xy/CandlestickSeries.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2016 AndroidPlot.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.androidplot.xy;
+
+import com.androidplot.xy.SimpleXYSeries;
+
+import java.util.*;
+
+/**
+ * Convenience class for representing a series of candlestick values;
+ * is NOT a descendant of {@link com.androidplot.xy.XYSeries} and therefore
+ * cannot be directly added to an {@link com.androidplot.xy.XYPlot}.
+ *
+ * This class is NOT threadsafe.
+ *
+ * @since 0.9.8
+ */
+public class CandlestickSeries {
+
+ private SimpleXYSeries highSeries = new SimpleXYSeries(null);
+ private SimpleXYSeries lowSeries = new SimpleXYSeries(null);
+ private SimpleXYSeries openSeries = new SimpleXYSeries(null);
+ private SimpleXYSeries closeSeries = new SimpleXYSeries(null);
+
+ protected static List generateRange(int start, int end) {
+ List range = new ArrayList<>(end - start);
+ for(int i = start; i < end; i++) {
+ range.add(i);
+ }
+ return range;
+ }
+
+ public CandlestickSeries(Item... items) {
+ this(Arrays.asList(items));
+ }
+
+ /**
+ * Creates a new CandlestickSeries.
+ * Calls {@link #CandlestickSeries(List, List)} with a list of xVals
+ * generated using the formula x=i.
+ * @param items
+ */
+ public CandlestickSeries(List- items) {
+ this(generateRange(0, items.size()), items);
+ }
+
+ public CandlestickSeries(List xVals, List
- items) {
+ if(xVals.size() != items.size()) {
+ throw new IllegalArgumentException("xVals and yVals length must be identical.");
+ }
+ for(int i = 0; i < xVals.size(); i++) {
+ Number x = xVals.get(i);
+ highSeries.addLast(x, items.get(i).getHigh());
+ lowSeries.addLast(x, items.get(i).getLow());
+ openSeries.addLast(x, items.get(i).getOpen());
+ closeSeries.addLast(x, items.get(i).getClose());
+ }
+ }
+
+ public SimpleXYSeries getHighSeries() {
+ return highSeries;
+ }
+
+ public void setHighSeries(SimpleXYSeries highSeries) {
+ this.highSeries = highSeries;
+ }
+
+ public SimpleXYSeries getLowSeries() {
+ return lowSeries;
+ }
+
+ public void setLowSeries(SimpleXYSeries lowSeries) {
+ this.lowSeries = lowSeries;
+ }
+
+ public SimpleXYSeries getOpenSeries() {
+ return openSeries;
+ }
+
+ public void setOpenSeries(SimpleXYSeries openSeries) {
+ this.openSeries = openSeries;
+ }
+
+ public SimpleXYSeries getCloseSeries() {
+ return closeSeries;
+ }
+
+ public void setCloseSeries(SimpleXYSeries closeSeries) {
+ this.closeSeries = closeSeries;
+ }
+
+ public static class Item {
+ private double low;
+ private double high;
+ private double open;
+ private double close;
+
+ /**
+ * An individual candlestick value. Since it is illegal to include
+ * null values for any member of a candlestick, this class is modeled with
+ * double values instead of {@link Number} instances.
+ * @param low
+ * @param high
+ * @param open
+ * @param close
+ */
+ public Item(double low, double high, double open, double close) {
+ this.low = low;
+ this.high = high;
+ this.open = open;
+ this.close = close;
+ }
+
+ public double getLow() {
+ return low;
+ }
+
+ public void setLow(double low) {
+ this.low = low;
+ }
+
+ public double getHigh() {
+ return high;
+ }
+
+ public void setHigh(double high) {
+ this.high = high;
+ }
+
+ public double getOpen() {
+ return open;
+ }
+
+ public void setOpen(double open) {
+ this.open = open;
+ }
+
+ public double getClose() {
+ return close;
+ }
+
+ public void setClose(double close) {
+ this.close = close;
+ }
+ }
+}
diff --git a/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java b/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java
index f836e89c..aea93ecd 100644
--- a/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java
+++ b/androidplot-core/src/main/java/com/androidplot/xy/CatmullRomInterpolator.java
@@ -20,7 +20,7 @@
import java.util.List;
/**
- * A primitive implementation of Catmull-Rom interpolation, based on the information found at:
+ * An implementation of Catmull-Rom interpolation, based on the information found at:
* http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/19283471#19283471
*/
public class CatmullRomInterpolator implements Interpolator {
@@ -66,11 +66,11 @@ public void setType(Type type) {
* Wraps a normal XYSeries, inserting a new point at the beginning and end of the series.
*/
static class ExtrapolatedXYSeries implements XYSeries {
- private final XY first;
- private final XY last;
+ private final XYCoords first;
+ private final XYCoords last;
private final XYSeries series;
- public ExtrapolatedXYSeries(XYSeries series, XY first, XY last) {
+ public ExtrapolatedXYSeries(XYSeries series, XYCoords first, XYCoords last) {
this.series = series;
this.first = first;
this.last = last;
@@ -119,7 +119,7 @@ public String getTitle() {
* @throws java.lang.IllegalArgumentException if pointsPerSegment is less than 2.
*/
@Override
- public List interpolate(XYSeries series, Params params) {
+ public List interpolate(XYSeries series, Params params) {
if (params.getPointPerSegment() < 2) {
throw new IllegalArgumentException(
"pointsPerSegment must be greater than 2, since 2 points is just the linear segment.");
@@ -140,7 +140,7 @@ public List interpolate(XYSeries series, Params params) {
double y1 = series.getY(0).doubleValue() - dy;
// Actually create the start point from the extrapolated values.
- XY start = new XY(x1, y1);
+ XYCoords start = new XYCoords(x1, y1);
// Repeat for the end control point.
int n = series.size() -1;
@@ -148,7 +148,7 @@ public List interpolate(XYSeries series, Params params) {
dy = series.getY(n).doubleValue() - series.getY(n - 1).doubleValue();
double xn = series.getX(n).doubleValue() + dx;
double yn = series.getY(n).doubleValue() + dy;
- XY end = new XY(xn, yn);
+ XYCoords end = new XYCoords(xn, yn);
// TODO: figure out whether this extra control-point synthesis is
// TODO: really necessary and either remove the above or fix the below.
@@ -162,14 +162,14 @@ public List interpolate(XYSeries series, Params params) {
ExtrapolatedXYSeries extrapolatedXYSeries = new ExtrapolatedXYSeries(series, start, end);
// Dimension a result list of coordinates.
- List result = new ArrayList<>();
+ List result = new ArrayList<>();
// When looping, remember that each cycle requires 4 points, starting
// with i and ending with i+3. So we don't loop through all the points.
for (int i = 0; i < extrapolatedXYSeries.size() - 3; i++) {
// Actually calculate the Catmull-Rom curve for one segment.
- List points = interpolate(extrapolatedXYSeries, i, params);
+ List points = interpolate(extrapolatedXYSeries, i, params);
// Since the middle points are added twice, once for each bordering
// segment, we only add the 0 index result point for the first
@@ -196,8 +196,8 @@ public List interpolate(XYSeries series, Params params) {
* @return the list of coordinates that define the CatmullRom curve
* between the points defined by index+1 and index+2.
*/
- public List interpolate(XYSeries series, int index, Params params) {
- List result = new ArrayList<>();
+ protected List interpolate(XYSeries series, int index, Params params) {
+ List result = new ArrayList<>();
double[] x = new double[4];
double[] y = new double[4];
double[] time = new double[4];
@@ -226,13 +226,13 @@ public List interpolate(XYSeries series, int index, Params params) {
}
int segments = params.getPointPerSegment() - 1;
- result.add(new XY(series.getX(index + 1), series.getY(index + 1)));
+ result.add(new XYCoords(series.getX(index + 1), series.getY(index + 1)));
for (int i = 1; i < segments; i++) {
double xi = interpolate(x, time, tstart + (i * (tend - tstart)) / segments);
double yi = interpolate(y, time, tstart + (i * (tend - tstart)) / segments);
- result.add(new XY(xi, yi));
+ result.add(new XYCoords(xi, yi));
}
- result.add(new XY(series.getX(index + 2), series.getY(index + 2)));
+ result.add(new XYCoords(series.getX(index + 2), series.getY(index + 2)));
return result;
}
@@ -251,7 +251,7 @@ public List