Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.

Issue 234 - Virtualization#253

Merged
Marc-Andre-Rivet merged 28 commits intomasterfrom
3.1-issue234-virtualization-2
Dec 4, 2018
Merged

Issue 234 - Virtualization#253
Marc-Andre-Rivet merged 28 commits intomasterfrom
3.1-issue234-virtualization-2

Conversation

@Marc-Andre-Rivet
Copy link
Copy Markdown
Contributor

@Marc-Andre-Rivet Marc-Andre-Rivet commented Nov 27, 2018

This PR closes #234 and introduces virtualization to the table.

To try it out
npm run build.watch
http://localhost:8080/index.html?mode=virtualized
http://localhost:8080/index.html?mode=fixed,virtualized

or spin off the review app and access the virtualization app
will load a 26 columns by 100k lines dataset

This functionality change is big and complex and ties in to most features provided by the table. I will be very appreciative of anyone spending some time testing actual scenarios with it! :)

To activate add prop virtualization=true and recommend setting prop pagination_mode=false (although virtualization and pagination can work together)

This implementation of virtualization is based on some basic assumptions that we can revisited or lifted later

  • row height is constant throughout the lifetime of the table
  • row height is the same for all rows
  • for certain settings the columns width may change based on the displayed rows content -- the columns width needs to be set explicitly to prevent variations based on data
  • headers height and number is constant throughout the lifetime of the table

Other known limitations are:

  • scrolling slowly upwards is glitchy at the moment
  • rendering may be blank for a fraction of a second on FF and Safari when scrolling fast -- this could be improved upon with either extra padding or buffering (possibly through the use of workers)

The virtualized_data is a new step in the chain of derived data generated by the table for display purposes

data: the entire dataset passed to the table
data + filtering + sorting -> virtual_data
virtual_data + pagination -> viewport_data
viewport_data + virtualization -> virtualized_data

Do note that the virtualized_data is not exposed to Dash as it is a considered a UI "implementation detail" -- in essence the user is still scrolling through the viewport, not in a special new state.

Most of the changes in this PR are to:

  1. switch usage away from viewport_data internally towards virtualized_data and dealing with the side-effects (offsets, additional calculations, etc.)
  2. dealing with scroll state and evaluating what needs to be displayed -- supporting fixed and unfixed rows while virtualized and creating no regressions in the non-virtualized use case
  3. additional tests to deal with virtualization specifically or dealing with existing functionality and making sure it still works with virtualization

The first batch of commits below (bump version --> build artefacts) have been rebased and it can be helpful to look at them one at a time to isolate complexity. Afterwards there is multiple commits that have not been rebased in order to fix all remaining regressions. Haven't rebased the rebase as there is a lot less going on there anyway.

The table provides a default height/max-height when virtualization is toggled, same as for n_fixed_rows > 0 -- additional styling can be done with style_table. No css: [...] overrides should be required.

@chriddyp chriddyp temporarily deployed to dash-table-review-pr-253 November 27, 2018 15:16 Inactive
case AppMode.Default:
default:
return getDefaultState();
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Various pre-configured modes for the demo app to use

}

function gendata(func, ndata = N_DATA) {
function gendata(func: (i: number) => any, ndata = N_DATA) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS to TS, adding typing info

cy.get('.row-1').then($el => {
$el[0].style.overflow = toggled ? '' : 'unset';
});
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cypress auto-scrolls very aggressively to guarantee element visibility -- this causes issues with the table's own scrolling mechanism and event hooks -- this allows disabling/enabling scrolling on the content section at will.

DashTable.getDelete(0).click();
DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '2'));
});
Object.values(AppMode).forEach(mode => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all standalone tests, execute the same tests with all existing app modes

@@ -0,0 +1,127 @@
import DashTable from 'cypress/DashTable';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a duplicate of the next file -- has been subsequently removed.

@@ -0,0 +1,127 @@
import DashTable from 'cypress/DashTable';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a duplicate of the next file -- has been subsequently removed.

"dash-table/*": ["./../../src/dash-table/*"]

"dash-table/*": ["./../../src/dash-table/*"],
"demo/*": ["./../../demo/*"]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need access to AppMode

viewport.data,
viewport.indices,
virtualized.data,
virtualized.indices,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Viewport is not the final result of the table processing its content anymore, now it's Virtualized!

data -> virtual_data -> viewport_data -> virtualized_data

If virtualization=false, virtualized_data === viewport_data + offset of zero

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming is not super clear to me, but I don't have any other suggestions, other than renaming everything, to something like data -> derived_data -> paginated_data -> virtualized_data (and even that perhaps doesn't make that much sense either). I think mostly the fact that the difference between virtual_data and virtualized_data isn't immediately obvious, and viewport_data to me kind of implies that that is what the virtualized_data should be. Just writing down some thoughts here, I guess.

