With different x and y scales, how do I retain circular shapes for markers?

#import "@preview/cetz:0.4.0"
#cetz.canvas(length: 1cm, {
  import cetz.draw: *

  scale(x: 3, y: 1)

  set-style(stroke: (cap: "round", thickness: 3pt), padding: 10pt)

  line((-5, 0), (5, 0), name: "xaxis", mark: (end: "triangle"), stroke: (paint: gray))
  content((), $ Re(z) $, anchor: "west")

  line((0, -12), (0, 12), name: "yaxis", mark: (end: "triangle"), stroke: (paint: gray))
  content((), $ Im(z) $, anchor: "south")

  content((0, 0), $ O $, anchor: "north-east")

  line((), (120deg, 3), stroke: (paint: orange))
  mark((120deg, 3), (0, 0), anchor: "center", symbol: "o", stroke: orange, fill: orange)
  content((120deg, 3), anchor: "south-west", padding: 10pt)[#text(fill: orange)[$ xi = 3 e ^(i (2 pi)/3) = r e^(i theta) $]]
})

This is my MWE. I have different x and y scales because of the way the data are spread. I want a circle rather than an ellipse at the end of the line.

How do I get an undistorted version of whatever marker or shape I use, given my unequal scales?

The line thickness doesn’t match the code snippet.

Since marks can only scale both axis, you’d have to use a primitive shape directly:

#import "@preview/cetz:0.4.0"
#cetz.canvas(length: 1cm, {
  import cetz.draw: *

  scale(x: 3, y: 1)

  let get-arg(name, args) = {
    let arg = args.named().at(name, default: none)
    if arg == none { (:) } else { ((name): arg) }
  }
  let unscaled-mark(from, to, x: 3, y: 1, symbol: "o", ..args) = {
    assert(symbol == "o")
    let anchor = get-arg("anchor", args)
    let stroke = get-arg("stroke", args)
    let fill = get-arg("fill", args)
    if fill == (:) { fill = (fill: black) }
    let radius = (1pt * y, 1pt * x)
    line(from, to, ..stroke)
    circle(from, stroke: none, ..fill, ..anchor, radius: radius)
  }

  set-style(stroke: (cap: "round", thickness: 3pt), padding: 10pt)

  line(
    (-5, 0),
    (5, 0),
    name: "xaxis",
    mark: (end: "triangle"),
    stroke: (paint: gray),
  )
  content((), $ Re(z) $, anchor: "west")

  line(
    (0, -12),
    (0, 12),
    name: "yaxis",
    mark: (end: "triangle"),
    stroke: (paint: gray),
  )
  content((), $ Im(z) $, anchor: "south")

  content((0, 0), $ O $, anchor: "north-east")

  line((), (120deg, 3), stroke: (paint: orange))
  unscaled-mark(
    (120deg, 3),
    (0, 0),
    anchor: "center",
    symbol: "o",
    stroke: orange,
    fill: orange,
  )
  // mark(
  //   (120deg, 3),
  //   (0, 0),
  //   anchor: "center",
  //   symbol: "o",
  //   stroke: orange,
  //   fill: orange,
  // )
  content(
    (120deg, 3),
    anchor: "south-west",
    padding: 10pt,
    text(orange)[$ xi = 3 e ^(i (2 pi)/3) = r e^(i theta) $],
  )
})

The PDF was zoomed for a screenshot to be taken.

I was looking for a solution more ready-to-use.

Some answers online have stated that there are some exceptions to scaling that do not require a workaround like above. I do not know if these are glyphs from the symbol set? Because of compilation errors, I have been able to test them to see whether they work or can be made to work.

What does this mean? Supporting all symbols?

Here is a smarter one:

#import "@preview/cetz:0.4.0"

#let get-arg(name, args) = {
  let arg = args.named().at(name, default: none)
  if arg == none { (:) } else { ((name): arg) }
}

#let fix-mark(from, to, symbol: "o", scale: 1, ..args) = {
  import cetz.draw: get-ctx, line, circle
  assert(symbol == "o", message: "Only \"o\" symbol is supported")
  let anchor = get-arg("anchor", args)
  let stroke = get-arg("stroke", args)
  let fill = get-arg("fill", args)
  if fill == (:) { fill = (fill: black) }
  get-ctx(ctx => {
    let ((x-scale, ..), (_, y-scale, ..), ..) = ctx.transform
    y-scale *= -1
    let radius = (1pt * y-scale * scale, 1pt * x-scale * scale)
    line(from, to, ..stroke)
    circle(from, stroke: none, ..fill, ..anchor, radius: radius)
  })
}

#cetz.canvas({
  import cetz.draw: *
  let mark = fix-mark
})

Saying that answers exist and not saying what they are won’t help answer the question.

You can use mark: (end: "o", fill: orange, anchor: "center") on your orange line:

#import "@preview/cetz:0.4.0"
#cetz.canvas(length: 1cm, {
  import cetz.draw: *

  scale(x: 3, y: 1)

  set-style(stroke: (cap: "round", thickness: 3pt), padding: 10pt)

  line((-5, 0), (5, 0), name: "xaxis", mark: (end: "triangle"), stroke: (paint: gray))
  content((), $ Re(z) $, anchor: "west")

  line((0, -12), (0, 12), name: "yaxis", mark: (end: "triangle"), stroke: (paint: gray))
  content((), $ Im(z) $, anchor: "south")

  content((0, 0), $ O $, anchor: "north-east")

  line((), (120deg, 3), stroke: (paint: orange), mark: (end: "o", fill: orange, anchor: "center"))
  content((120deg, 3), anchor: "south-west", padding: 10pt)[#text(fill: orange)[$ xi = 3 e ^(i (2 pi)/3) = r e^(i theta) $]]
})

(It seems the transform-shape does not work for mark elements, which is a bug.)

2 Likes

I am sorry I did not keep those scripts that did not work nor the links that led to them.

The one that did work had two words:
mark and symbol as in my MWE but the symbol was nevertheless scaled. I tried to look for mark but did not find it. I now think it is used at the end of a line like an arrowhead.

Thank you for the solution. What exactly do you mean by the line quoted above? Will this solution work in future?

I think it (your solution) should work. The mark element seems to ignore the transform-shape style attribute of marks. But I have to look into it.

1 Like