Skip to content
Ivan Schütz edited this page Apr 28, 2017 · 39 revisions

An axis can be created with a fixed array of values or generators, which allow more dynamic behaviours. A chart can have an unlimited number of axes on each side (left/bottom/top/right).

It's probably helpful to keep in mind, before we start with more details, that the axes are entirely decoupled from the chart points. The axes don't care about the chart data. If the data happens to be in the axis range, it will be displayed, otherwise it will show somewhere outside of the axis boundaries (clipped, unless explicitly disabled).

Axis model

The first step to make an axis, is to create an axis model. The axis model is basically a specification of our axis, which will be used to generate the axis layer that will be displayed on the screen. We can create an axis model using a fixed values array or generators.

Fixed values array

This was the only possible way to define axes pre-0.6. 0.6 works only with generators, but for convenience and backwards compatibility, it's still possible to use fixed values (they are mapped internally to generators).

Let's see how we create an axis with a fixed array of values. This is a very simple axis with the values 0, 2 and 4:

let labelSettings = ChartLabelSettings();
let values = [
    ChartAxisValueInt(0, labelSettings: labelSettings),
    ChartAxisValueInt(2, labelSettings: labelSettings),
    ChartAxisValueInt(4, labelSettings: labelSettings),
]
let xModel = ChartAxisModel(axisValues: values, axisTitleLabel: ChartAxisLabel(text: "Axis title", settings: labelSettings))

It's possible to pass also values representing irregular intervals, e.g. (-1, 2, 10...). It's only important that they are sorted.

We used instances of ChartAxisValueInt for our values. ChartAxisValueInt extends ChartAxisValue which is a thing that can be represented with a number (called "scalar"), and can produce an array of labels, which will be displayed in the chart. There are several default implementations of ChartAxisValue, like ChartAxisValueInt, ChartAxisValueDouble (which has a number formatter) and ChartAxisValueDate, among others. You of course can also create your own custom axis values, by extending ChartAxisValue.

Generators

For more dynamic behaviour and performance, in particular when using zooming, we use generators. Let's create an axis model for an axis that goes from 0 to 100, with an interval size of 5:

let valuesGenerator = ChartAxisGeneratorMultiplier(5)
let labelsGenerator = ChartAxisLabelsGeneratorNumber(ChartLabelSettings())

let xModel = ChartAxisModel(firstModelValue: 0, lastModelValue: 100, axisTitleLabels: [ChartAxisLabel(text: "Axis title", settings: labelSettings)], axisValuesGenerator: valuesGenerator, labelsGenerator: labelsGenerator)

Here we don't pass anymore a values array but a specification of how we want these values to be generated. We see 2 new things, a values generator and a label generator. Let's look at these in more detail.

Values generator

Values generators implement the protocol ChartAxisValuesGenerator. The function of this class is to generate an array of Double, based on the current state of the axis. Each time the visible area of the chart changes (during initialization or zooming and panning) the generator is called to generate axis values. It's up to the generator how to do this. In most cases it's wise to generate values only for the part of the axis which is currently visible (all which can be queried from the passed ChartAxis), but that's not enforced.

SwiftCharts provides some implementations of ChartAxisValuesGenerator:

ChartAxisGeneratorMultiplier

This generates values for the visible part of the axis, which are multiples of the passed multiplier. For example, a multiplier generator, used in an axis with the start values 0...100, will generate the values 0, 5, 10, 15, etc. When zooming, this generator will simply create a new axis value in the middle between two axis values when they become too far apart (2x the current interval size). For example, if we have an axis with the values 2, 4, 6 and we zoom in 2x, we get the values 2, 3, 4, 5, 6 with this mode. The next time we zoom in 2x, we get 2, 2.5, 3... etc.

ChartAxisGeneratorNice

This generator is a subclass of ChartAxisGeneratorMultiplier which generates more user friendly numbers when zooming in.

ChartAxisValuesGeneratorDate

This generator is also a subclass of ChartAxisGeneratorMultiplier dedicated to dates.

ChartAxisValuesGeneratorFixed

This generator wraps a fixed values array. It allows to pass values at specific intervals which can't be automatically generated. It's also what's used for backwards compatibility with the fixed axis values array used in SwiftCharts pre-0.6.

ChartAxisValuesGeneratorFixedNonOverlapping

This is a subclass of ChartAxisValuesGeneratorFixed with some additional functionality to avoid overlapping values.

You of course can also create your own ChartAxisValuesGenerator implementations!

Labels generator

The job of a label generator is convert the numbers generated by ChartAxisValuesGenerator into text - whatever it is, a formatted number, alphabetic string, etc. More specifically, it maps a Double to a ChartAxisLabel (which is just a wrapper for text and some text related state). The label generator doesn't have any awareness of the axis, it just receives individual numbers.

A label generator conforms to the protocol ChartAxisLabelsGenerator. These are the label generators currently provided by SwiftCharts:

ChartAxisLabelsGeneratorBase

Not meant to be used directly, it holds some useful state, like a cache, so we just have to generate a label for a specific Double only once.

