From 281bc9031449b0addc2aeb197bc942d73235fae9 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Mon, 28 Oct 2024 16:28:39 +1300 Subject: [PATCH] [entropy] don't update if not on screen Use an intersection observer to detect when the entropy panel is visible on the screen (viewport). When it's offscreen we don't update entropy data (an expensive calculation). When it comes back onscreen we recalculate the entropy data if it has become stale. This results in slightly strange behaviour when the entropy panel will be shown with no bars and they'll be drawn after a slight delay (while the data's recalculated). The wins are much improved performance when the entropy panel is not on screen (which is common). --- src/actions/entropy.js | 2 +- src/actions/recomputeReduxState.js | 1 + src/actions/types.js | 1 + src/components/entropy/entropyD3.js | 1 + src/components/entropy/index.js | 26 ++++++++++++++++++++++---- src/reducers/entropy.js | 5 ++++- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/actions/entropy.js b/src/actions/entropy.js index 4a067c16f..de15c6bb7 100644 --- a/src/actions/entropy.js +++ b/src/actions/entropy.js @@ -14,7 +14,7 @@ export const updateEntropyVisibility = debounce((dispatch, getState) => { controls.animationPlayPauseButton !== "Play" ) {return;} - if (!controls.panelsToDisplay.includes("entropy")) { + if (!controls.panelsToDisplay.includes("entropy") || entropy.onScreen===false) { if (entropy.bars===undefined) { return; // no need to dispatch another action - the state's already been invalidated } diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 588e5dc62..18e55b07d 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -1014,6 +1014,7 @@ export const createStateFromQueryOrJSONs = ({ const [entropyBars, entropyMaxYVal] = calcEntropyInView(tree.nodes, tree.visibility, entropy.selectedCds, entropy.showCounts); entropy.bars = entropyBars; entropy.maxYVal = entropyMaxYVal; + entropy.onScreen = true; } /* update frequencies if they exist (not done for new JSONs) */ diff --git a/src/actions/types.js b/src/actions/types.js index fb59b92fe..fbe3d3cd8 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -35,6 +35,7 @@ export const CHANGE_TREE_ROOT_IDX = "CHANGE_TREE_ROOT_IDX"; export const TOGGLE_NARRATIVE = "TOGGLE_NARRATIVE"; export const ENTROPY_DATA = "ENTROPY_DATA"; export const ENTROPY_COUNTS_TOGGLE = "ENTROPY_COUNTS_TOGGLE"; +export const ENTROPY_ONSCREEN_CHANGE = "ENTROPY_ONSCREEN_CHANGE"; export const PAGE_CHANGE = "PAGE_CHANGE"; export const MIDDLEWARE_ONLY_ANIMATION_STARTED = "MIDDLEWARE_ONLY_ANIMATION_STARTED"; export const URL_QUERY_CHANGE_WITH_COMPUTED_STATE = "URL_QUERY_CHANGE_WITH_COMPUTED_STATE"; diff --git a/src/components/entropy/entropyD3.js b/src/components/entropy/entropyD3.js index f05a6a5e0..806b41ee0 100644 --- a/src/components/entropy/entropyD3.js +++ b/src/components/entropy/entropyD3.js @@ -468,6 +468,7 @@ EntropyChart.prototype._highlightSelectedBars = function _highlightSelectedBars( /* draw the bars (for each base / aa) */ EntropyChart.prototype._drawBars = function _drawBars() { if (!this.okToDrawBars) {return;} + this._groups.mainBars.selectAll("*").remove(); // bars may be undefined (indicating the underlying data became stale) diff --git a/src/components/entropy/index.js b/src/components/entropy/index.js index e666aa85a..2e37091e7 100644 --- a/src/components/entropy/index.js +++ b/src/components/entropy/index.js @@ -12,7 +12,7 @@ import { tabGroup, tabGroupMember, tabGroupMemberSelected } from "../../globalSt import EntropyChart from "./entropyD3"; import InfoPanel from "./infoPanel"; import { changeEntropyCdsSelection, showCountsNotEntropy } from "../../actions/entropy"; -import { ENTROPY_DATA } from "../../actions/types"; +import { ENTROPY_DATA, ENTROPY_ONSCREEN_CHANGE } from "../../actions/types"; import { timerStart, timerEnd } from "../../util/perf"; import { encodeColorByGenotype } from "../../util/getGenotype"; import { nucleotide_gene } from "../../util/globals"; @@ -70,6 +70,7 @@ const getStyles = (width) => { maxYVal: state.entropy.maxYVal, showCounts: state.entropy.showCounts, loaded: state.entropy.loaded, + onScreen: state.entropy.onScreen, colorBy: state.controls.colorBy, /** * Note that zoomMin & zoomMax only represent the state when changed by a URL @@ -178,13 +179,15 @@ class Entropy extends React.Component { ); } - requestRecalculationOfBars() { + requestRecalculationOfBars(onScreen) { /* The component is now responsible for requesting a recalculation of the underlying entropy data as this allows us to skip calculations when (e.g.) the panel is not rendered */ this.props.dispatch((dispatch, getState) => { const { entropy, tree } = getState(); const [bars, maxYVal] = calcEntropyInView(tree.nodes, tree.visibility, entropy.selectedCds, entropy.showCounts); - dispatch({type: ENTROPY_DATA, data: bars, maxYVal}); + const action = {type: ENTROPY_DATA, data: bars, maxYVal}; + if (onScreen!==undefined) action.onScreen = onScreen; + dispatch(action); }); } setUp(props) { @@ -209,9 +212,24 @@ class Entropy extends React.Component { } this.setState({chart}); } + visibilityOnScreenChange(entries) { + if (entries.length!==1) { + return console.error(`Unexpected IntersectionObserver callback entries of length`, entries.length); + } + const onScreen = entries[0].isIntersecting; + if (onScreen===this.props.onScreen) return; // can happen when component initially rendered + // if gone off screen or come back on screen with the bars still valid then it's a simple toggle action + if (!onScreen || this.props.bars) { + return this.props.dispatch({type: ENTROPY_ONSCREEN_CHANGE, onScreen}) + } + // else if back on screen and the bars are invalid then we want to regenerate them + this.requestRecalculationOfBars(onScreen); + } componentDidMount() { if (this.props.loaded) { - this.setUp(this.props); + this.setUp(this.props); + const observer = new IntersectionObserver(this.visibilityOnScreenChange.bind(this), {threshold: 0.0}); + observer.observe(this.d3entropy) } } UNSAFE_componentWillReceiveProps(nextProps) { diff --git a/src/reducers/entropy.js b/src/reducers/entropy.js index a17b212b2..0431b3206 100644 --- a/src/reducers/entropy.js +++ b/src/reducers/entropy.js @@ -20,8 +20,11 @@ const Entropy = (state = defaultEntropyState(), action) => { return Object.assign({}, state, { loaded: true, bars: action.data, - maxYVal: action.maxYVal + maxYVal: action.maxYVal, + onScreen: Object.hasOwn(action, 'onScreen') ? action.onScreen : state.onScreen, }); + case types.ENTROPY_ONSCREEN_CHANGE: + return {...state, onScreen: action.onScreen}; case types.ENTROPY_COUNTS_TOGGLE: return Object.assign({}, state, { showCounts: action.showCounts