From 2c81abcc04c406e22db1591beb10583da4e68d96 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 | 4 ++++ src/components/entropy/index.js | 24 ++++++++++++++++++++++-- src/reducers/entropy.js | 5 ++++- 6 files changed, 33 insertions(+), 4 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 4c4ca72c9..6eea03087 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -1014,6 +1014,7 @@ export const createStateFromQueryOrJSONs = ({ // The panel will calculate the bars/maxYVal if needed, so we defer calculation until then entropy.bars = undefined; entropy.maxYVal = 1; + 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 bd2b95a70..806b41ee0 100644 --- a/src/components/entropy/entropyD3.js +++ b/src/components/entropy/entropyD3.js @@ -468,8 +468,12 @@ 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) + if (!this.bars) {return;} + /* Calculate bar width */ const validXPos = this.scales.xMain.domain()[0]; // any value inside the scale's domain will do let barWidth = this.scales.xMain(validXPos+1) - this.scales.xMain(validXPos); // pixels between 2 nucleotides diff --git a/src/components/entropy/index.js b/src/components/entropy/index.js index 882d73995..fc16e615e 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 @@ -219,9 +220,28 @@ class Entropy extends React.Component { } } + 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 + return 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, 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