ChartAxisLabelsGeneratorNumber

Formats numbers, using a NumberFormatter. Extends ChartAxisLabelsGeneratorBase.

ChartAxisLabelsGeneratorNumberSuffix

This generates shortened numbers, with kilo/mega/etc. suffixes. E.g. it displays 1K instead of 1000. Extends ChartAxisLabelsGeneratorBase.

ChartAxisLabelsGeneratorDate

Formats dates, using a DateFormatter.

ChartAxisLabelsGeneratorFunc

Wraps an arbitrary function f: (Double) -> [ChartAxisLabel] that's used to generate the labels.

ChartAxisLabelsGeneratorFixed

Contains a dictionary with fixed mappings Double: [ChartAxisLabel]. The main purpose of this is probably backwards compatibility, to map the fixed axis values array to the new api.

Additional axis model settings

Padding

We can specify padding, in pt, which is an empty initial space between the leading/trailing border of the inner frame and the start/end of the axis. This of doesn't affect the axis line - only the displayed values. The contents of the chart of course are then "moved" inwards to match the modified axis values. Note that padding doesn't stay fixed during zooming or panning, it works like normal axis content, being thus limited only to the non transformed state of the chart.

There are several options to fine-tune the padding, in particular in relation with the axis labels. This is the enum code with some inline explanations, which is probably enough to understand how it works:

public enum ChartAxisPadding {
    case label /// Add padding corresponding to half of leading / trailing label sizes
    case none
    case fixed(CGFloat) /// Set a fixed padding value
    case maxLabelFixed(CGFloat) /// Use max of padding value corresponding to .Label and a fixed value
    case labelPlus(CGFloat) /// Use .Label padding + a fixed value
}

In this image, for example, we use padding .label for both axes, leading and trailing:

padding

Clipping

You also can control the presentation of the labels (and other possible content, in case you are using custom axes) by passing a clipContents flag to the axis model. Here we see the effect of clipping on the leading adge of an x-axis:

clipping

Of course, when clipping is disabled, the axis value still disappears when the tick (i.e. the center of the label in most cases) goes outside of the visible part of the axis, but this is due to the axis value generator, which generates ticks only for the visible part of the axis (nothing hinders you though of implementing an axis value generator that doesn't do this, but it normally doesn't make sense).

Space reservation mode

When a zoomed chart is panned, the labels on the y axis may have different lengths. The space reservation mode determines how we let the chart inner frame be affected by these length differences. Here's also the enum with inline explanations should be enough as documentation:

public enum AxisLabelsSpaceReservationMode {
    case minPresentedSize /// Doesn't reserve less space than the min presented label width/height so far
    case maxPresentedSize /// Doesn't reserve less space than the max presented label width/height so far
    case fixed(CGFloat) /// Fixed value, ignores labels width/height
    case current /// Reserves space for currently visible labels
}

For example, .current will make the chart resize immediately (during the gesture) when there are deltas in the max length of currently presented labels. maxPresentedSize will remember the max length presented so far and fix the reserved width by axis to that, minimizing the inner frame "jumps".

Axis layer

OK now we have an axis model. What's next? We have to use it to 1. together with the other axes models, compute the chart inner frame, i.e. the space available to display chart points and 2. convert it to an axis layer, which is what we use to initialize the chart layers and also what we have to pass to the chart instance in order to be displayed.

Sounds a bit complicated, but there's a class called ChartCoordsSpace which will do this work for us. What we have to do is simply to pass our axis models, together with the chart frame (i.e. the total frame available to display the chart, axes included), and let it generate the axis layers as well as compute the inner frame for us. There are also some additional helpers which make life a little more easier when working with standard axes like one left y-axis and one bottom x-axis. We only have to use ChartCoordsSpace directly when working with a non standard axes configuration, like e.g. 2 y-axis (more about this in the multiple axis section).

For example, to generate the axes layers and inner frame for a typical left x-axis, bottom y-axis chart we need this:

let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: chartFrame, xModel: xModel, yModel: yModel)
let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame)

(The chart settings are needed because they contain diverse spacing settings, which are involved in the calculation of the axis as well as the inner frame).

Using the axis layer

Now that we generated the axis layers, we can create chart content layers (i.e. the layers that render our data) with them. More specifically, with the ChartAxis instances which are contained in ChartAxisLayer. ChartAxis contains the logic to map between domain and screen coordinates, which is what the content layers (i.e. subclasses of ChartCoordsSpaceLayer) need. To instantiate a line layer, for example:

ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel]...)

Finally, if we want the axis to be rendered, we have to pass the axis layer to the chart's layers array. This isn't required logic-wise. The chart doesn't know anything about axes, it just sees layers that have to be rendered.

Note that when you don't pass an axis layer, the space will still be reserved. The reason of this is that the inner frame, as well as the edges of other axes were already computed by ChartCoordsSpace. The easiest way at the moment to not reserve space for an axis is to pass an axis model that's effectively empty and ensure that all the related spacing settings in ChartSettings are 0. A detailed list of these steps can be found in this issue.

Multiple axes

multiaxis

Example

TODO text