Permalink
3e18f39 Dec 16, 2016
408 lines (316 sloc) 15.8 KB

XYPlot

An XYPlot renders one or more XYSeries onto an instance of XYGraphWidget. It also includes two instances of TextLabelWidget used to display an optional title for the domain and range axis, and an instance of XYLegendWidget which by default will automatically display legend items for each XYSeries added to the plot.

image

XYSeries

XYSeries is the interface Androidplot uses to retrieve your numeric data. You may either create your own implementation of XYSeries or use SimpleXYSeries if you don't have tight performance requirements or if your numeric data is easily accessed as an array or list of values.

SimpleXYSeries

As a convenience, Androidplot provides an all-purpose implementation of the XYSeries interface called SimpleXYSeries. SimpleXYSeries is used to wrap your raw data with an implementation of the XYSeries interface.

You can supply your data in several ways:

As a list of y-vals (x = i for each supplied y-val):

Number[] yVals = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64};
XYSeries series1 = new SimpleXYSeries(
    Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "my series");

An interleaved list of x/y value pairs (x[0] = 1, y[0] = 4, x[1] = 2, y[1] = 8, ...):

Number[] yVals = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64};
XYSeries series1 = new SimpleXYSeries(
    Arrays.asList(yVals), SimpleXYSeries.ArrayFormat.XY_VALS_INTERLEAVED, "my series");

Separate lists of x-vals and y-vals:

Number[] xVals = {1, 4, 2, 8, 4, 16, 8, 32, 16, 64};
Number[] yVals = {5, 2, 10, 5, 20, 10, 40, 20, 80, 40};
XYSeries series = new SimpleXYSeries(xVals, yVals, "my series");

Keep in mind that SimpleXYSeries is designed to be easy to use for a broad number of applications; it's not optimized for any specific scenario; if you are dynamically displaying data that needs to be refreshed more than several times a second, consider building your own implementation of XYSeries designed for your app's specific needs.

The Graph

XYGraphWidget encapsulates XYPlot's graphing functionality. Given an instance of XYPlot, a reference to XYGraphWidget can be retrieve via XYPlot.getGraph().

Domain & Range Boundaries

By default, Androidplot will analyze all XYSeries instances registered with the XYPlot, determine the min/max values for domain and range and adjust the XYPlot boundaries to match those values. If your plot contains dynamic data, especially if your plot can periodically contain either no series data or data with no resolution on one or both axis (all identical values for either x or y) then you may want to manually set your XYPlot's domain and range boundaries.

To set your plot's boundaries use:

  • XYPlot.setDomainBoundaries(Number value, BoundaryMode mode)
  • XYPlot.setRangeBoundaries(Number value, BoundaryMode mode)

Note that the value argument is only used when setting BoundaryMode.FIXED. For all other modes, pass in null.

BoundaryMode

Androidplot provides four boundary modes:

FIXED

The plot's boundaries on the specified axis are fixed to user defined values.

AUTO (default)

The plot's boundaries auto adjust to the min/max values for the defined axis.

GROW

The plot's boundaries automatically increase to the max value encountered by the plot. The initial determines the starting boundaries from which the BoundaryMode.GROW behavior will be based.

SHRINK

The plot's boundaries automatically shrink to the min value encountered by the plot. The initial determines the starting boundaries from which the shrink behavior will be based.

Domain & Range Lines

These are the horizontal lines drawn on a graph. These lines are configured via:

  • XYPlot.setDomainStep(StepMode mode, Number value)
  • XYPlot.setRangeStep(StepMode, Number value)

Androidplot provides these step modes:

Subdivide

When using BoundaryMode.SUBDIVIDE, the graph is subdivided into the specified number of sections.

IncrementByValue

BoundaryMode.INCREMENT_BY_VALUE instructs Androidplot draw grid lines at the specified interval. This is the most commonly used modes as is produces an easy to read result.

IncrementByPixels

BoundaryMode.INCREMENT_BY_PIXELS behaves identically to BoundaryMode.INCREMENT_BY_VALUE except that the increment quantity is expressed in pixels.

Domain & Range Labels

Androidplot supports labeling domain values on either or both the top and bottom graph edges and range values on either or both the left and right graph edges. Most default styles show labels only on the left and bottom edges.

Line Label Insets

Insets are used to control where line labels are drawn in relation to the graph space. The Insets instance can be obtained via XYPlot.getGraph().getLineLabelInsets(). For example, to move the range labels on the left of the graph further to the left by 5dp:

xml

ap:lineLabelInsetLeft="-5dp"

java

plot.getGraph().getLineLabelInsets().setLeft(PixelUtils.dpToPix(-5));

