Transformation Interface
Transformations are one of many steps that transform the data of a plot before drawing. They slot in after "conversions" as noted in Conversion, Transformation and Projection Pipeline.
Every scene and every plot contains a Transformation object which consists of a transform_func and a model matrix. The former is, more or less, an arbitrary function that acts on positional data. The latter is a matrix which encodes scaling, rotation and translation in 3D space. It can be set via translate!(), scale!(), rotate!() and origin!().
This page will discuss transformations from a developer perspective, i.e. how they work and how they can be extended. For a user perspective, see the Transformation reference docs.
Inheritance
By default Transformation objects are inherited from scene/plot to scene/plot.
using Makie
double(x) = 2 * x
t = Transformation(double, translation = Vec3f(1,2,3))
scene = Scene(transformation = t);
child = Scene(scene);
display(scene.transformation)
child.transformationTransformation()
parent = Transformation(…)
translation = [0.0, 0.0, 0.0]
scale = [1.0, 1.0, 1.0]
rotation = 1.0 + 0.0im + 0.0jm + 0.0km
origin = [0.0, 0.0, 0.0]
model = [1.0 0.0 0.0 1.0; 0.0 1.0 0.0 2.0; 0.0 0.0 1.0 3.0; 0.0 0.0 0.0 1.0]
transform_func = doubleAs you can see the Transformation objects are unique, but both contain the same transform_func and model matrix. The transform_func is set by on(tf -> child.transform_func[] = tf, parent.transform_func). It will update whenever the parent transformation function is updated.
The model matrix works a bit differently. Each transformation contains a parent_model field which gets updated to the parent transformations model matrix like transform_func. That then gets combined with the local model matrix derived from translation, scale, rotation and origin as parent_model * local_model to set transformation.model. This way each level of transformations can add its own translation, scale, etc.
scale!(child, 1, 2, 2)
translate!(child, 0, 0, -3)
child.transformationTransformation()
parent = Transformation(…)
translation = [0.0, 0.0, -3.0]
scale = [1.0, 2.0, 2.0]
rotation = 1.0 + 0.0im + 0.0jm + 0.0km
origin = [0.0, 0.0, 0.0]
model = [1.0 0.0 0.0 1.0; 0.0 2.0 0.0 2.0; 0.0 0.0 2.0 0.0; 0.0 0.0 0.0 1.0]
transform_func = doubleApplication
Transformations are typically applied in primitive plots. The transform_func applies first, the model matrix second.
If multiple levels of transformations exist, the transform_func and model matrix of the lowest level transformation, i.e. that of the specific plot, applies. With how inheritance works this means that the local model matrices of the whole transformation tree apply, starting from the lowest level and working up. E.g. in the example above, the (1, 2, 2) scaling and (0, 0, -3) translation apply before the (1, 2, 3) translation.
The model matrix itself is build from a translation, scale, rotation and origin. The origin applies first, effectively setting the origin for the rotation and scaling. Then rotation applies as set by a Makie.Quaternion. Next scale applies to each dimension after the rotation. Finally, translation applies to translate positions again.
Transform Function Interface
How the transform_func is applied is controlled by a function called Makie.apply_transform(transform_func, data). The data is typically an array of Points as generated by the conversion pipeline. If no specialized methods are available, the transform function will be applied per dimension of each point in data by recursively hitting
function apply_transform(transform_func, data::AbstractArray)
return map(point -> apply_transform(transform_func, point), data)
endSo the double function we defined above would be called with float values. If we wanted to define a transform function that acts differently in the x and y (and z) dimension we have 3 options:
using CairoMakie
# Reference plot (black empty circles)
r = Rect2f(1, 0, 1, 2)
f,a,p = scatter(r, color = :white, strokecolor = :black, strokewidth = 2, markersize = 20)
# Option 1:
# Pass multiple functions as a tuple, with each acting in a different dimension
t1 = Transformation((x -> 2x, y -> 0.5y))
scatter!(r, color = :red, markersize = 20, marker = '+', transformation = t1)
# Option 2:
# Wrap a function accepting points in `Makie.PointTrans`
t2 = Transformation(Makie.PointTrans{2}(xy -> Point(xy[2], xy[1])))
scatter!(r, color = :blue, markersize = 20, marker = 'x', transformation = t2)
# Option 3:
# Add a `apply_transform` method for a specific transform function.
# Boundingboxes will likely require a 3D version of the transform function
mytransform(p::VecTypes{2}) = Point(p[1] + 0.5, 0.5 * p[2])
mytransform(p::VecTypes{3}) = Point(p[1] + 0.5, 0.5 * p[2], p[3])
Makie.apply_transform(f::typeof(mytransform), p::VecTypes) = f(p)
t3 = Transformation(mytransform)
scatter!(r, color = :green, markersize = 20, marker = :rect, transformation = t3)
f
Beyond this there are a few more methods that may be worth implementing when adding a transformation function to the interface:
# Defines the inverse transformation function for a given function.
# Used for example in Axis limits
half(x) = 0.5 * x
Makie.inverse_transform(::typeof(double)) = half
# Defines how bounding boxes are transformed. By default the transformed corners
# are used to build a new bounding box.
Makie.apply_transform(f::typeof(double), r::Rect3) = Rect3(f(origin(r)), f(widths(r)))
# Defines the (1D) limits of an empty Axis using the transform function as the
# xscale or yscale.
Makie.defaultlimits(::typeof(double)) = (0.0, 10.0)
# Defines the range of values that are valid for a given transform function in
# an Axis.
Makie.defined_interval(::typeof(double)) = Makie.OpenInterval(-Inf, Inf)
# Defines how a direction vector at specific position gets transformed. By default
# this transforms two points separated by `delta * direction` to calculate a new
# post-transform direction. The result is normalized.
# Used in `streamplot` and `contour` labels to rotate arrow markers and text
function Makie.apply_transform_to_direction(f::typeof(double), position::VecTypes, direction::VecTypes, delta)
return direction
endAs an alternative to inverse_transform, defaultlimits and defined_interval a transform function can also be wrapped in a ReversibleScale. This allows bundling the inverse, limits and interval with the transform function.
Makie.ReversibleScale Type
ReversibleScaleCustom scale struct, taking a forward and inverse arbitrary scale function.
Fields
forward::Function: forward transformation (e.g.log10)inverse::Function: inverse transformation (e.g.exp10forlog10such that inverse ∘ forward ≡ identity)limits::Tuple{Float32, Float32}: default limits (optional, defaults to(0, 10))interval::IntervalSets.AbstractInterval: valid limits interval (optional, defaults to(-Inf32, Inf32))name::Symbol