# Makie: one (nice?) way to control the units of axes

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.

5 Likes

There is some work ongoing to give Axis actual awareness of special types being used on its x and y axes. Like units, categorical values, timestamps. This will also hook into tick formatting, but go a step further, so that plotting incompatible data doesnâ€™t work. But I donâ€™t have a timeline, we want to get the 0.20 release done first.

1 Like

Thanks for sharing this. Just a nitpick about semantics.

I usually think of a unit as something with dimensions, e.g. m, g. In my opinion your code does scaling to deal with prefixes and such. For example, 1e-2 = centi and 1e-6 = Âµ, without caring about the base unit of m. And also works for things not related to prefixes, such as one might take an axis in radians and plot in multiples of pi.

So I would recommend calling the adjustment `AxisScale` or some such. Nevertheless, thanks for figuring this out and showing you managed to accomplish this.

1 Like

@artkuo, glad to know that you like my finding. And thanks for the suggestion! I agree with you. If I end up creating a PR, I will name the type appropriately.

Iâ€™ve never used Makie, but you could look into how Unitful.jl and Plots.jl interact (formerly UnitfulRecipes.jl).

So this PR should achieve that automatically, without specifying a x/y unit.
For units with the same base unit like time, space etc, it also supports mixing them in the same axis.
It should offer pretty complete Unitful support once merged.

3 Likes

@gustaphe and @sdanisch, the concept of tagging units to data and showing them in the units of axes is the most correct procedure in principle. However, I find that such a procedure is sometimes too convoluted. If data are already tagged with Unitful units, thatâ€™s a different story, but in many cases, data are produced as plain numbers, even though they are produced in some assumed units (e.g., solutions of differential equations solvers). Plotting such numeric data directly, without having to use Unitful, feels more straightforward to me. Having to learn how to use Unitful in order to perform axis scaling feels like too much for me, because plotting is often just a means, not an end goal: I want to generate plots by the simplest method possible and move on to the next step.

Having to use Unitful for axis scaling could be also too restrictive sometimes. There are many cases where using Unitful could lead to weird situations; for example, in some cases I want to plot two datasets that are in different physical dimensions (e.g., one in length and one in mass) on the same axes, to inspect the correlation between the two.

I was initially supportive about the idea of attaching units to data for axis scaling, but for these reasons I donâ€™t like the idea anymore. It is definitely a nice feature to have, but it shouldnâ€™t be the only way to achieve axis scaling.

1 Like

That is a typical case where you not being able to do what you want is a good thing.

1 Like

This is quite a strong statement to make in general.

Do you think itâ€™s completely unreasonable to inspect the correlation of to different quantities, that are conceptually equivalent, e.g. kinetic energy and temperature but obviously cannot carry the same unit?

Sure, for any kind of publication you would have to homogenize it in a way, (e.g. by multiplying temperature with boltzmanns constant) but for a quick consistency check I think itâ€™s not outright wrong to skip this step and still plot them on the same axis with appropriate scaling.

Just my 2câ€¦

2 Likes

Then you plot them against each other (looking for a straight line), or their ratio (looking for a constant). Visually identifying correlation is difficult, youâ€™re likely to get both false positives and false negatives.

If youâ€™re looking for things like â€śhow long after the spike in pressure does the temperature start rising?â€ť, my preferred way is `plot(plot(t, p), plot(t, T); layout=(2,1))`. No unit confusion, no need to rescale manually.