Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grid: colspan, rowspan support #123

Closed
Guria opened this issue Feb 25, 2016 · 28 comments
Closed

Grid: colspan, rowspan support #123

Guria opened this issue Feb 25, 2016 · 28 comments

Comments

@Guria
Copy link

Guria commented Feb 25, 2016

What I want to achieve:

variant 1

Use separate grid per each header row with ScrollSync:

<Grid
  className={styles.HeaderGrid}
  columnWidth={columnWidth*3}
  columnsCount={columnsCount / 3}
  height={rowHeight}
  overscanColumnsCount={overscanColumnsCount}
  renderCell={this._renderSpannedHeaderCell}
  rowHeight={rowHeight}
  rowsCount={1}
  scrollLeft={scrollLeft}
  width={width}
/>
<Grid
  className={styles.HeaderGrid}
  columnWidth={columnWidth}
  columnsCount={columnsCount}
  height={rowHeight}
  overscanColumnsCount={overscanColumnsCount}
  renderCell={this._renderHeaderCell}
  rowHeight={rowHeight}
  rowsCount={1}
  scrollLeft={scrollLeft}
  width={width}
/>

variant 2

override cell width in rendered content and hide "unused" cells

<Grid
  className={styles.HeaderGrid}
  columnWidth={columnWidth}
  columnsCount={columnsCount}
  height={rowHeight*2}
  overscanColumnsCount={overscanColumnsCount}
  renderCell={this._renderHeaderCell}
  rowHeight={rowHeight}
  rowsCount={2}
  scrollLeft={scrollLeft}
  width={width}
/>
/* ... */
  _renderHeaderCell ({ columnIndex, rowIndex }) {
    if (rowIndex === 0) {
      return (columnIndex % 3 === 0) ? (
        <div className={styles.headerCell} style={{ width: 75*3 }}>
          {`C${columnIndex}-long-long-long`}
        </div>
      ) : (
        <div className={styles.headerCell} style={{ display: 'none' }} />
      )
    } else {
      return (
        <div className={styles.headerCell}>
          {`C${columnIndex}`}
        </div>
      )
    }
  }

variant 3 (not available currently)

But would be great to have colspan, rowspan support in grid to avoid extra hidden cells in DOM.
Don't sure in implementation but one possible is:

<Grid
  className={styles.HeaderGrid}
  columnWidth={columnWidth}
  columnsCount={columnsCount}
  height={rowHeight*2}
  overscanColumnsCount={overscanColumnsCount}
  renderCell={this._renderHeaderCell}
  rowHeight={rowHeight}
  rowsCount={2}
  scrollLeft={scrollLeft}
  width={width}
/>
/* ... */
  _renderHeaderCell ({ columnIndex, rowIndex, Grid__cell }) {
    if (rowIndex === 0) {
      return (
        <Grid__cell colspan={3}> { /* since colspan="3" _renderHeaderCell calls for R0,C1 and R0,C2 will be skipped */ }
          <div className={styles.headerCell}>
            {`C${columnIndex}-long-long-long`}
          </div>
        </Grid__cell>
      )
    } else {
      return (
        <div className={styles.headerCell}>
          {`C${columnIndex}`}
        </div>
      )
    }
  }
@bvaughn
Copy link
Owner

bvaughn commented Feb 25, 2016

Sorry @Guria. This isn't going to be implemented. I've considered it before and it would add a lot of complication. If you'd like to control this level of cell-sizing you should create your own high-order component wrapper around VirtualScroll.

@bvaughn bvaughn closed this as completed Feb 25, 2016
@Guria
Copy link
Author

Guria commented Feb 25, 2016

@bvaughn this is ok, since there is a way to acheive desired result. btw, which existing variant do you like more?

@bvaughn
Copy link
Owner

bvaughn commented Feb 25, 2016

I think your first variant, with ScrollSync, is better. It's more inline with how I'm using RV components in other projects.

@phahn
Copy link

phahn commented May 10, 2016

I'm currently working on a similar requirement. I'm using Collection instead of two Grid. Works pretty well so far.

image

@bvaughn
Copy link
Owner

bvaughn commented May 10, 2016