Dual Axis Labels

Androidplot provides methods for enabling / disabling axis labels along all edges of the graph. By default, only the left and bottom edge labels are enabled. To enable labels on the right edge:

xml

ap:lineLabels="left|bottom|right"

java

plot.getGraph().setLineLabelEdges(
    XYGraphWidget.Edge.BOTTOM, 
    XYGraphWidget.Edge.LEFT, 
    XYGraphWidget.Edge.RIGHT);

Once the edge has been enabled, text formatting can be controlled by enabling a custom formatter for the desired edge:

plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.RIGHT).setFormat(new Format() {
    @Override
    public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
        // obj contains the raw Number value representing the position of the label being drawn.
        // customize the labeling however you want here:
        int i = Math.round(((Number) obj).floatValue());
        return toAppendTo.append(i + " thingies");
    }

    @Override
    public Object parseObject(String source, ParsePosition pos) {
        // unused
        return null;
    }
});

You're likely also going to need to normalize your series data in order to get it to display properly. To help simplify this step, Androidplot provides NormedXYSeries which can be used to wrap any XYSeries to provide the normalized representation of it's data.

There's a full reference implementation of a dual scale plot using a custom Formatter and NormedXYSeries in the DemoApp.

Line Label Interval

Androidplot allows you to configure the interval at which labels are rendered for domain and range lines:

  • XYPlot.getGraph().setLinesPerDomainLabel(int interval)
  • XYPlot.getGraph().setLinesPerRangeLabel(int interval)

LineLabelStyle

The styling of the line labels drawn on each edge of the graph is controlled by it's associated style. this style contains params that control:

  • Paint used to draw labels (determines, color, font size, etc.)
  • Format used to draw text (can be NumberFormat, etc.)
  • Rotation of the label text

To get the style used to draw the left edge (range) labels:

plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT);

LineLabelRenderer

If you need to implement special rendering behavior for your line labels, such as drawing graphics, symbols, etc. you can create a custom renderer by extending LineLabelRenderer and injecting it into the graph:

plot.getGraph().setLineLabelRenderer(XYGraphWidget.Edge.LEFT, customLineLabelRenderer);

f(x) plot example source provides an example of this kind of customization.

Pan & Zoom

You can enable pan/zoom behavior on any instance of XYPlot using the PanZoom class like this:

PanZoom.attach(plot);

The default behavior is to enable horizontal and vertical panning an to zoom using uniform scaling. If you want to override this behavior use the three argument form of PanZoom.attach(Plot). For example, to enable pan and zoom (stretch) on the horizontal axis only:

PanZoom.attach(plot, PanZoom.Pan.HORIZONTAL, PanZoom.Zoom.STRETCH_HORIZONTAL);

Pan and zoom operations abide by your plot's defined outer limits limits. If no such limits have been set then the plot will pan and zoom on both axes infinitely. To set the plot's outer limits:

// cap pan/zoom limits for panning and zooming to a 100x100 space:
plot.getOuterLimits().set(0, 100, 0, 100);

For a more detailed look at pan & zoom behavior, check out the Touch Zoom Example source code.

Series Renderers

There are several renderers available for XYPlots:

  • LineAndPointRenderer
  • BarRenderer
  • CandlestickRenderer

When you add a new series to your plot, you tell Androidplot how to render it by passing in a subclass of XYSeriesFormatter that corresponds to the desired renderer. For example, to use LineAndPointRenderer, you'd register your series with an instance of LineAndPointFormatter:

LineAndPointFormatter format = new LineAndPointFormatter();
plot.addSeries(series, format);

If you need special behavior not provided by an existing renderer you can create your own by either extending XYSeriesRenderer or one of the above implementations. You'll also need to create a matching implementation of XYSeriesFormatter that returns your renderer's class from it's getRendererClass() method.

LineAndPointRenderer

LineAndPointRenderer is the go-to renderer when it comes to XYSeries. It provides the most robust feature set of any XYSeriesRenderer and has been the most carefully optimized for performance. Having said that, LineAndPointRenderer isn't always the best tool for the job. Androidplot includes two additional variations of LineAndPointRenderer:

  • FastLineAndPointRenderer - intended for use in apps displaying large amounts of dynamic data where fast refresh rates are important.
  • AdvancedLineAndPointRenderer - provides capabilities for dynamically coloring individual line segments, etc. See the ECGExample source in the demo app for a complete example.

Labeling Points

Most implementations of XYSeriesRenderer support labeling rendered points with text. This functionality is activated by setting the PointLabelFormatter property in the associated XYSeriesFormatter. For example, to enable point labels for a LineAndPointFormatter:

