3

I have a Vue app calculating a table with rowspans. The algorithm calculates the data ( and rowspans ) based on a configuration file so the application only renders the columns ( and the column order ) based on the calculated result.

Given the following sample ( Reproduction link )

<template>
  <table>
    <thead>
      <th>City</th>
      <th>Inhabitant</th>
      <th>House</th>
      <th>Room</th>
    </thead>
    <tbody>
      <tr v-for="(row, rowIndex) in tableMatrix" :key="rowIndex">
        <template v-for="(cell, columnIndex) in row" :key="columnIndex">
          <td v-if="cell.isCoveredByPreviousCell" style="display: none" />
          <td v-else :rowspan="cell.rowspan ?? 1">
            <template v-if="cell.content">
              {{ cell.content }}
            </template>
          </td>
        </template>
      </tr>
    </tbody>
  </table>
</template>

<script setup lang="ts">
import { ref, Ref } from 'vue';

interface Cell { isCoveredByPreviousCell: boolean; rowspan: number; content?: string; }

type TableMatrix = Cell[][];

const tableMatrix: Ref<TableMatrix> = ref([
  [
    { isCoveredByPreviousCell: false, rowspan: 5, content: "City 1" },
    { isCoveredByPreviousCell: false, rowspan: 4, content: "Inhabitant 1" },
    { isCoveredByPreviousCell: false, rowspan: 3, content: "House 1" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Room 2" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Room 3" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "House 2" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Inhabitant 2" },
    { isCoveredByPreviousCell: false, content: "House 3" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ]
])
</script>

<style>
table, th, td { border-collapse: collapse; border: 1px solid black; }
</style>

I get the following ( correct ) output

enter image description here

It's quite hard to identify a single "line", I'm looking for a way to make this more clear. A zebra striped table won't work for the table design. Maybe I need to add "dividing" rows with a fixed height and a different background color. Or increase the border bottom width of row cells.

I tried to add tr { border-bottom: 5px solid black; } ( reproduction link ) but then I get the following output

enter image description here

I also tried to add a dividing row ( reproduction link )

    <tr>
      <template v-for="(cell, columnIndex) in row" :key="columnIndex">
        <td style="background: red">divider</td>
      </template>
    </tr>

but get this result

enter image description here

Do you have any ideas?

baitendbidz
  • 187
  • 3
  • 19
  • Maybe you could synthesize class names that would be placed on cells (or `` elements) according to what you want a "row" to be. Then you could get the `` or `` elements (however you decide; you could even do both) with a class selector. – Pointy Mar 27 '23 at 14:46
  • If you want to have zebra striping in spanned rows, implement a plain table with zebra striping and place it behind the table with spanned rows. Make the background of the table with spanned rows transparent so the zebra strips are visible. You'll need to make sure the text remains the same in the non-spanned rows so that the heights are the same. I'd do that by putting the same text in there, with a color that is the same as the background color, but otherwise identical font/text characteristics to those of the displayed text. – Heretic Monkey Mar 27 '23 at 18:43
  • @baitendbidz `rowspan` basically span the height of more than one cell or row. Hence, after span it consider as a single cell. That's the reason we can apply the style on a whole rowspan. – Debug Diva Apr 06 '23 at 06:08
  • @baitendbidz 1. Does it have to be `` or just a grid-looking? 2. Do rows have equal height?
    – Dimava Apr 10 '23 at 16:26
  • I have defferent ideas for non-table, for even rows, and for fixed-width cols – Dimava Apr 10 '23 at 16:28
  • zebra will look badly or you just failed to implement it for rowspans? – Dimava Apr 10 '23 at 16:33
  • @Dimava it doesn't have to be a table but it should look like one because customers know Excel... and I'm using the Vuetify framework with the table component https://vuetifyjs.com/en/components/tables/ ( which basically is a standard HTML table ) – baitendbidz Apr 10 '23 at 18:16
  • zebra would be bad, yes. That's why I'm looking for a better solution :) – baitendbidz Apr 10 '23 at 18:17
  • @baitendbidz why exactly would zebra be bad? If it's not zebra, should the solution be zebra-like (small rows shadowing rowspans), border-zebra-like (small borders shadowed on rowspans), anti-zebra-like (rowspans shadowing single rows), gradients (showing that green rows belong to green rowspan), or something else? (or you may need an actual paid designer to design the concept) – Dimava Apr 10 '23 at 18:33
  • because 1 row ( 1 array item ) might be huge and then just everything is gray. it would look better if 1 row would have a border bottom width of ???px in a different color. – baitendbidz Apr 10 '23 at 18:48
  • @baitendbidz can you please make an *image* of what you think would be good? – Dimava Apr 10 '23 at 21:20
  • @Dimava sure, does this example help? https://jsfiddle.net/2nay53dp/2/ Unfortunately this didn't work when dealing with rowspans... but I don't know how to identify a single row via CSS – baitendbidz Apr 11 '23 at 05:09
  • @baitendbidz no, I'm asking for a literal image (e.g. png on imgur drawn in paint or whatever) so I can see how you want it to look like – Dimava Apr 12 '23 at 11:55
  • @Dimava does this help? https://imgur.com/a/BE2mgCq – baitendbidz Apr 13 '23 at 13:40
  • @baitendbidz actually very yes, that's just `tr:not(:first-child).split >td {border-top}` – Dimava Apr 13 '23 at 17:59
  • not sure about that. I tested it shorturl.at/ioPY8 but 1. nothing changed 2. how would it know that each city represents a row? – baitendbidz Apr 13 '23 at 18:28

6 Answers6

1

Not really a code answer, more a frame challenge:

Must it be rows? Instead you can use arrows to point from div to div (example code)

example of connecting

This aproach is very similar to what Google does in its Cloud Platform, where they have to solve the same issue you have. Of course this is not the most pretty drawing, its an example. You could have straight lines with 90deg turns, or everything the same colour and/or dotted.

If you wrap your current text in a span with an border and use the tables vertical align, you are very close to the example above "only" needing to connect the dots.

Martijn
  • 15,791
  • 4
  • 36
  • 68
  • thanks for your reply. For now: Yes. I would like to get rid of the Excel like table ( 2.0 ) but if you start with *n* nodes with *m* subnodes ( *of different type / column ) per node and so on... it's getting really hard to display the data for humans. I thought about creating a view like the Github actions graph but the "leading" / "starting" column could be anywhere or even hidden... – baitendbidz Apr 05 '23 at 14:20
1

I think the issue is that you don't really have lines or rows anymore, but rather nested tiles, which is why zebra stripes won't work. I think in this case, it makes more sense to highlight these tiles and their nesting level. You can do this by adjust the borders to the level, i.e. use a thick border between cities, use a thin border between rooms:

Adjusting the vertical borders gives you these ⊢ shapes, which outline the tiles more clearly. Add some colors for a more intuitive distinction between structure and content:

enter image description here

Doubling the size of the borders makes the structure even more apparent, but you need to check if it works with the rest of your page:

enter image description here

To figure out the top border width of a row, you just have to find the index of the first data cell:

const rowClasses = computed(() => tableMatrix.value.map(row => 'border-width-'+ (4 - row.findIndex(cell => !cell.isCoveredByPreviousCell))))

You can set it as a class on each row.

Here it is in the playground

Moritz Ringler
  • 9,772
  • 9
  • 21
  • 34
1
The problem

OP wants the table's primary rows (the most tall ones) to be clearly wisible
Here's the OP's image provided in comment: https://i.stack.imgur.com/oVSxp.png

The solution

This is easy to implement by adding a top border for those rows

<template>
  <table>
    <tr v-for="(row, rowIndex) in tableMatrix"
          :class="{split: isFullSplit(row)}"> ... </tr>
  </table>
</template>
<script setup lang="ts">
  function isFullSplit(row: Cell[]) {
    return row.every(c => !c.isCoveredByPreviousCell)
  }
</script>
<style>
  /* make a big border-top for the primary rows  */
  tr:not(:first-child).split { border-top: 10px solid red; }
</style>

playground
enter image description here

Dimava
  • 7,654
  • 1
  • 9
  • 24
0

I think that it's messy, both visually and in the code, to have it implemented with rowspans. This is because you want two cells on top of each other to be in the same row, but that's not how it's implemented, so any solution that makes them appear to be in the same row will be messy

This is a matter of opinion, but I think that you should put those stacked cells in the same table row, then use CSS to put one piece of information above the other in the same cell. Then you can use the tr { border-bottom: 5px solid black; } to get the result that you want.

  • unfortunately this is not possible because I have multiple columns with multiple rowspans and children in the second column might have multiple subchildren in the third column etc. – baitendbidz Mar 27 '23 at 17:12
0

thing you are wanting to have like zebra strips tabe you should have something in corresponding to every cell like you have city 2, inhabitant 1 and house 1 and in room there is some data as well but in the first row there only is on td in that row there is not other td corresponding to it and now even if you want to have that zebra kinda of table then the styling will only occur in the corresponding td like the room 1 will only have a color because there is no other td corresponding to it

your code also seems a bit messy using the rowspan in js like it can be done through the html as well One of the main thing in the coding is that to keep your code as short as possible

and if you want it to be like zebra striped here's the code

<template>
  <table>
    <thead>
      <th>City</th>
      <th>Inhabitant</th>
      <th>House</th>
      <th>Room</th>
    </thead>
    <tbody>
      <tr v-for="(row, rowIndex) in tableMatrix" :key="rowIndex">
        <template v-for="(cell, columnIndex) in row" :key="columnIndex">
          <td v-if="cell.isCoveredByPreviousCell" style="display: none" />
          <td v-else :rowspan="cell.rowspan ?? 1">
            <template v-if="cell.content">
              {{ cell.content }}
            </template>
          </td>
        </template>
      </tr>
    </tbody>
  </table>
</template>

<script setup lang="ts">
import { ref, Ref } from 'vue';

interface Cell { isCoveredByPreviousCell: boolean; rowspan: number; content?: string; }

type TableMatrix = Cell[][];

const tableMatrix: Ref<TableMatrix> = ref([
  [
    { isCoveredByPreviousCell: false, rowspan: 5, content: "City 2" },
    { isCoveredByPreviousCell: false, rowspan: 4, content: "Inhabitant 1" },
    { isCoveredByPreviousCell: false, rowspan: 3, content: "House 1" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Room 2" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Room 3" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "House 2" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Inhabitant 2" },
    { isCoveredByPreviousCell: false, content: "House 3" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ]
])
</script>
<style>
table {
  border-collapse: collapse;
}
th
th, td {
  padding: 5px;
  text-align: left;
  border-bottom: 2px solid #ddd;
}

tr:nth-child(even) {
  background-color: #f2f2f2;
}

tr:nth-child(odd) {
  background-color: #E020;
}

tr:nth-child(4n+1) {
  background-color: #FFCAD2; /* color for House 1 row */
}

tr:nth-child(3n+2) {
  
  background-color: #B2EBF2; /* color for Room 2 row */
}

tr:nth-child(3n+3) {
  background-color: #F8G4; /* default color for other rows */
}
</style>

-1

An implementation of my comment. First with a blurred green zebra striping:

const data = [
  [{
      isCoveredByPreviousCell: false,
      rowspan: 5,
      content: "City 1"
    },
    {
      isCoveredByPreviousCell: false,
      rowspan: 4,
      content: "Inhabitant 1"
    },
    {
      isCoveredByPreviousCell: false,
      rowspan: 3,
      content: "House 1"
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 1"
    },
  ],
  [{
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 2"
    },
  ],
  [{
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 3"
    },
  ],
  [{
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: false,
      content: "House 2"
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 1"
    },
  ],
  [{
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: false,
      content: "Inhabitant 2"
    },
    {
      isCoveredByPreviousCell: false,
      content: "House 3"
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 1"
    },
  ]
];
const tbody = document.querySelector('table tbody');
let rowIndex = 0;
for (let r of data) {
  let row = document.createElement('tr');
  row.classList.toggle('even', rowIndex % 2 === 0);
  row.classList.toggle('odd', rowIndex % 2 !== 0);
  for (let c of r) {
    let cell = document.createElement('td');
    if (c.content) {
      cell.appendChild(document.createTextNode(c.content));
    }
    row.appendChild(cell);
  }
  tbody.appendChild(row);
  rowIndex++;
}
tbody.parentElement.classList.add('zebra');
const dataTable = tbody.parentElement.cloneNode(true);
document.body.appendChild(dataTable);
dataTable.classList.remove('zebra');
const dataTbody = dataTable.querySelector('tbody');
dataTbody.innerHTML = "";
rowIndex = 0;
for (let r of data) {
  let row = document.createElement('tr');
  for (let c of r) {
    if (!c.isCoveredByPreviousCell) {
      let cell = document.createElement('td');
      if (c.rowspan && c.rowspan > 0) {
        cell.setAttribute('rowspan', c.rowspan);
      }
      if (c.content) {
        cell.appendChild(document.createTextNode(c.content));
      }
      row.appendChild(cell);
    }
  }
  dataTbody.appendChild(row);
  rowIndex++;
}
.as-console-wrapper { max-height: 44px; height: 44px; }

.zebra .even {
  background-color: white;
  color: white;
}

.zebra .odd {
  background-color: palegreen;
  color: palegreen;
}

.zebra th {
  background-color: white;
  color: white;
}

.zebra {
  border-collapse: collapse;
  border: 1px solid palegreen;
  position: absolute;
  z-index: 0;
  filter: blur(4px);
}

table:not(.zebra) {
  border-collapse: collapse;
  border: 1px solid;
  position: absolute;
  z-index: 1;
}
table:not(.zebra) td {
  border: 1px solid;
}
<table>
  <thead>
    <th>City</th>
    <th>Inhabitant</th>
    <th>House</th>
    <th>Room</th>
  </thead>
  <tbody>
  </tbody>
</table>

Then with blurred colored bottom borders on the background table. I tend to like this one better.

const data = [
  [{
      isCoveredByPreviousCell: false,
      rowspan: 5,
      content: "City 1"
    },
    {
      isCoveredByPreviousCell: false,
      rowspan: 4,
      content: "Inhabitant 1"
    },
    {
      isCoveredByPreviousCell: false,
      rowspan: 3,
      content: "House 1"
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 1"
    },
  ],
  [{
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 2"
    },
  ],
  [{
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 3"
    },
  ],
  [{
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: false,
      content: "House 2"
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 1"
    },
  ],
  [{
      isCoveredByPreviousCell: true
    },
    {
      isCoveredByPreviousCell: false,
      content: "Inhabitant 2"
    },
    {
      isCoveredByPreviousCell: false,
      content: "House 3"
    },
    {
      isCoveredByPreviousCell: false,
      content: "Room 1"
    },
  ]
];
const tbody = document.querySelector('table tbody');
let rowIndex = 0;
for (let r of data) {
  let row = document.createElement('tr');
  row.classList.toggle('even', rowIndex % 2 === 0);
  row.classList.toggle('odd', rowIndex % 2 !== 0);
  for (let c of r) {
    let cell = document.createElement('td');
    if (c.content) {
      cell.appendChild(document.createTextNode(c.content));
    }
    row.appendChild(cell);
  }
  tbody.appendChild(row);
  rowIndex++;
}
tbody.parentElement.classList.add('zebra');
const dataTable = tbody.parentElement.cloneNode(true);
document.body.appendChild(dataTable);
dataTable.classList.remove('zebra');
const dataTbody = dataTable.querySelector('tbody');
dataTbody.innerHTML = "";
rowIndex = 0;
for (let r of data) {
  let row = document.createElement('tr');
  for (let c of r) {
    if (!c.isCoveredByPreviousCell) {
      let cell = document.createElement('td');
      if (c.rowspan && c.rowspan > 0) {
        cell.setAttribute('rowspan', c.rowspan);
      }
      if (c.content) {
        cell.appendChild(document.createTextNode(c.content));
      }
      row.appendChild(cell);
    }
  }
  dataTbody.appendChild(row);
  rowIndex++;
}
.as-console-wrapper { max-height: 44px; height: 44px; }

.zebra .even {
  background-color: white;
  color: white;
}

.zebra .odd {
  background-color: white;
  color: white;
}

.zebra th {
  background-color: white;
  color: white;
}

.zebra td {
  border: 1px solid palegreen;
}
.zebra {
  border-collapse: collapse;
  border: 1px solid palegreen;
  position: absolute;
  z-index: 0;
  filter: blur(2px);
}

table:not(.zebra) {
  border-collapse: collapse;
  border: 1px solid;
  position: absolute;
  z-index: 1;
}
table:not(.zebra) td {
  border: 1px solid;
}
<table>
  <thead>
    <th>City</th>
    <th>Inhabitant</th>
    <th>House</th>
    <th>Room</th>
  </thead>
  <tbody>
  </tbody>
</table>
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122