Conversion, Transformation and Projection Pipeline
This section describes all the processing stages that are applied to data given to a plot on its way to being displayed.
Overview
The pipeline can be broadly be summarized in 3 parts each with a few steps:
- Conversions which mainly normalize types
- `expand_dimensions()` adds defaulted/generated data (e.g. x, y in `image()`)
- `dim_convert` processes special types like Units
- `convert_arguments()` normalizes numeric types & data formats
- Transformations which transform data on a per-plot basis
- `transform_func` is a function applied to data
- `model` matrix applies linear transformations
- Projections which project data from one coordinate system to another
- `view` matrix moves data from "world" space to a camera "view/eye" space
- `projection` matrix moves from the camera space to "clip" space
- `viewport` moves "clip" space to "pixel/screen" space
As a user you have direct control over the model
matrix (1.2) with the scale!()
, translate!()
and rotate!()
functions. You have indirect control over projections (3) with the space
attribute. It sets what coordinate system is used as the initial space and adjusts the projections as a result. You also have indirect control over transform_func
, which can be set by passing a Transformation()
directly to a plot. However it usually inherited and controlled by the Axis
.
As a developer, i.e. someone who wants to extend Makie, you can interact with most these steps. Most likely you will extend with convert_arguments()
to allow special types to be plotted. But you can also implement more dim_converts, add methods for expand_dimensions()
, implement more transform functions or add a camera which produces its ownview
andprojection
matrix. Onlymodel
andviewport
handling as well as the interpretation ofspace
are set.
Argument Conversions
When calling a plot function, e.g. scatter!(axis_or_scene, args...)
a new plot object is constructed. It keeps track of the original input arguments as an Observables in plot.args
. Those input arguments are then converted by the conversion pipeline and stored in plot.converted
. The pipeline consists of 3 steps as mentioned above:
Data Generation
The first step is to generate "missing" data. For example, you can create an image plot with just image(rand(10, 10))
. The most general form however also includes a ClosedInterval
for the x and y dimensions, declaring the size of the image. This data is generated by expand_dimensions(::Trait, args...)
where the Trait = conversion_trait(::PlotType, args...)
.
Special Type Processing
The second step handles special types like Unitful
types, Dates
types or categorical values which need to be synchronized within the scene. For example, if one plot uses "hours" as unit for its x values other plots need to also use time units for x. If the scale of the unit differs between plots, i.e. one uses hours, the other minutes, then a common unit must be found and the values need to be scaled appropriately. This is what dim_converts handles. You can find more documentation on them in the Dimension conversions docs.
Convert Arguments
The last step and main work-horse in the conversion pipeline is the convert_arguments()
function. It's purpose is to convert different data types and layouts into one or a select few formats. For example, any data passed to scatter()
is converted to a Vector{Point{D, T}}
where D = 2
or 3
and T = Float32
or Float64
. These conversions can happen based on the plot type or its conversion trait. For scatter()
the conversion trait PointBased
is used.
convert_arguments()
can also accept keyword arguments sourced from plot attributes. For this the attribute needs to be marked with used_attribute(::PlotType) = (names...)
. Any name in that list will be removed from the attributes of the final plot and be passed to convert_arguments()
instead.
If you want to plot your own custom types you may want to extend convert_arguments()
. Let's say you have some custom type MySimulation
and some function positions(::MySimulation)
which returns positions you want to plot when calling scatter(::MySimulation)
. In this case you can define
function Makie.convert_arguments(PT::PointBased, sim::MySimulation)
return Makie.convert_arguments(PT, positions(sim))
end
to make that possible. You can use the plot type (i.e. Scatter
) as the first argument as well.
Transformations
After conversions have normalized the type and layout of plot data, transformations can now adjust it. They are handled by the Transformation
object, which exists both on the plot and scene level in the transformation
field. It contains a transform_func
which is a function-like object applied to the data, and a model
matrix which handles linear transformations of the data.
The plot Transformation
object may inherit from it's parent scene or plot if it acts in the same space
. For transform_func
this means using the same function as the parent. For model
it means merging the parents model
matrix with the local one as plot.model = parent.model * local_model
.
Transformation Function
The transformation function or transform_func
is part of the Transformation
object. It handles non-linear transformations like log transform of a logarithmic axis.
Each transform_func
implements at least
Makie.apply_transform(transform_func, arg::VecTypes{N, T}) where {N, T}
where the transform function can be represented by any type, not just a Function
. That way it can carry auxiliary information that may be important to the transformation. Additionally methods with other arg
types such as numbers of Vector
s thereof may also be implemented to more efficiently apply the transform_func.
Typically a transform_func also implements
Makie.inverse_transform(transform_func)
Makie.apply_transform(transform_func, arg::Rect3)
The inverse allows transforming data back, which is used for example in Axis limits. If a reasonable inverse exists (even if it is incomplete or ambiguous) it should be given by inverse_transform
. The other apply_transform
method is used in boundingbox()
and defaults to transforming the corners of the bounding box. It should be implemented if the default returns wrong results, e.g. with a Polar transform.
Model Transformations
The model matrix takes care of linear transformations of plot data. This includes scaling with scale!()
, translations with translate!()
and rotations with rotate!()
. See the Transformation reference docs for more details.
Projections
Projections are matrix transformations that move data from one coordinate system or "space" to another. The matrices are part of the camera(scene)
and managed either by the cameracontrols(scene)
or by the parent Block
. The plot cannot change these matrices but can control which will be used via the space
attribute.
Camera Controller
The camera controller, be that the object in the Scene or the parent Block, generates the view
and projection
matrices. These matrices are stored in camera(scene)
which also combines them into projectionview = projection * view
. The view
matrix moves plot data from a "world" coordinate system to centered and oriented based on the viewer. This coordinate system is typically called camera, eye or view space. From there the projection
matrix may apply perspective projection and scales data to move to "clip" space. This space is a normalized space where everything outside a -1 .. 1 box is clipped. The final projection to pixel or screen space is handled implicitly by the Graphics API or explicitly in CairoMakie based on viewport(scene)
. The camera(scene)
also holds onto a pixel_space
matrix, which transforms pixel space to clip space, the scenes resolution as well as some auxiliary information about the cameras orientation. Note that the camera controller is often referred to as the "camera" because the camera(scene)
is just inactive storage.
Space Attribute
The space
attribute controls which projection matrices the plot uses. The options refer to the input space, which generally transforms to clip space. The options include:
space = :data
: Apply the camerasview
andprojection
matrices. (This is usually called world space in Graphics APIs.)space = :pixel
: Apply the camerapixel_space
matrix.space = :clip
: Apply an identity matrix.space = :relative
: Apply a constant translation-scale matrix.
Note that all of these act after transformations, i.e. after the model
matrix is applied.
Marker Space Attribute
A few plots include a markerspace
attribute. For these, the projections above are split into two steps, going from space
to markerspace
to clip space. The same options as above apply. If needed, some of these matrices may also be inverted (e.g. going from :pixel -> :data -> :clip space).
Float32Convert
The Float32Convert is an optional step that doesn't fit as cleanly into the pipeline. It's job is to make sure that the data, projection matrices and model matrix are save to convert to Float32 types and thus can be passed to the graphics API without precision issues.
Currently only Axis
actually defines this transformation. When calling plot!(axis, ...)
the data_limits()
of the plot are recorded by the Axis and combined with existing limits. The limits eventually trigger a camera update, where the transform_func
gets applied. The result gets passed to the Float32Convert
, which updates its linear transformation to make the transformed limits Float32-safe. The projection matrices are then derived from the safe limits. At this point the linear transformation of the Float32Convert exists just before view
. If possible, it is permuted with model
so that the model matrix can processed by the graphics API, i.e. on the GPU.