How can a table be split horizontally?

I have a table with 2 columns but a lot of rows. This leads it to go over to the next page.

Is there a way to split the table horizontally into 4 columns (preferably with header repetition)?

An example of what I’m thinking of:

#align(center,
  table(
    align: center + horizon,
    columns: (10%, 15%),
    table.header("Left", "Right"),
    [l1], [r1],
    [l2], [r2],
    // And so on...
    [l50], [r50]
  )
)

Hopefully achieving the same effect, automatically, as below:

#align(center,
  table(
    align: center + horizon,
    columns: (10%, 15%, 10%, 15%),
    table.header("Left", "Right", "Left", "Right"),
    [l1], [r1], [l26], [r26],
    [l2], [r2], [l27], [r27],
    // And so on...
    [l25], [r25], [l50], [r50]
  )
)

Thanks!

Wrapping a columns(2) might helps.

#set page(height: 10em, width: 20em)


#columns(2, align(center,
  table(
    align: center + horizon,
    columns: (2fr, 3fr),
    table.header("Left", "Right", repeat: true),
    [l1], [r1],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l2], [r2],
    [l50], [r50]
  )
))
1 Like

Thank you! This works to a degree, but not in the way I want it to. >.< When I’m including the table, I want it to be evenly split (25 rows each column if there are 50). When I set gutter as 0, it’s also impossible to center the whole table at the center unless its width is 100%.

#columns(
  2,
  gutter: 0pt,
  align(
    center,
    table(
      align: center + horizon,
      columns: (25%, 25%),
      table.header("Left", "Right"),
      [l1], [r1],
      [l2], [r2],
      [l3], [r3],
      [l4], [r4],
      [l5], [r5],
      [l6], [r6],
      [l7], [r7],
      [l8], [r8],
      [l9], [r9],
      [l10], [r10],
      [l11], [r11],
      [l12], [r12],
      [l13], [r13],
      [l14], [r14],
      [l15], [r15],
      [l16], [r16],
      [l17], [r17],
      [l18], [r18],
      [l19], [r19],
      [l20], [r20],
      [l21], [r21],
      [l22], [r22],
      [l23], [r23],
      [l24], [r24],
      [l25], [r25],
      [l26], [r26],
      [l27], [r27],
      [l28], [r28],
      [l29], [r29],
      [l30], [r30],
      [l31], [r31],
      [l32], [r32],
      [l33], [r33],
      [l34], [r34],
      [l35], [r35],
      [l36], [r36],
      [l37], [r37],
      [l38], [r38],
      [l39], [r39],
      [l40], [r40]
    )
  )
)

gives

Once again, thank you for your help!

1 Like

There is no automatic way to balance columns currently, but you can force the table to a specific height by wrapping the whole columns call into a block with a fixed height (and width of 100%). You may need to play around with the value to achieve the result you are looking for.

1 Like

I see… thank you. :D

I’ll try to do that and see if that’s faster or if copy-pasting is.

To get back to your original approach, I cooked up something:

#let repeated-table(num-repeats: 2, ..args) = {
  let options = args.named()
  let data = args.pos()

  // STEP 1: transform table options to apply to a multiple of the original columns

  let columns = options.at("columns", default: ())
  let (column-count, columns) = if type(columns) == int {
    // for numbers, that's number of columns
    (columns, columns * num-repeats)
  } else if type(columns) == array and columns != () {
    // for arrays with elements, the number of elements is number of columns
    (columns.len(), columns * num-repeats)
  } else {
    // lengths, auto or an empty array mean there's one column
    (1, (auto,) * num-repeats)
  }
  options.columns = columns

  // TODO transform other per-column fields, such as align

  // STEP 2: separate header and footer from the table body, with repeated cells

  let header = if data.len() > 0 and type(data.first()) == content and data.first().func() == table.header {
    let (children, ..args) = data.remove(0).fields()
    table.header(..args, ..children * num-repeats)
  }

  let footer = if data.len() > 0 and type(data.last()) == content and data.last().func() == table.footer {
    let (children, ..args) = data.pop().fields()
    table.footer(..args, ..children * num-repeats)
  }

  // STEP 3: rearrange the data, so that after a number of rows the next repetition begins

  // split into rows
  let rows = data.chunks(column-count)
  // split into repeats of rows
  let num-rows = calc.ceil(rows.len() / num-repeats)
  let repeats = rows.chunks(num-rows)
  // pad the last repeat so that all have the same number of rows
  let empty-row = (none,) * column-count
  repeats.last() += (empty-row,) * (num-rows - repeats.last().len())
  // join repeats into combined rows
  let rows = array.zip(..repeats)
  // combine into flat data
  data = rows.flatten()

  // STEP 4: re-add header and footer to the data

  if header != none {
    data.insert(0, header)
  }
  if footer != none {
    data.push(footer)
  }

  // STEP 5: produce table
  
  table(..options, ..data)
}

#align(center,
  repeated-table(
    num-repeats: 3,
    align: center + horizon,
    columns: (10%, 15%),
    table.header("Left", "Right", repeat: true),
    ..array.range(1, 29).map(n => ([l#n], [r#n])).flatten(),
    table.footer("Left", "Right"),
  )
)

It’s inferior to eventually having balanced columns, but it works right now ;) there is a big TODO in the code; if you use per-column align, you still need to fix that, but I think the principle is clear from how columns is transformed for this to work, and the hard part of transforming the data is done.

For nicer styling, I would recommend looking at double strokes in the table guide; by using the correct column gutters, you can separate the “repeats” of the table and achieve nice double strokes at the same time.

3 Likes

That is amazing, thank you very much!