Nice! If you run into performance issues with Collection (which isn't as performant as Grid since it has to do more work, not being able to assume things about its underlying data) you may try switching to Grid and using the cellRangeRenderer property to render the rows/columns Grid tells you to, below your own fixed header.

@hung-phan
Copy link

hung-phan commented Jun 16, 2016

Oh @phahn, do you mind to share you solution with Collection? I have a problem with grid. I want to achieve both colspan, and rowspan

@phahn
Copy link

phahn commented Jun 17, 2016

Hey @hung-phan I have to see if I can share some code after the weekend but in the meantime here's what I'm doing:

I have configuration objects for columns like ColumnGroup which can contain other ColumnGroups and Columns

I pass this list of ColumnGroups to my grid which flattens it and calculates for every entry it's level (distance from top) and the totalWidth of that ColumnGroup which is the sum of it's leaf Column widths.

With the information totalHeaderHeight, number of levels, current level and width of each ColumnGroup and Column I can then create the appropriate div elements in cellSizeAndPositionGetter

For row spans the concept is similar.

@bvaughn
Copy link
Owner

bvaughn commented Jun 17, 2016

Interesting. Initially, I think it may be possible to do something similar with Grid by overriding (forking) cellRangeRenderer. And Grid would be more efficient than Collection since it can make some shortcut assumptions about its data due to the fact that it's all checkerboard/linear.

@hung-phan
Copy link

Thanks @phahn, and @bvaughn for the idea

@bvaughn
Copy link
Owner

bvaughn commented Jun 17, 2016

If you create a cellRangeRenderer implementation this that you like, consider submitting a PR. If it seems generalizable enough, I'll add it for others. :)

@csvan
Copy link

csvan commented Aug 11, 2017

Out of interest, has anyone had any luck doing this within a grid? I am migrating a major angular 1.x project to React and am looking to do something like the following:

rowspan

IOW some non-fixed rows should span the entire grid and contain an SVG. At the moment I am pretty new to react-virtualized, so apologies if this has already been discussed.

@bvaughn
Copy link
Owner

bvaughn commented Aug 11, 2017

I don't really have enough info to say for sure @csvan, but I believe you might want to look into using List for that. Grid is really only necessary when you want to have (a lot of) scrolling horizontal content as well.

@csvan
Copy link

csvan commented Aug 13, 2017

@bvaughn I do need both horizontal and vertical scrollsync, since we will be rendering several thousand rows and columns at once. Sorry for not providing sufficient detail.

I looked into alterative 2 in the OP, and it does approximately what I want (with some hacks). It's regrettable that it leaves redundant invisible columns, but there is no noticeable performance degradation so I can live with that. Will update here if I find a better solution.

@bvaughn
Copy link
Owner

bvaughn commented Aug 13, 2017

Thanks for the update!

It's regrettable that it leaves redundant invisible columns

For what it's worth, you can return null for these "invisible" columns.

@csvan
Copy link

csvan commented Aug 14, 2017

@bvaughn awesome, thanks!

@csvan
Copy link

csvan commented Aug 22, 2017

@Guria @bvaughn unfortunately, I hit a roadblock.
If I set the width of the "spanning" cell to that of the visible width of the Grid, and set display: none for all remaining cells (as per example 2 in the OP), the "spanning" cell will still pop out of existence when the user scrolls the initial section of it out of the visible part of the grid.

For example, if the Grid expects each cell to be 75px wide, and I force my "spanning" cell to be 1200px wide, it will still disappear when the first 75px (approx) are scrolled out the visible part.

Is there any way I can tell the Grid to only remove elements after a certain scroll threshold?

@bvaughn
Copy link
Owner

bvaughn commented Aug 22, 2017

I don't really have a clear picture of what you're doing to be honest, but if you're using a custom cellRangeRenderer then you should be able to cheat a little in terms of when the extra-width cell gets hidden.

@pete-moss
Copy link

@bvaughn I am using a MultiGrid so that I can have nice distinct column and row headers to render a spreadsheet-type grid. Working very nicely so far. I also have an upcoming requirement to support grouped columns. This would affect only how the cells are rendered in the top right Grid. I like some of your suggestions here like trying cellRangeRenderer and/or returning null for "invisible" cells. I haven't tried anything yet, but I was thinking of just trying to handle it in the cellRenderer by having the grouped column cell that spans multiple child columns inject a position: relative div that has the appropriate width. This allows it to bleed out of its containing div. Heck, maybe I could just modify the containing div's width directly. Needs more thought and experimentation. I like your response about being able to return null for "invisble" cells.
But one question for you is with a MultGrid, what is the best way to get the reference to the contained Grids? Where in the React life-cycle can I do this? I would need to know this if I wanted to try the cellRangeRenderer technique since I want that for only the top-right Grid. Thanks for all this great body of work!

@pete-moss
Copy link

pete-moss commented Apr 1, 2018

@csvan @bvaughn I have made some progress with MultiGrid in trying to render grouped columns. The approach I started with follows some of the suggestions here:

  1. Render "spanning" cells and modify their width to be the number of child columns spanned.
  2. Render null for the other cell slots in a grouped column that are "underneath" the spanning cell.

It looked good until I started scrolling horizontally. I hit the same roadblock @csvan did. That is, when the spanning cell falls out of the viewport being rendered (because MultiGrid thinks it has a much smaller width), it disappears.
Here are 2 screen shots which illustrate what I see. First is the non-scrolled picture which looks good:
image
Then, if I scroll right a bit so that the leaf column Col 0 scrolls out of view, I see this:
image
What tells me that my Group 0 spanning cell isn't being rendered is that I don't see the border anymore. This is confirmed by looking at the DOM.

I understand this behavior from the Grid's POV, but at this moment I am scratching my head. I guess I will investigate @bvaughn 's suggestion of using cellRangeRenderer. Haven't had any experience with that yet...

@MarkBarbieri
Copy link

Yes, I have done this. cellRangeRenderer is the solution I used and it works well. Just adjust the columnStartIndex to make sure it captures the first 'real' column.

I also started with hiding cells, then returned null to prevent them rendering at all. Ends up quite a neat solution.

@pete-moss
Copy link

@MarkBarbieri Thanks, Mark. Yes, I am starting down that path of using cellRangeRenderer. To render my grouped column headers above the leaf column, I think my strategy has to be to examine the columnStartIndex passed to my override of defaultCellRangeRenderer for the MultiGrid._topRightGridRef. Find the leaf column at that index and then walk up its ancestor chain of columns and if the "cell slot" for those ancestor columns have a columnIndex that is less than columnStartIndex, then I have to manually rerender those (in my screen shots above that means the 2 cells for Group 0 and Group 0.0) and push them on to the children returned from defaultCellRangeRenderer. Making progress... What could go wrong? (A common saying in our little pod of developers as the code then crashes and burns...)

@MarkBarbieri
Copy link

I used this approach to create a TV guide layout with the whole grid full of irregular blocks. I pre-processed the data and stored a 2D array with indices matching the column/row indices of the grid. Each array object stores whether the cell should be rendered, the width (which is really a columnSpan), the column index of the first visible cell for this element and the column index of the next visible cell. This provides easy manipulation of the focused element.

For example, a cell the spans columns 8-12, it stores

    {
        render: false (except for column 8)
        thisProgramColumn: 8
        nextProgramColumn: 13
        width: 5
    }

For cellRangeRendered, I then looped through the rows to be rendered,

               for (var rowIndex = rowStartIndex; rowIndex < rowStopIndex; rowIndex++) {

                    thisProgramColumn = gridCellParams[rowIndex][columnIndex].thisProgramColumn;

                    if (typeof thisProgramColumn !== 'undefined') {
                        columnStartIndex = Math.min(columnStartIndex, thisProgramColumn);
                    }

                }

                ref.columnStartIndex = columnStartIndex - fixedColumnCount;

            }

