Skip to content
Ivan Schütz edited this page Nov 6, 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.

Settings

There are global axis settings, which are in ChartSettings, and individual axis settings, which are passed to the axis models.

Global axis settings (ChartSettings)

axisettings

  1. labelsToAxisSpacingY
  2. labelsToAxisSpacingX
  3. labelsSpacing
  4. axisTitleLabelsToLabelsSpacing
  5. spacingBetweenAxesY
  6. spacingBetweenAxesX

Additionally, with axisStrokeWidth it's possible to define the stroke width of the axes.

Individual axis settings (ChartAxisModel)

Line color

Sets the line color of the axis

Padding

Empty initial space in pt 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

During a chart transformation (zoom/pan), the labels on the y axis may have different lengths or there may be at times no visible x-axis labels (because they are too far apart and the value generator doesn't generate values in between). The space reservation mode determines how the chart inner frame reacts to this. These are the supported modes:

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".

Warning: .current mode currently can lead to misalignments between chart components (e.g. bars don't show correctly aligned with grid/labels), in following setup: 1) Labels (in particular of the y-axis) have different lengths, 2) Combining layers that use a per-element transform matrix (e.g. gridlines, axes, scatter points) with layers that inherit the transform matrix of content view - i.e. scalable UIView based layers, like bars (for more details about transform management please visit this section) 3) The chart is zoomable/pannable, such that panning across labels with different lengths will cause the content frame continuously to resize. The solution in these cases is to use .maxPresentedSize or .fixed(CGFloat) mode instead.

Other settings

Label rotation

You can specify a label rotation in the ChartLabelSettings instance that's passed to the label generator or fixed axis values array. You can set a different rotation for individual labels, if for some reason this was necessary.

Showing images or other views

In some cases you may want to show something different than text as axis labels, or maybe together with the labels. The axis layers provided by SwiftCharts have the suffix "default" for a reason - you can implement your custom layers that render whatever you want, which have to conform to the ChartAxisLayer protocol (Note: this protocol currently is coupled with ChartAxisLabelsGenerator but this shouldn't be the case. If you need to implement custom axes and this is a problem please open an issue!).

Implementing a custom axis layer may be an interesting task, in particular if it can be added to SwiftCharts to provide such functionality in the future, but there's also a slightly easier way. You can use ChartPointsViewsLayer to generate views for the axis values as if they were chart points. In order for this to work, you have to disable the clipping of the inner frame (ChartSettings.clipInnerFrame). For it to work with zooming and panning you need a somewhat more complex version of this approach. For more details, see this issue.

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. There are also some additional helpers which make life a little more easier when working with standard configurations like one left y-axis and one bottom x-axis. We only have to use ChartCoordsSpace directly when working with a non standard 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 y-axis, bottom x-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.

Hiding axis lines

To hide both axis lines, pass 0 as ChartSettings.axisStrokeWidth to the chart. To hide only one of them, you can pass a transparent lineColor parameter to ChartAxisModel (The latter is admittedly a bit workaround-ish. I may add axisStrokeWidth to the axis models too in the future).

Hiding an axis layer

From the above follows, that to hide an axis you simply have to not pass its layer to the chart's layers array. Depending on how we define "hiding", this may be not entirely true, as 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

To work with multiple axes you have to use multiple layers and pass to each layer the x and y axis it should use. For example, while to display a chart with 3 lines using the same domain (i.e. a single x and y axis) you would use only one line layer, to add a second y axis and have one of the lines referencing it, you would create a new line layer, which would be initialized with this second y axis instead of the other and the line you want to use it.

So far it's cleared how to create content layers when we have the axes, but how to we create the axes? How do we transform our axis models into axis layers, that will be positioned where we want? This is where the more explicit way to use ChartCoordsSpace comes into play, where we can pass arrays of axis models, instead of only one x and y axis:

ChartCoordsSpace(chartSettings: chartSettings, chartSize: viewFrame.size, yLowModels: yLowModels, yHighModels: yHighModels, xLowModels: xLowModels, xHighModels: xHighModels)

The mapping of the arrays and positions in these and the actual position of the axes on the screen can be viewed in the following image:

multiaxis

To retrieve the generated axis layers from the ChartCoordsSpace's result (the returned object from above initializer), you use the same indices you used for the axis models, on the following arrays:

let yLowAxes = coordsSpace.yLowAxesLayers
let yHighAxes = coordsSpace.yHighAxesLayers
let xLowAxes = coordsSpace.xLowAxesLayers
let xHighAxes = coordsSpace.xHighAxesLayers

Now you have the axis layers, which contain the axes that you need to initialize the chart layers, in the same way as it's done when working with single axes.

Here's a full example.

Transform axis locking

See View hierarchy and transforms

Clone this wiki locally