Suppose we have (x,y) data following y = f(x), x is measured in centimeters, and y is measured in micrometers. When plotting the (x,y) data, we would want to show the x-axis in centimeters and the y-axis in micrometers, i.e., show the tick labels generated in appropriate units without large exponents of base 10.
In Makie (or any plotting package in general), this is achieved by dividing the data by the units, like
xs = ... # x data
ys = ... # y data
xunit = 1e-2 # preferred unit of x data (centimeter)
yunit = 1e-6 # preferred unit of y data (micrometer)
fig = Figure()
fig[1,1] = Axis(fig, ...)
lines!(xs ./ xunit, ys ./ yunit, ...) # divide data by units
This works, but I don’t like the fact that the data passed to lines!()
change from xs
to xs ./ xunit
. When we plot data in different units, we want the way the data are displayed changed, not the data themselves. So, I feel that changing the units should be achieved by changing the properties of axes, rather than the data themselves. In other words, I would like to do something like
xs = ...
ys = ...
fig = Figure()
fig[1,1] = Axis(fig, xunit=1e-2, yunit=1e-6, ...) # units are set as properties of Axis
lines!(xs, ys, ...) # data remain unchanged even if units change
Another advantage of this way of controlling the axis units is that we can control the units of all plotted datasets in one place. For example, suppose we have many datasets (xs1, ys1)
, (xs2, ys2)
, (xs3, ys3)
, … Then the code would look like
lines!(xs1, ys1, ...)
lines!(xs2, ys2, ...)
lines!(xs2, ys2, ...)
rather than the more convoluted
lines!(xs1 ./ xunit, ys1 ./yunit, ...)
lines!(xs2 ./ xunit, ys2 ./yunit, ...)
lines!(xs3 ./ xunit, ys3 ./yunit, ...)
Currently we don’t have properties like xunit
and yunit
in Makie.Axis
, so I have been looking for a simple workaround for quite a while. Recently, I noticed that Makie’s docstrings have been updated with information about customization:
help?> Axis.xtickformat
[...]
Usually, the tick values are determined first using Makie.get_tickvalues, after which
Makie.get_ticklabels(xtickformat, xtickvalues) is called. If there is a special method
defined, tick values and labels can be determined together using Makie.get_ticks instead.
[...]
Using the information as a clue, I experimented with a few possibilities, and here is the one I like. Define a type that contains the axis unit:
struct AxisUnit
unit
end
Then, define Makie.get_ticks
mentioned in the docstrings for this type:
function Makie.get_ticks(ticks, scale, au::AxisUnit, vmin, vmax)
tickvalues, ticklabels = Makie.get_ticks(ticks, scale, Makie.automatic, vmin / au.unit, vmax / au.unit)
tickvalues .*= au.unit
return tickvalues, ticklabels
end
This function delegates the arguments to the default Makie.get_ticks
, but normalizes the axis bounds vmin
and vmax
with respect to au.unit
before doing so in order to generate correct tick values and labels in the unit. However, because the data to plot are not normalized, the function unnormalizes the tick values before returning.
With these two simple additions, we can control the axis units as follows:
fig = Figure()
fig[1,1] = Axis(fig, xtickformat=AxisUnit(1e-2), ytickformat=AxisUnit(1e-6), ...)
xs = ...
ys = ...
lines!(xs, ys, ...)
Here is an actual working example
using CairoMakie
# Custom code for axis unit control
struct AxisUnit
unit
end
function Makie.get_ticks(ticks, scale, au::AxisUnit, vmin, vmax)
tickvalues, ticklabels = Makie.get_ticks(ticks, scale, Makie.automatic, vmin / au.unit, vmax / au.unit)
tickvalues .*= au.unit
return tickvalues, ticklabels
end
# Two x-datasets between 0 and 1 cm
xs1 = sort(rand(100)) .* 1e-2
xs2 = sort(rand(200)) .* 1e-2
# Two y-datasets between -1 and 1 µm
ys1 = cos.(Ď€ .* xs1 / 1e-2) .* 1e-6
ys2 = sin.(Ď€ .* xs2 / 1e-2) .* 1e-6
# Plots
fig = Figure(fontsize=24)
fig[1,1] = Axis(fig, xtickformat=AxisUnit(1e-2), ytickformat=AxisUnit(1e-6), xlabel="cm", ylabel="µm")
lines!(xs1, ys1, color=:red, linewidth=3, label="cosine")
lines!(xs2, ys2, color=:blue, linewidth=3, label="sine")
axislegend()
display(fig)
Here is the result:
Hope people find this useful. If many people like this feature, I may want to create a pull request.