@IanGrainger
Copy link

I used this approach to create a TV guide layout with the whole grid full of irregular blocks. I pre-processed the data and stored a 2D array with indices matching the column/row indices of the grid. Each array object stores whether the cell should be rendered, the width (which is really a columnSpan), the column index of the first visible cell for this element and the column index of the next visible cell. This provides easy manipulation of the focused element.

For example, a cell the spans columns 8-12, it stores

    {
        render: false (except for column 8)
        thisProgramColumn: 8
        nextProgramColumn: 13
        width: 5
    }

For cellRangeRendered, I then looped through the rows to be rendered,

               for (var rowIndex = rowStartIndex; rowIndex < rowStopIndex; rowIndex++) {

                    thisProgramColumn = gridCellParams[rowIndex][columnIndex].thisProgramColumn;

                    if (typeof thisProgramColumn !== 'undefined') {
                        columnStartIndex = Math.min(columnStartIndex, thisProgramColumn);
                    }

                }

                ref.columnStartIndex = columnStartIndex - fixedColumnCount;

            }

Wow this sounds like a really useful modification!

I was looking for a component which can create timetables. I'd really appreciate any tips you could give me about modifying rect-virtualized to support this timetable approach!

@pisharodySrikanth
Copy link

If any one is still interested in this. I have created a component with colspan and rowspan support with dynamic cell height.

https://github.com/pisharodySrikanth/virtualized-table

The component takes in data prop in a format similar to how we create the table in html. The dummy cells are hidden behind the rowspan div. This component has some caveats like the one mentioned by @pete-moss, border goes when the colspan / rowspan div is removed from the DOM. I handled this by showing border of the hidden dummy cells. This works because I don't need to vertically center align by rowspan div and knew beforehand that the content of the rowspanned cell is relatively smaller.

