Skip to content

Commit

Permalink
feat: progressive dashboard loading (#244)
Browse files Browse the repository at this point in the history
This restricts the loading of dashboard items to the current viewport, with a buffer of one half-viewport above and below.  As such, only 1.5x the viewport is initially loaded.  Other items will be loaded when the user scrolls them into this buffered viewport.

* feat: progressive dashboard loading

* feat: support initial buffer factor

* feat: support progressive loading after a filter change

* fix: use margin instead of padding to prevent overflow

* Move memoize to its own file

* chore: remove unused import
  • Loading branch information
amcgee authored Feb 19, 2019
1 parent 9220ba0 commit 19bd63b
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 39 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"jest": "^23.6.0",
"jsdoc": "^3.5.5",
"lint-staged": "^7.0.0",
"lodash": "^4.17.11",
"material-design-icons": "^3.0.1",
"material-ui": "^0.20.0",
"object-assign": "4.1.1",
Expand Down
29 changes: 29 additions & 0 deletions src/__tests__/memoizeOne.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import memoizeOne from '../modules/memoizeOne';

describe('memoizeOne', () => {
it('Should return the same value when called twice with shallow-equal arguments', () => {
const object = { a: 0, b: 0 };
const fn = x => x.a + x.b;
const memoizedFn = memoizeOne(fn);

const val0 = memoizedFn(object);
expect(val0).toBe(0);
object.a = 1; // maintain shallow equality
expect(fn(object)).toBe(1);
const val1 = memoizedFn(object);
expect(val1).toBe(0);
});

it('Should forget the first value when called thrice', () => {
const object = { a: 0, b: 0 };
const memoizedFn = memoizeOne(x => x.a + x.b);

const val0 = memoizedFn(object);
expect(val0).toBe(0);
object.a = 1; // maintain shallow equality
const val1 = memoizedFn({ a: 42, b: 84 }); // This will bump val0 out of the cache
expect(val1).toBe(42 + 84);
const val2 = memoizedFn(object);
expect(val2).toBe(1);
});
});
91 changes: 91 additions & 0 deletions src/components/Item/ProgressiveLoadingContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';

const defaultDebounceMs = 100;
const defaultBufferPx = 0;
const defaultInitialBufferFactor = 0.5;

class ProgressiveLoadingContainer extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
debounceMs: PropTypes.number,
bufferPx: PropTypes.number,
initialBufferFactor: PropTypes.number,
};
static defaultProps = {
debounceMs: defaultDebounceMs,
bufferPx: defaultBufferPx,
initialBufferFactor: defaultInitialBufferFactor,
};

state = {
shouldLoad: false,
};
containerRef = null;
shouldLoadHandler = null;

checkShouldLoad(customBufferPx) {
const bufferPx = customBufferPx || this.props.bufferPx;

if (!this.containerRef) {
return;
}

const rect = this.containerRef.getBoundingClientRect();
if (
rect.bottom > -bufferPx &&
rect.top < window.innerHeight + bufferPx
) {
this.setState({
shouldLoad: true,
});

this.removeHandler();
}
}

registerHandler() {
this.shouldLoadHandler = debounce(
() => this.checkShouldLoad(),
this.props.debounceMs
);

window.addEventListener('scroll', this.shouldLoadHandler);
}
removeHandler() {
window.removeEventListener('scroll', this.shouldLoadHandler);
}

componentDidMount() {
this.registerHandler();

const initialBufferPx = this.props.initialBufferFactor
? this.props.initialBufferFactor * window.innerHeight
: undefined;
this.checkShouldLoad(initialBufferPx);
}

componentWillUnmount() {
this.removeHandler();
}

render() {
const {
children,
debounceMs,
bufferPx,
initialBufferFactor,
...props
} = this.props;
const { shouldLoad } = this.state;

return (
<div ref={ref => (this.containerRef = ref)} {...props}>
{shouldLoad && children}
</div>
);
}
}

export default ProgressiveLoadingContainer;
66 changes: 33 additions & 33 deletions src/components/Item/VisualizationItem/Item.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import VisualizationItemHeaderButtons from './ItemHeaderButtons';
import DefaultPlugin from './DefaultPlugin';
import { colors } from '../../../modules/colors';
import ChartPlugin from 'data-visualizer-plugin';
import ProgressiveLoadingContainer from '../ProgressiveLoadingContainer';
import uniqueId from 'lodash/uniqueId';
import memoizeOne from '../../../modules/memoizeOne';

const styles = {
icon: {
Expand Down Expand Up @@ -44,6 +47,8 @@ export class Item extends Component {
showFooter: false,
};

getUniqueKey = memoizeOne(filter => uniqueId());

pluginCredentials = null;

onToggleFooter = () => {
Expand Down Expand Up @@ -123,46 +128,30 @@ export class Item extends Component {
/>
) : null;

getPluginComponent = () => {
const { item } = this.props;
const elementId = getGridItemDomId(item.id);

getContentStyle = () => {
const { item, editMode } = this.props;
const PADDING_BOTTOM = 4;
const contentStyle = !this.props.editMode
return !editMode
? {
height: item.originalHeight - HEADER_HEIGHT - PADDING_BOTTOM,
}
: null;

switch (item.type) {
case CHART: {
return (
<div id={elementId} className="dashboard-item-content">
<ChartPlugin
config={this.props.visualization}
filters={this.props.itemFilter}
forDashboard={true}
style={contentStyle}
/>
</div>
);
}
default: {
return (
<div
id={elementId}
className="dashboard-item-content"
style={contentStyle}
>
<DefaultPlugin {...this.props} />
</div>
);
}
}
};

getPluginComponent = () =>
this.props.item.type === CHART ? (
<ChartPlugin
config={this.props.visualization}
filters={this.props.itemFilter}
forDashboard={true}
style={this.getContentStyle()}
/>
) : (
<DefaultPlugin {...this.props} />
);

render() {
const { item, editMode } = this.props;
const { item, editMode, itemFilter } = this.props;
const { showFooter } = this.state;

return (
Expand All @@ -172,7 +161,18 @@ export class Item extends Component {
actionButtons={this.getActionButtons()}
editMode={editMode}
/>
{this.getPluginComponent()}
<ProgressiveLoadingContainer
id={getGridItemDomId(item.id)}
key={
this.getUniqueKey(
itemFilter
) /* remount the progressive loader every time itemFilter changes */
}
className="dashboard-item-content"
style={this.getContentStyle()}
>
{this.getPluginComponent()}
</ProgressiveLoadingContainer>
{!editMode && showFooter ? <ItemFooter item={item} /> : null}
</Fragment>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ItemGrid/ItemGrid.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
/* dashboard item - content */

.dashboard-item-content {
padding: 0 4px 4px;
margin: 0 4px 4px;
overflow: auto;
}

Expand Down
23 changes: 23 additions & 0 deletions src/modules/memoizeOne.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Note that this ignores discrepancies in 'this', so shouldn't be used with bound functions
// This is useful instead of lodash/memoize when we only need to memoize a single value
// Inspiration: https://github.com/alexreardon/memoize-one

const memoizeOne = fn => {
let lastArgs = undefined;
let lastValue = undefined;

return (...args) => {
if (
lastArgs &&
args.length === lastArgs.length &&
args.every((arg, i) => arg === lastArgs[i])
) {
return lastValue;
}
lastArgs = args;
lastValue = fn(...args);
return lastValue;
};
};

export default memoizeOne;
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11069,11 +11069,6 @@ webpack-sources@^1.0.1:
source-list-map "^2.0.0"
source-map "~0.6.1"

webpack-stats-plugin@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595"
integrity sha512-OYMZLpZrK/qLA79NE4kC4DCt85h/5ipvWJcsefKe9MMw0qU4/ck/IJg+4OmWA+5EfrZZpHXDq92IptfYDWVfkw==

webpack@3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.8.1.tgz#b16968a81100abe61608b0153c9159ef8bb2bd83"
Expand Down

0 comments on commit 19bd63b

Please sign in to comment.