// create a new PointLabelFormatter with a text size of 12sp and a color of red:

PointLabelFormatter plf = new PointLabelFormatter();
plf.getTextPaint().setTextSize(PixelUtils.spToPix(12));
plf.getTextPaint().setColor(Color.RED);
lineAndPointFormatter.setPointLabelFormatter(plf);

By default this will enable labels for all points using a string representation of the yVal of each point. This behavior can be customized by setting a custom instance of PointLabeler on the XYSeriesFormatter:

formatter.setPointLabeler(new PointLabeler() {
    @Override
    public String getLabel(XYSeries series, int index) {
        // draw labels on even indexes only:
        if(index % 2 == 0) {
            return "Y=" + series.getY(index).doubleValue();
        }
        return null;
    }
});

BarRenderer

See the barcharts documentation.

CandlestickRenderer

See the candlestick documentation

Drawing Smooth Lines

Smooth lines can be created by applying the Catmull-Rom interpolator to your series' Format.

The Legend

By default, Androidplot will automatically produce a legend for your Plot. You however choose to hide the legend or you can customize it to suit your needs.

Hiding Legend Items

As mentioned above, Androidplot automatically produces a legend for your Plot. This "auto legend" includes items for each series added to the plot. If you wish to omit a series from the legend:

formatter.setLegendIconEnabled(false);

The TableModel

The TableModel controls how and where each item in the legend is drawn. Androidplot provides two default implementations; DynamicTableModel and FixedTableModel (detailed below). All TableModel implementations organize elements into a grid. This grid is populated with items based on the order which it's corresponding series was added to the plot. This ordering can be further controlled by setting the TableModel's TableOrder param to either ROW_MAJOR (items are added left-to-right, top-down) or COLUMN_MAJOR (items are added top-down, left-to-right).

DynamicTableModel

The DynamicTableModel takes a desired of numbered rows and columns and evenly subdivides the LegendWidget's visible space into cells. For example, A 2x2 legend using ROW_MAJOR ordering:

plot.getLegend().setTableModel(new DynamicTableModel(2, 2, TableOrder.ROW_MAJOR));

FixedTableModel

The FixedTableModel takes a desired size of each cell in pixels and adds cells using the specified TableOrder. It automatically wraps to the next row or column (based on TableOrder) when the cell being added exceeds the legend's available space on a given axis. For example, A FixedTableModel using 300w*100h cells and a TableOrder of COLUMN_MAJOR:

plot.getLegend().setTableModel(new FixedTableModel(PixelUtils.dpToPix(300), 
    PixelUtils.dpToPix(100), TableOrder.COLUMN_MAJOR));

Graph Rotation

Androidplot provides the Widget.setRotation(Widget.Rotation) method for controlling the orientation of Widgets. For example, if you wanted to create a bar graph where the bars extended across the screen from left to right:

xml

ap:graphRotation="ninety_degrees"

java

plot.getGraph().setRotation(Widget.Rotation.NINETY_DEGREES);

Optimization Tips

Here are a few suggestions to improve performance when plotting dynamic data:

  • Create your own implementation of XYSeries to work with your data in it's rawest form.
  • Use FastLineAndPointRenderer instead of LineAndPointRenderer:
plot.addSeries(azimuthHistorySeries,
    new FastLineAndPointRenderer.Formatter(
        Color.rgb(100, 100, 200), null, null, null));
  • Consider averaging or subsampling very large datasets before rendering. If you have the time and inclination, the LTTB algorithm is particularly well suited for downsampling XYSeries data.
  • If possible, avoid rendering vertices (points).
  • Disable anti-aliasing on your XYSeriesFormatter's paint values:
LineAndPointFormatter format = new LineAndPointFormatter(...);
format.getLinePaint().setAntiAlias(false);

Screen<->Series Conversion

Because the coordinate system used by your XYSeries data is almost always different than the screen coordinate system upon which the data is rendered, you'll often need to convert from one system to the other. XYPlot provides convenience methods for this purpose:

To convert screen vals to series vals:

// x
float screenX = ...
Number x = plot.screenToSeriesX(screenX);

// y
float screenY = ...
Number y = plot.screenToSeriesY(screenY);

// x and y
PointF screenCoords = ...
XYCoords xy = plot.screenToSeries(screenCoords)

To convert series vals to screen vals:

// x
Number x = ...
float screenX = plot.seriesToScreenX(x);

// y
Number y = ...
float screenY = plot.seriesToScreenY(y);

// x and y
XYCoords xy = ...
PointF screenCoords = plot.series.Screen(xy);

Whats Next?

Explore Advanced XYPlot Topics