columns,
viewport.data,
virtualized.data,
virtualized.offset,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offset is now required to reconcile the various memoized handlers, active and selected cell evaluations, etc.

const DERIVED_REGEX = /^derived_/;

export default class Table extends Component<PropsWithDefaultsAndDerived> {
export default class Table extends Component<PropsWithDefaultsAndDerived, StandaloneState> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improving internal typing for standalone case by defining the State type -- the controlled table's state has been entirely moved back to the table now and modifies it through props.setState

This is necessary because we need to hook up to the scroll event and to eval various element sizes to make virtualization work correctly.

virtual,
virtual_selected_rows
virtual_selected_rows,
virtualized
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ControlledTable's props are now a combination of all props and state (for standalone) + all derived props

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that virtualized_data is not part of the data contract with Dash -- this is purely internal

/*#if DEV*/
const props: any = this.props;
R.forEach(
key => props[key] === newProps[key] && Logger.fatal(`Updated prop ${key} was mutated`),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some additional sanity here, make sure we are not sending a mutated version of an existing props property -- since some of the comps are pure, this prevents updates from happening correctly

/*#if DEV*/
const props: any = this.state;
R.forEach(
key => props[key] === (newProps as any)[key] && Logger.fatal(`Updated prop ${key} was mutated`),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

uiHeaders?: IUserInterfaceCell[];
}

export type StandaloneState = IState & Partial<PropsWithDefaultsAndDerived>;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table's standalone state is the combination of the connected props + basic state props

import { memoizeOneFactory } from 'core/memoizer';
import memoizerCache from 'core/memoizerCache';
import { ICellFactoryOptions } from 'dash-table/components/Table/props';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming, clean up

(datum, rowIndex) => mapRow(
(column, columnIndex) => {
const active = isActiveCell(activeCell, rowIndex, columnIndex);
const active = isActiveCell(activeCell, rowIndex + offset.rows, columnIndex + offset.columns);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now need the offset to reference the right row and column (although this one is always 0 for now)

"width": 50,
"max_width": 50,
"min_width": 50
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make width stable so the table doesn't resize itself constantly as we scroll!

@chriddyp
Copy link
Copy Markdown
Member

Looks great!

I found that for horizontal scrolling, I needed to "scroll twice". The first scoll nudged the headers like 5 pixels but didn't move anything else. Then, scrolling again moved the table.
horizontal-scroll

@cldougl
Copy link
Copy Markdown
Member

cldougl commented Nov 27, 2018

probably not blocking but there's some unexpected highlighting behaviour when copying a large selection (i can move this to a separate issue if we want to address after this pr/during clean up).

  • select a cell
    -scroll down (so selected cell is no longer displayed)
  • shift and select a new cell to select a range
  • see unexpected highlighting (blue in gif)
  • copy and paste works as expected

copy

@Marc-Andre-Rivet
Copy link
Copy Markdown
Contributor Author

@chriddyp Hum.. looking at the code I think this is a pre-existing problem as the onScroll handling hasn't changed for that part -- I'll try to reproduce the issue.

@Marc-Andre-Rivet
Copy link
Copy Markdown
Contributor Author

@chriddyp Tried in Chrome/FF/Safari with and without scroll "always" visible -- I can't reproduce -- if you scroll just a little does it eventually realign itself? The alignment delay is a known issue with fixed rows. I'm unable to tell from the gif exactly what/when this is happening.

@Marc-Andre-Rivet
Copy link
Copy Markdown
Contributor Author

@cldougl Agreed. I would make this a subsequent PR to improve the behavior afterwards.

…lization-2

# Conflicts:
#	CHANGELOG.md
#	dash_table/package-info.json
#	demo/App.js
#	demo/data.ts
#	package-lock.json
#	package.json
#	src/core/components/IsolatedInput/index.tsx
#	tests/cypress/tests/standalone/navigation_test.ts
@chriddyp chriddyp temporarily deployed to dash-table-review-pr-253 November 29, 2018 13:55 Inactive
Copy link
Copy Markdown
Contributor

@valentijnnieman valentijnnieman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of questions / remarks. Looks good to me otherwise! I'll leave the approving up to others, when they're done with QA etc.

} : (newProps: Partial<PropsWithDefaultsAndDerived>) => this.setState(newProps);
} : (newProps: Partial<PropsWithDefaultsAndDerived>) => {
/*#if DEV*/
const props: any = this.state;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it slightly (but only slightly) confusing to assign state as props here. Just for clarity's sake, perhaps renaming it const state = this.state; is slightly more readable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what allows the table to operate on its own -- standalone. There's not much that can be done about it while keeping that capability.. setProps === setState in that scenario :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh misread the comment! I'm trying to be consistent and "play" with props in all cases even though the props really are state. If that makes sense..

viewport.data,
viewport.indices,
virtualized.data,
virtualized.indices,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming is not super clear to me, but I don't have any other suggestions, other than renaming everything, to something like data -> derived_data -> paginated_data -> virtualized_data (and even that perhaps doesn't make that much sense either). I think mostly the fact that the difference between virtual_data and virtualized_data isn't immediately obvious, and viewport_data to me kind of implies that that is what the virtualized_data should be. Just writing down some thoughts here, I guess.

return;
}

const { r1c1 } = this.refs as { [key: string]: HTMLElement };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain the use of refs here? Is it absolutely necessary? Or is there perhaps a way to handle this more gracefully in React? I guess it's just to get the top cell, to do some calculations on, but I can see this be very costly on performance. If it needs to create a ref every time you scroll.

Copy link
Copy Markdown
Contributor Author

@Marc-Andre-Rivet Marc-Andre-Rivet Dec 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unaware of using refs having (or not having) a performance cost. Can you elaborate?

r1c1 is actually the second's row's second's column's "cell", the table that contains the main content of the data table (or all the content without fixed rows/columns)

Without refs, I could reference the top $el of the component and do the querySelector on .row-1 .cell-1-1 tr > td:first-of-type not sure if this is better or worse. It's a little less React-ish than using refs to do the same thing but not significantly so I guess.

I'll move line 157 after the if.. might be evaluating it uselessly and running selectors is not free either!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, while this is a very costly operation it happens only once -- once uiCell is set the if at line 151 will be triggered.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have misread - if it happens only once then it's not so bad!

@chriddyp chriddyp temporarily deployed to dash-table-review-pr-253 December 3, 2018 22:21 Inactive
@chriddyp chriddyp temporarily deployed to dash-table-review-pr-253 December 3, 2018 22:22 Inactive
@Marc-Andre-Rivet
Copy link
Copy Markdown
Contributor Author

@valentijnnieman Did a small optimization change based on comments. As stated above, wondering how refs are expensive vs. other approaches?

@cldougl
Copy link
Copy Markdown
Member

cldougl commented Dec 4, 2018

I can reproduce #253 (comment) but only in firefox and only when the first cell is selected

scroll

@cldougl
Copy link
Copy Markdown
Member

cldougl commented Dec 4, 2018

otherwise I'm not seeing any issues in the QA!

@chriddyp chriddyp temporarily deployed to dash-table-review-pr-253 December 4, 2018 03:58 Inactive
@chriddyp chriddyp temporarily deployed to dash-table-review-pr-253 December 4, 2018 04:09 Inactive
@Marc-Andre-Rivet
Copy link
Copy Markdown
Contributor Author

@cldougl W-o-w! Thanks! I can reproduce the issue and changed the style so as the focused cell gets a box-shadow as normal instead of a border.

…lization-2

# Conflicts:
#	CHANGELOG.md
#	dash_table/bundle.js
#	dash_table/demo.js
#	dash_table/package-info.json
#	package-lock.json
#	package.json
#	tests/cypress/tsconfig.json
Copy link
Copy Markdown
Member

@cldougl cldougl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💃 everything is looking good for me now! I will open a new issue for #253 (comment) after this is merged

Copy link
Copy Markdown
Contributor

@valentijnnieman valentijnnieman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm 👍

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Virtualization

4 participants