Also, I have created a wrapper around cell measurer and I calculate the height of the cell with rowspan by using the sum of cache.rowHeight for the next rowSpan no.of rows.

https://github.com/pisharodySrikanth/virtualized-table/blob/master/src/js/components/VirtualizedTable/CellMeasureWrapper.js

@dkskv
Copy link

dkskv commented May 13, 2022

Thanks everyone for the advice. It turned out to implement a table with hierarchical columns.

I forked "defaultCellRangeRenderer" with 3 changes:

  • I do not call render for "invisible" cells;
  • I calculate the size of each cell, taking into account "invisible" cells overlapped by it;
  • I render cells outside the visibility zone if visible indexes depend on them.

I also used a separate Grid for the header, synchronized with the body via ScrollSync.

@pedromoter
Copy link

pedromoter commented Feb 1, 2023

I used this approach to create a TV guide layout with the whole grid full of irregular blocks. I pre-processed the data and stored a 2D array with indices matching the column/row indices of the grid. Each array object stores whether the cell should be rendered, the width (which is really a columnSpan), the column index of the first visible cell for this element and the column index of the next visible cell. This provides easy manipulation of the focused element.

For example, a cell the spans columns 8-12, it stores

    {
        render: false (except for column 8)
        thisProgramColumn: 8
        nextProgramColumn: 13
        width: 5
    }

For cellRangeRendered, I then looped through the rows to be rendered,

               for (var rowIndex = rowStartIndex; rowIndex < rowStopIndex; rowIndex++) {

                    thisProgramColumn = gridCellParams[rowIndex][columnIndex].thisProgramColumn;

                    if (typeof thisProgramColumn !== 'undefined') {
                        columnStartIndex = Math.min(columnStartIndex, thisProgramColumn);
                    }

                }

                ref.columnStartIndex = columnStartIndex - fixedColumnCount;

            }

Did you modify the entire cellRangeRendered or just the visible part of it. trying to the same as you here!
@pisharodySrikanth

@pedromoter
Copy link

Thanks everyone for the advice. It turned out to implement a table with hierarchical columns.

I forked "defaultCellRangeRenderer" with 3 changes:

  • I do not call render for "invisible" cells;
  • I calculate the size of each cell, taking into account "invisible" cells overlapped by it;
  • I render cells outside the visibility zone if visible indexes depend on them.

I also used a separate Grid for the header, synchronized with the body via ScrollSync.

Would you mind sharing a bit of your cellRangeRendered? @dkskv

@dkskv
Copy link

dkskv commented Feb 1, 2023

Thanks everyone for the advice. It turned out to implement a table with hierarchical columns.
I forked "defaultCellRangeRenderer" with 3 changes:

  • I do not call render for "invisible" cells;
  • I calculate the size of each cell, taking into account "invisible" cells overlapped by it;
  • I render cells outside the visibility zone if visible indexes depend on them.

I also used a separate Grid for the header, synchronized with the body via ScrollSync.

Would you mind sharing a bit of your cellRangeRendered? @dkskv

@pedromoter I signed all the differences in cellRangeRenderer with numbers.
columnManager - my internal object for getting information about cells, taking into account rowSpan and colSpan:

  • columnManager.getSpanningColumnIndex - returns the index of the column for the cell that spans the given one. If the cell is not spanned by other, the index of the cell itself is returned.
  • columnManager.isCellSpanned - predicate indicating whether the cell is spanned by other.
  • columnManager.getCellWidth - adds to current cell width of spanned cells.

// ... some code

  for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
        const rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex);
  
        // 1. An out-of-sight cell must be mounted if visible cells depend on it
        const startIndex = columnManager.getSpanningColumnIndex(rowIndex, columnStartIndex);
  
        for (let columnIndex = startIndex; columnIndex <= columnStopIndex; columnIndex++) {
          // 2. Render for "invisible" cells is not called
          if (columnManager.isCellSpanned(rowIndex, columnIndex)) {
            continue;
          }

// ... some code

              function getHeight(index: number) {
                return rowSizeAndPositionManager.getSizeAndPositionOfCell(index).size;
              }
  
              function getWidth(index: number) {
                return columnSizeAndPositionManager.getSizeAndPositionOfCell(index).size;
              }
  
              style = {
                // 3. Overridden height
                height: columnManager.getCellHeight(getHeight, rowIndex, columnIndex),
                left: columnDatum.offset + horizontalOffsetAdjustment,
                position: "absolute",
                top: rowDatum.offset + verticalOffsetAdjustment,
                // 3. Overridden width
                width: columnManager.getCellWidth(getWidth, rowIndex, columnIndex),
              } as const;

// ... some code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests