How to set the columnspan or the rowspan of a specific cell with a show-set rule (when creating table from a CSV file)?

I am trying to create a very large table from a CSV file. It has some cells that need to have column span and some that need row span. The CSV looks something like this:
image

I have copy-pasted the contents of the CSV file below:

,Spanned column,
A,B,C
Spanned row,1,2
,3,4
D,5,6

What I have tried so far:

#let dat = csv("test.csv")

#{
// approach 1
  show table.cell.where(x:1, y:0): set table.cell(colspan: 2)
  show table.cell.where(x:0, y:2): set table.cell(rowspan: 2)
  table(
    columns: 3,
    ..dat.flatten()
  )
}

#{
// approach 2
  show table.cell.where(x: 1, y:0): it => table.cell(colspan: 2)[#it]
  show table.cell.where(x: 0, y:2): it => table.cell(colspan: 2)[#it]
  table(
    columns: 3,
    ..dat.flatten()
  )
}

Neither approach seem to work. Neither table has any column or row span. MWE link: Typst

Any help regarding this would be greatly appreciated. Thank you.

1 Like

From a comment Show-set rules don't seem to work for `table.cell` · Issue #4159 · typst/typst · GitHub :

Unfortunately this just hasn’t been implemented yet, but it will be in the future, for sure.

In the meantime, if your selector only relies on the cell’s position, you can use e.g. #set table(fill: (x, y) => if x == 0 { red } else { blue }).

The described workaround only works for fields that support function type and are available in the table() function: fill, align, stroke, inset.

Since you need to modify the cell itself by changing the colspan/rowspan field, you also won’t be able to use something like this:

#show table.cell: it => {
  if it.x == 1 and it.y == 0 { table.cell(colspan: 2, it) } else { it }
}

This is because you can’t create a new element inside a show rule for that element, i.e., creating a table cell inside a show rule for a table cell. You probably can get away with it if your newly created cell won’t trigger the same branch of the if statement, but in this case this is not possible. Only if you change cell’s content or inset value etc. beforehand.

The second approach probably didn’t work either because you again try to create a new cell inside a show rule for a cell and/or because show rule is applied later (not when you create a table with elements). I’m not entirely sure.

I still think that maybe some hacky workaround is possible, but I need time to figure out if this is true.

Update. Yes my hacky solution works, but I need to make it prettier and more reusable.

Welp, guess I’ll just have to wait till it is implemented then.

Default result
#let dat = csv("test.csv")

#repr(dat)

#set table(align: center + horizon)

#table(columns: (3cm,) * 3, ..dat.flatten())

image


First attempt:

#let dat = csv("test.csv")
#set table(align: center + horizon)
#{
  // show table.cell.where(y: 0, x: 1): set table.cell(colspan: 2)
  // show table.cell.where(y: 2, x: 0): set table.cell(rowspan: 2)
  let colspan-cells = ((0, 1),) // (y, x)
  let rowspan-cells = ((2, 0),) // (y, x)
  let data = dat
  for ((y, x)) in colspan-cells {
    data.at(y) = {
      let row = data.at(y)
      let _ = row.remove(x + 1)
      row
    }
  }
  for ((y, x)) in rowspan-cells {
    data.at(y + 1) = {
      let row = data.at(y + 1)
      let _ = row.remove(x)
      row
    }
  }
  let bonus-coloring = (fill: green.transparentize(70%))
  table(columns: (3cm,) * 3, ..data
  .enumerate()
  .map(((y, row)) => row.enumerate().map(((x, cell)) =>
  if x == 1 and y == 0 {
    table.cell(colspan: 2, cell, ..bonus-coloring)
  } else if x == 0 and y == 2 {
    table.cell(rowspan: 2, cell, ..bonus-coloring)
  } else {
    cell
  }))
  .flatten())
}

image


Second attempt:

#let dat = csv("test.csv")
#set table(align: center + horizon)

/// Removes elements which will be overwritten by a `colspan`/`rowspan`.
/// * `data` a 2D array of future cell content elements
/// * `colspan-cells` an array of tuples `(y, x, length)` describing each colspan cell
/// * `rowspan-cells` an array of tuples `(y, x, length)` describing each rowspan cell
/// Returns a prepared 2D `array`.
///
/// Note that length of each row can differ.
#let prepare-data(data, colspan-cells, rowspan-cells) = {
  for ((y, x, length)) in colspan-cells {
    data.at(y) = {
      let row = data.at(y)
      for i in range(length - 1) {
        assert(
          (x + 1) < row.len(),
          message: "Column span out of bounds: " + repr((y, x, length)),
        )
        let _ = row.remove(x + 1)
      }
      row
    }
  }
  for ((y, x, length)) in rowspan-cells {
    for i in range(length - 1) {
      data.at(y + 1 + i) = {
        assert(
          (y + 1 + i) < data.len(),
          message: "Row span out of bounds: " + repr((y, x, length)),
        )
        let row = data.at(y + 1 + i)
        let _ = row.remove(x)
        row
      }
    }
  }
  data
}

/// Adds `colspan`/`rowspan` field to specified cells.
/// * `cells` a 2D array of cells (content)
/// * `colspan-cells` an array of tuples `(y, x, length)` describing each colspan cell
/// * `rowspan-cells` an array of tuples `(y, x, length)` describing each rowspan cell
/// Returns flatten `array` of patched cells.
///
/// Note that this is not tested for when cell already wrapped in `cell()`.
#let patch-span-cells(cells, colspan-cells, rowspan-cells) = {
  let fill = (fill: green.transparentize(70%))
  for ((y, x, length)) in colspan-cells {
    cells.at(y).at(x) = table.cell(colspan: length, cells.at(y).at(x), ..fill)
  }
  for ((y, x, length)) in rowspan-cells {
    cells.at(y).at(x) = table.cell(rowspan: length, cells.at(y).at(x), ..fill)
  }
  cells.flatten()
}

#{
  // show table.cell.where(y: 0, x: 1): set table.cell(colspan: 2)
  // show table.cell.where(y: 2, x: 0): set table.cell(rowspan: 2)
  let colspan-cells = ((0, 1, 2),) // (y, x, length), min_length = 2 (no-op for "< 2")
  let rowspan-cells = ((2, 0, 2),) // (y, x, length), min_length = 2 (no-op for "< 2")
  let data = prepare-data(dat, colspan-cells, rowspan-cells)
  table(
    columns: (3cm,) * 3,
    ..patch-span-cells(data, colspan-cells, rowspan-cells),
  )
}

image

You can merge or rewrite the 2 helper functions however you see fit. If you don’t need to do anything in between, then you don’t really need two of them. But they can also be easily merged by making a 3rd wrapper function:

#let patch-spans(data, colspan-cells, rowspan-cells) = {
  patch-span-cells(
    prepare-data(dat, colspan-cells, rowspan-cells),
    colspan-cells,
    rowspan-cells,
  )
}

#table(
  // A small bonus (can only use (the initial) unmodified
  // 2D array here).
  columns: dat.at(0).len(),
  ..patch-spans(dat, colspan-cells, rowspan-cells),
)
4 Likes

That works perfectly, thank you very much.

But, now I wonder if this could be extended to support cells with both column span and row span.

1 Like

Most definitely! If you can easily (with a hack) do this for column-/row-span-only cells, then you can do this for column-and-rowspan cells.

The algorithm does 2 basic steps:

  1. delete all elements from a matrix that shouldn’t be visible in the result table (due to a span);
  2. add a table.cell() wrapper to all elements in the matrix that are span cells’ content.

Based on that, for a “block-span” cell you need to delete a block of elements from the matrix first (excluding the top-left element, which is the span-cell) — needs 2 nested for-loops (instead of 2 separate ones), and then add a table.cell() wrapper with colspan and rowspan fields set to the desired size.

1 Like

I have come up with a “arguably simpler” method (depending on whom you ask).

It relies on the CSV itself having flags for which cells should not be visible. However, this is not a problem for my current use case because the data is generated from R. So, I can just put something like “” in the cells that will be spanned over by other cells in R before writing the CSV to disk.

For example, suppose that the CSV looks as follows:

image

You can copy from below (I have named it new.csv in the code that follows):

,Spans column,<spanned>
A,B,C
Spans row,1,2
<spanned>,3,4
D,Spans rect,<spanned>
E,<spanned>,<spanned>
F,5,6

The Typst code I have written to make the table is:

#let ndat = csv("new.csv")

#let cellinfo = (
  (0, 1, 1, 2, red.transparentize(70%)),    // (row, col, rowspan, colspan, cell color)
  (2, 0, 2, 1, green.transparentize(70%)),
  (4, 1, 2, 2, blue.transparentize(70%))
)

#let prepdat(csvdat, cellinfo) = {
  for (r, c, rs, cs, clr) in cellinfo{
    csvdat.at(r).at(c) = table.cell(csvdat.at(r).at(c), colspan: cs, rowspan: rs, fill: clr)
  }
  
  let filter_func(item) = {item != "<spanned>"}
  
  let csvflat = csvdat.flatten()
  csvflat = csvflat.filter(filter_func)
  return csvflat
}

#table(
  columns: 3, 
  ..prepdat(ndat, cellinfo)
)

The minimum working example can be found here: Typst

I would like to thank @Andrew for his original solution which inspired me to write my own solution

1 Like

If the CSV file indeed can be altered, then this is indeed a better solution size-wise (and feature-wise). I see you’ve added block-spans, nice. One downside to this will be instances where you have big spans, which means you have to mark a lot of elements/cells in the CSV instead of just providing a position and a span length of a single spanned element. And also a very small potential problem is if you can have virtually any string in the CSV cell, including the spanned marker. But I don’t think this is the case here.

You should share the output too (you can put it in a “details” block if you want).

I think the main issue with the my solution would be performance if the table is really big or if there are many such tables, since we have to filter out elements of an array (which is quite an expensive task, I assume).

I tried to post the screenshot of the output. But, it doesn’t let me post multiple screenshots because I’m new to the forum.

Right, I forgot about that. Yeah, filtering will be more expensive. I think it will go from O(s) to O(r*c) where s — number of spans, r — number of rows, c — number of columns.

Oh, I see. If you want to lift the current limits, you can check out New user limits are annoyingly restrictive - #2 by quachpas.