Can cells in a table be offset?

I’ve been having some trouble with this LaTeX timetable example, so I thought I’d reimplement it in Typst. The obvious way to draw a timetable is with a table, but the LaTeX example uses TikZ, probably so you can offset some classes to start on the half or quarter hour, as some of mine need to.

Can I do this in Typst by abusing table or grid syntax, or do I need to learn CeTZ?

1 Like

Table cells can’t natively be offset, but you sure can abuse table cells to get the desired effect: to imitate an offset cell, add a cell without a stroke that contains an offset box with a stroke:

#let cell = table.cell.with(stroke: 1pt)
#table(
  columns: 4,
  rows: 1cm,
  align: horizon,
  inset: 5pt,
  [a],
  table.cell(inset: 0pt, stroke: none,
    move(dy: -5mm, box(stroke: 1pt, height: 1cm, inset: 5pt)[hello]),
  ),
  [b],
  [c], [d],
  table.cell(stroke: none, none), // black cell with no outline
  [e]
)

3 Likes

Using the vertical spacing v function and grid-in-grid might be easier.

#set page(height: auto, width: auto)

#let hour = 1.5em
#grid(
  columns: (5em,) + (6em,) * 5,
  align: center,
  [],
  ..([Lundi], [Mardi], [Mercredi], [Jeudi], [Vendredi]).map(grid.cell.with(
    stroke: 0.5pt,
    inset: (x: 1em, y: 0.5em),
    fill: rgb("ffffcc"),
  )),
  // Hours
  grid(
    ..range(8, 19).map(n => block(
      fill: rgb("ffffb3"),
      stroke: 0.5pt,
      width: 100%,
      height: hour,
      align(horizon)[#(n)--#(n + 1)],
    ))
  ),
  // Skip easy columns
  [],
  [],
  [],
  // Here's the hard column
  grid(
    block(
      fill: rgb("ffcccc"),
      stroke: 0.5pt,
      width: 100%,
      height: 2 * hour,
      align(center + horizon)[Physique],
    ),
    block(
      fill: rgb("ccccff"),
      stroke: 0.5pt,
      width: 100%,
      height: 2 * hour,
      align(center + horizon)[Maths],
    ),
    v(1.5 * hour), // 👈 Here's an arbitrary vertical spacing
    block(
      fill: white,
      stroke: 0.5pt,
      width: 100%,
      height: 1 * hour,
      align(center + horizon)[Planche],
    ),
  ),
)
1 Like

You could work with a subdivision and use rowspan to group hours into multiple cells. Here’s a start on that. The decimal type makes it easy to do correctly with no worries, subdivision can be 15, 20, 30 mins etc.

With that, you’re halfway to a timetable package…

// implement time using `decimal` type; "830" is 8:30 in this input.
#let classes = (
  (name: "Physics", st: "800", end: "1000"),
  (name: "English", st: "1000", end: "1130"),
  (name: "Planche", st: "1330", end: "1430"),
)
#let subdiv = "30"
#let startday = "700"

#let totime(time) = {
  let dec = decimal(time)
  let h = calc.div-euclid(dec, 100)
  let m = calc.rem-euclid(dec, 100) / 60
  assert(m < decimal(1))
  h + m
}
#let subdiv-t = totime(subdiv)
#let subdivparts = 1/subdiv-t

#let make-class(day, class) = {
  let y = (totime(class.st) - totime(startday)) * subdivparts
  let duration = (totime(class.end) - totime(class.st))
  let length = duration/subdiv-t
  let length-rem = calc.rem(duration, subdiv-t)
  if length-rem != decimal("0") {
    panic("Class not divisible by subdivision: " + repr(class))
  }
  table.cell(x: day, y: int(y), [#class.name], rowspan: int(length), stroke: 1pt)
}

#table(
  columns: 8,
  stroke: 0.1pt,
  ..range(10).map(x => x + totime(startday)).map(str).enumerate().map(((i, elt)) => table.cell(x: 0, y: i * int(subdivparts), elt, rowspan: int(subdivparts), stroke: 1pt)),
  ..classes.map(make-class.with(1))
)

3 Likes

I think this is going to be the easiest one to work with. All that code can be hidden away in a template file, and the actual data would look fairly clean. In the meantime, I managed to get the LaTeX one to work, so I don’t need this for now, but I’ll probably use it next time.

All the ideas are interesting and potentially useful. Thanks everyone.

1 Like