How to rotate or reposition axis labels in lilaq?

Hi everyone,

I’m currently experimenting with switching my plots from cetz-plot to lilaq, and I have to say—I’m already quite impressed! The design flexibility is great, and I’ve managed to recreate most of my custom layout using the documentation and examples.

One thing I’m still trying to figure out, though, is how to rotate and fine-tune the position of the axis labels (xlabel and ylabel). I’d like to rotate them slightly and adjust their placement to better match my preferred layout style (which I used in cetz-plot before).

Here’s a minimal working example of what I have so far:

#import "@preview/lilaq:0.1.0" as lq
#import "@preview/zero:0.3.3": * 
#import "@preview/tiptoe:0.3.0"

#let schoolbook-style = it => {
  let filter(value, distance) = value != 0 and distance >= 5pt
  let axis-args = (position: 0, filter: filter)
  
  show: lq.set-tick(inset: 2pt, outset: 2pt, pad: 0.4em)
  show: lq.set-spine(tip: tiptoe.triangle)
  show: lq.set-diagram(xaxis: axis-args, yaxis: axis-args)
  show lq.selector(lq.label): set align(top + right)
  it
}

#set-num(decimal-separator: ",")

#show: schoolbook-style

#let x = lq.linspace(0, 10, num:200) 

#lq.diagram(
  xlabel: $x$, 
  ylabel: $y$,
  lq.plot(
    mark: none,
    stroke: 1.5pt,
    x,
    x.map(x => 
      calc.pow(x,2)  
    )
  ),
)

Any tips on how to manually rotate or reposition the axis labels (xlabel, ylabel)? Is there a clean way to apply a rotate or offset to them in the current lilaq API?

Thanks in advance, and also big kudos to everyone working on these awesome libraries!

CeTZ plot:

Current lilaq plot:

Hi there, have you tried setting “angle:-90deg”?

The Lilaq doc mentions:

lq.label(body, dx=0pt, dy=0pt, pad=0.75em, angle=0deg)

angle : angle default: 0deg

Angle at which the label is drawn. The label of a y axis is often drawn at -90deg.

1 Like

You can pass a label to the parameters xlabel and ylabel to adjust the angle and the position.

#lq.diagram(
  xlabel: lq.xlabel($x$, dx: 0.3cm, dy: -0.7cm, angle: 0deg),
  ylabel: lq.ylabel($y$, dx: 0.9cm, dy: -0.4cm, angle: 0deg),
  lq.plot(
    mark: none,
    stroke: 1.5pt,
    x,
    x.map(x => 
      calc.pow(x,2)  
    )
  ),
)

I’m not sure if there is also a way to do this with a show rule? But since the position probably needs to be customized for every plot anyway, I think it is fine to create the elements here manually.

2 Likes

Thank you very much for your code snippet!
It results in the exact intended layout.

https://typst.app/project/rMJq47dMdXyKz9LuqiLvsQ

Yes, using lq.label is indeed the way to go.

Ideally you would want to write

#show lq.label.where(kind: "y"): set lq.label(angle: 0deg)

but it’s currently not possible due to a limitation of the package elembic which provides the “user-defined types/elements” in Lilaq. It will become possible though, when types are properly introduced in the Typst compiler.

Regarding the preset: you can also add the label (if it always has the same text) to the lq.diagram set rule.

show: lq.set-diagram(
  xaxis: axis-args + (
    label: lq.xlabel($x$, dx: 0.3cm, dy: -0.7cm)
  ), 
  yaxis: axis-args + (
    label: lq.ylabel($y$, dx: 0.9cm, dy: -0.4cm, angle: 0deg)
  ), 
)

This way you can avoid a good deal of repetition.

I will think about how to improve positioning of the labels so that it does not need to be tuned each time the label text changes.

3 Likes

A follow-up question from @Bryn:

Your solution works. I like the fact you are using em instead of cm.

I wanted to reply but could not find a clean solution allowing for the height and width of the diagram and combining label.pad: none.

Something in the lines of:

// This does not work - hypothetical solution
xlabel: lq.xlabel(pad: none, dx: diagram.width + offset)[$x$],
ylabel: lq.ylabel(pad: none, dy: diagram.height + offset, angle: 0deg)[$y$],

And also have a way to center the label with something like align: center like the tick labels.

Perhaps @Mc-Zen has something more elegant?

1 Like

Hi @vmartel08 and @Bryn !

For this purpose I had developed the feature label.pad: none so that the label may ignore the axis ticks and be aligned at the axis spine itself. Unfortunately, I broke label.pad: none in lilaq:0.4.0 but for the next version it will be fixed again. This detail is important because otherwise, there needs to be manual placement because the depth of the tick labels of course varies with the values that the ticks display.

Comparison: without and with pad: none

Now, with that fixed, how should the placement be done? There are two ways that I see.

Using std.place

Since lilaq:0.4.0, it is finally possible to apply set and show rules to x and y axes individually. We can express the diagram width and height with 100% and then we add a little padding of 0.5em to give the label a bit of constant space. Note the alignments used in place: we place the ylabel at the bottom and shift it up by 100% (and a little more). The center part aligns the label in the center. For the xlabel, we need to place it at the left and shift it by 100% to the right. This guarantees that the placement works with a label of any length.

#show: lq.set-label(pad: none)
#show: lq.show_(
  lq.label.with(kind: "y"),
  it => place(bottom + center, dy: -100% - .5em, it)
)
#show: lq.show_(
  lq.label.with(kind: "x"),
  it => place(left + horizon, dx: 100% + .5em, it)
)
#lq.diagram(
  xlabel: $x$,
  ylabel: lq.ylabel($y$, angle: 0deg)
)

This approach has just one downside: due to the usage of std.place the labels are not measured and taken into account when the bounding box of the diagram is computed. This is visualized below:

Using conditional set rules

This second approach has the potential to be cleaner, unfortuanately it does not work currently.

The idea is to use the alignment method described in the posts above and the apply conditional set rules to set the dx and dy value for the xaxis and yaxis separately.

#show: lq.set-label(pad: none)
#show lq.selector(lq.label): set align(bottom + left)
#show: lq.cond-set(lq.label.with(kind: "x"), dx: 100% + 0.5pt)
#show: lq.cond-set(lq.label.with(kind: "y"), dy: -100% - 0.5pt)

This currently does not work due how elembic elements work. The new dx and dy values are internally realized at a later stage than they are read…

This approach could potentially solve the bounding-box issue. I will work on this. The downside of this second approach is that it is more difficult to get the center/horizon alignment along the other direction.

For now I recommend the approach with std.place and if necessary to wrap the diagram in a box with inset: (top: 1em, right: 1em) or something like that. The label.pad: none thing will be fixed in the next release!

3 Likes

Clever! Especially the

#show: lq.show_(...)

which I did not know about.

Not always easy to generate the solution, especially when Elembic is involved. Thanks for all the great work on Lilaq and the detailed explanations @Mc-Zen.

1 Like

You’re welcome!

Yeah, the new show_ feature has been introduced in elembic:1.0.0 and is not widely known about. I’m aware that all this is very messy compared to regular types in Typst but we’ll get there with user-defined types!

1 Like