Commit 2f0e2ae3 authored by Lauri Himanen's avatar Lauri Himanen
Browse files

Integrated Placeholders to Plot and Structure, fixed unnecessary re-rendering...

Integrated Placeholders to Plot and Structure, fixed unnecessary re-rendering for individual DOS/BS plots.
parent 768dffbe
Pipeline #92768 passed with stages
in 32 minutes and 15 seconds
......@@ -25,7 +25,6 @@ import VibrationalOverview from '../visualization/VibrationalOverview'
import { ApiDialog } from '../ApiDialogButton'
import Structure from '../visualization/Structure'
import Actions from '../Actions'
import Placeholder from '../visualization/Placeholder'
import Quantity from '../Quantity'
import { Link as RouterLink } from 'react-router-dom'
import { DOI } from '../search/DatasetList'
......@@ -38,6 +37,35 @@ import _ from 'lodash'
import {appBase, encyclopediaEnabled, normalizeDisplayValue} from '../../config'
import GeoOptOverview from '../visualization/GeoOptOverview'
const useHeaderStyles = makeStyles(theme => ({
root: {
marginBottom: theme.spacing(1),
display: 'flex',
flexDirection: 'row',
alignContent: 'flex-start'
},
title: {
fontSize: '1.25rem'
}
}))
function Header({title, actions}) {
const classes = useHeaderStyles()
return <Box className={classes.root}>
<Box flexGrow={1}>
<Typography className={classes.title}>
{title}
</Typography>
</Box>
{actions}
</Box>
}
Header.propTypes = {
title: PropTypes.string,
actions: PropTypes.any
}
const usePropertyCardStyles = makeStyles(theme => ({
root: {
marginBottom: theme.spacing(2)
......@@ -51,14 +79,7 @@ function PropertyCard({title, children, actions}) {
const classes = usePropertyCardStyles()
return <Card className={classes.root}>
<CardContent>
<Box display="flex" flexDirection="row" alignContent="flex-start">
<Box flexGrow={1}>
<Typography className={classes.title}>
{title}
</Typography>
</Box>
{actions}
</Box>
<Header title={title} actions={actions}></Header>
{children}
</CardContent>
</Card>
......@@ -70,6 +91,31 @@ PropertyCard.propTypes = {
title: PropTypes.string
}
const useSidebarCardStyles = makeStyles(theme => ({
content: {
padding: 0,
paddingBottom: theme.spacing(2)
},
title: {
fontSize: '1.25rem',
marginBottom: theme.spacing(1)
}
}))
function SidebarCard({title, actions, children}) {
const classes = useSidebarCardStyles()
return <CardContent className={classes.content}>
<Header title={title} actions={actions}></Header>
{children}
</CardContent>
}
SidebarCard.propTypes = {
title: PropTypes.string,
actions: PropTypes.any,
children: PropTypes.any
}
const useStyles = makeStyles(theme => ({
root: {
marginBottom: theme.spacing(4)
......@@ -80,19 +126,12 @@ const useStyles = makeStyles(theme => ({
cardHeader: {
paddingBottom: 0
},
title: {
fontSize: '1.25rem',
marginBottom: theme.spacing(1)
},
sidebar: {
paddingRight: theme.spacing(3)
},
actions: {
marginBottom: theme.spacing(2)
},
sidebarContent: {
marginBottom: theme.spacing(2)
},
divider: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1)
......@@ -202,15 +241,13 @@ export default function DFTEntryOverview({data}) {
'pbc': sys.configuration_periodic_dimensions
})
}
if (!failed) {
if (!failed && energies.length > 2) {
energies = convertSI(energies, 'joule', {energy: 'electron_volt'}, false)
const e_criteria_wf = section_wf?.section_geometry_optimization?.input_energy_difference_tolerance
const sampling_method = section_run?.section_sampling_method
const e_criteria_fs = sampling_method && sampling_method[0]?.geometry_optimization_energy_change
const e_criteria = e_criteria_wf || e_criteria_fs
setGeoOpt({energies: energies, structures: trajectory, energy_change_criteria: e_criteria})
} else {
setGeoOpt({energies: null, structures: null, energy_change_criteria: null})
}
} else if (wfType === 'phonon') {
// Find phonon dos and dispersion
......@@ -323,8 +360,7 @@ export default function DFTEntryOverview({data}) {
})
}, [data, api, raiseError, setElectronicStructure, setStructures])
const loadingRepo = !data
const quantityProps = {data: data, loading: loadingRepo}
const quantityProps = {data: data, loading: !data}
const domain = data.domain && domains[data.domain]
// Create toggle buttons for each structure option
......@@ -369,11 +405,12 @@ export default function DFTEntryOverview({data}) {
return (
<Grid container spacing={0} className={classes.root}>
{/* Left column */}
<Grid item xs={4} className={classes.sidebar}>
<ApiDialog data={data} open={showAPIDialog} onClose={() => { setShowAPIDialog(false) }}></ApiDialog>
<Actions className={classes.actions} justifyContent='flex-start' variant='contained' size='medium' actions={actions}></Actions>
<Box className={classes.sidebarContent}>
<Typography variant="body1" className={classes.title}>Method</Typography>
<SidebarCard title='Method'>
<Quantity flex>
<Quantity quantity="dft.code_name" label='code name' noWrap {...quantityProps}/>
<Quantity quantity="dft.code_version" label='code version' noWrap {...quantityProps}/>
......@@ -385,10 +422,9 @@ export default function DFTEntryOverview({data}) {
{method?.van_der_Waals_method && <Quantity quantity="van_der_Waals_method" label='van der Waals method' noWrap {...quantityProps}/>}
{method?.relativity_method && <Quantity quantity="relativity_method" label='relativity method' noWrap data={method}/>}
</Quantity>
</Box>
</SidebarCard>
<Divider className={classes.divider} />
<Box className={classes.sidebarContent}>
<Typography variant="body1" className={classes.title}>Author metadata</Typography>
<SidebarCard title='Author metadata'>
<Quantity flex>
<Quantity quantity='comment' placeholder='no comment' {...quantityProps} />
<Quantity quantity='references' placeholder='no references' {...quantityProps}>
......@@ -415,10 +451,9 @@ export default function DFTEntryOverview({data}) {
</div>}
</Quantity>
</Quantity>
</Box>
</SidebarCard>
<Divider className={classes.divider}/>
<Box className={classes.sidebarContent}>
<Typography variant="body1" className={classes.title}>Processing information</Typography>
<SidebarCard title='Processing information'>
<Quantity column style={{maxWidth: 350}}>
<Quantity quantity="mainfile" noWrap ellipsisFront withClipboard {...quantityProps}/>
<Quantity quantity="calc_id" label={`${domain ? domain.entryLabel : 'entry'} id`} noWrap withClipboard {...quantityProps}/>
......@@ -442,13 +477,15 @@ export default function DFTEntryOverview({data}) {
</Typography>
</Quantity>
</Quantity>
</Box>
</SidebarCard>
</Grid>
{/* Right column */}
<Grid item xs={8}>
<PropertyCard title="Material">
<Grid container spacing={1}>
<Grid item xs={5}>
<Box marginTop={1}>
<Box>
<Quantity column>
<Quantity quantity="formula" label='formula' noWrap {...quantityProps}/>
<Quantity quantity="dft.system" label='material type' noWrap {...quantityProps}/>
......@@ -465,28 +502,23 @@ export default function DFTEntryOverview({data}) {
</Box>
</Grid>
<Grid item xs={7}>
{loading
? <Placeholder className={classes.structure} variant="rect"></Placeholder>
: structures &&
<>
{structureToggles.length > 1 &&
<ToggleButtonGroup
size="small"
exclusive
value={shownSystem}
onChange={handleStructureChange}
aria-label="text formatting"
>
{structureToggles}
</ToggleButtonGroup>
}
<Structure system={structures.get(shownSystem)} aspectRatio={1.5} />
</>
}
<>
{structureToggles?.length > 1 &&
<ToggleButtonGroup
size="small"
exclusive
value={shownSystem}
onChange={handleStructureChange}
aria-label="text formatting"
>
{structureToggles}
</ToggleButtonGroup>
}
<Structure system={structures && structures.get(shownSystem)} aspectRatio={1.5} />
</>
</Grid>
</Grid>
</PropertyCard>
{electronicStructure &&
<PropertyCard title="Electronic properties">
<ElectronicStructureOverview
......@@ -494,13 +526,11 @@ export default function DFTEntryOverview({data}) {
</ElectronicStructureOverview>
</PropertyCard>
}
{geoOpt && structures &&
<PropertyCard title="Geometry optimization">
<GeoOptOverview data={geoOpt}></GeoOptOverview>
</PropertyCard>
}
{vibrationalData &&
<PropertyCard title="Vibrational properties">
<VibrationalOverview
......
......@@ -45,7 +45,7 @@ function BandStructure({data, layout, aspectRatio, className, classes, onRelayou
// Determine the final plotted data based on the received data. Will work with
// normalized and unnormalized data.
useEffect(() => {
if (data === undefined) {
if (!data) {
return
}
......
......@@ -65,23 +65,27 @@ function ElectronicStructureOverview({data, range, className, classes, raiseErro
// Synchronize panning between BS/DOS plots
const handleBSRelayouting = useCallback((event) => {
let update = {
yaxis: {
autorange: false,
range: [event['yaxis.range[0]'], event['yaxis.range[1]']]
if (data.dos) {
let update = {
yaxis: {
autorange: false,
range: [event['yaxis.range[0]'], event['yaxis.range[1]']]
}
}
setDosLayout(update)
}
setDosLayout(update)
}, [])
}, [data])
const handleDOSRelayouting = useCallback((event) => {
let update = {
yaxis: {
autorange: false,
range: [event['yaxis.range[0]'], event['yaxis.range[1]']]
if (data.bs) {
let update = {
yaxis: {
autorange: false,
range: [event['yaxis.range[0]'], event['yaxis.range[1]']]
}
}
setBsLayout(update)
}
setBsLayout(update)
}, [])
}, [data])
return (
<RecoilRoot>
......@@ -102,17 +106,14 @@ function ElectronicStructureOverview({data, range, className, classes, raiseErro
{data.bs
? <Box className={style.bs}>
<Typography variant="subtitle1" align='center'>Band structure</Typography>
{data?.bs?.section_k_band
? <BandStructure
data={data.bs.section_k_band}
layout={bsLayout}
aspectRatio={1.0}
unitsState={unitsState}
onRelayouting={handleBSRelayouting}
onReset={() => { setDosLayout({yaxis: {range: range}}) }}
></BandStructure>
: <Placeholder className={null} aspectRatio={1.1} variant="rect"></Placeholder>
}
<BandStructure
data={data?.bs?.section_k_band}
layout={bsLayout}
aspectRatio={1.0}
unitsState={unitsState}
onRelayouting={handleBSRelayouting}
onReset={() => { setDosLayout({yaxis: {range: range}}) }}
></BandStructure>
</Box>
: null
}
......
......@@ -150,14 +150,12 @@ function GeoOptOverview({data, className, classes}) {
</Box>
<Box className={style.structure}>
<Typography variant="subtitle1" align='center'>Optimization trajectory</Typography>
<ErrorHandler message='Could not load structure.'>
<Structure
system={data.structures[step]}
aspectRatio={0.75}
options={{view: {fitMargin: 0.75}}}
positionsOnly={true}
></Structure>
</ErrorHandler>
<Structure
system={data.structures[step]}
aspectRatio={0.75}
options={{view: {fitMargin: 0.75}}}
positionsOnly={true}
></Structure>
</Box>
</Box>
</RecoilRoot>
......
......@@ -19,6 +19,7 @@ import React from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { Skeleton } from '@material-ui/lab'
import PropTypes from 'prop-types'
import clsx from 'clsx'
/**
* Component that is used as a placeholder while loading data. Fairly simple
......@@ -26,10 +27,12 @@ import PropTypes from 'prop-types'
*/
export default function Placeholder(props) {
// If aspect ratio is provided, use it to determine width and height
const {aspectRatio, ...other} = props
const {aspectRatio, className, classes, ...other} = props
const useStyles = makeStyles(props => {
if (aspectRatio) {
return {
root: {
},
skeletonContainer: {
height: 0,
overflow: 'hidden',
......@@ -46,15 +49,19 @@ export default function Placeholder(props) {
}
}
})
const classes = useStyles()
const styles = useStyles(classes)
if (aspectRatio) {
return <div className={classes.skeletonContainer}>
<Skeleton variant="rect" className={classes.skeleton} {...other}/>
return <div className={clsx(className, styles.root)}>
<div className={styles.skeletonContainer}>
<Skeleton variant="rect" className={styles.skeleton} {...other}/>
</div>
</div>
}
return <Skeleton {...other}></Skeleton>
}
Placeholder.propTypes = {
aspectRatio: PropTypes.number
aspectRatio: PropTypes.number,
className: PropTypes.string,
classes: PropTypes.string
}
......@@ -21,7 +21,8 @@ import { makeStyles } from '@material-ui/core/styles'
import { cloneDeep } from 'lodash'
import {
Typography
Typography,
Box
} from '@material-ui/core'
import {
Fullscreen,
......@@ -30,6 +31,7 @@ import {
Replay
} from '@material-ui/icons'
import Floatable from './Floatable'
import Placeholder from '../visualization/Placeholder'
import Actions from '../Actions'
import Plotly from 'plotly.js-cartesian-dist-min'
import clsx from 'clsx'
......@@ -40,6 +42,7 @@ export default function Plot({data, layout, config, floatTitle, capture, aspectR
const [float, setFloat] = useState(false)
const [captureSettings, setCaptureSettings] = useState()
const firstUpdate = useRef(true)
const [loading, setLoading] = useState(true)
useEffect(() => {
let defaultCapture = {
......@@ -63,6 +66,14 @@ export default function Plot({data, layout, config, floatTitle, capture, aspectR
},
root: {
},
placeHolder: {
left: 0,
right: 0,
position: 'absolute'
},
plot: {
visibility: loading ? 'hidden' : 'visible'
},
spacer: {
flex: 1
},
......@@ -73,7 +84,7 @@ export default function Plot({data, layout, config, floatTitle, capture, aspectR
}
})
const style = useStyles(classes)
const styles = useStyles(classes)
// Set the final layout
const finalLayout = useMemo(() => {
......@@ -154,67 +165,53 @@ export default function Plot({data, layout, config, floatTitle, capture, aspectR
return mergeObjects(config, defaultConfig)
}, [config])
// Initialize the plot object on first render
useEffect(() => {
Plotly.newPlot(canvasRef.current, data, finalLayout, finalConfig)
if (firstUpdate.current) {
// Attach events when the canvas is first created
if (onRelayouting) {
canvasRef.current.on('plotly_relayouting', onRelayouting)
}
if (onRedraw) {
canvasRef.current.on('plotly_redraw', onRedraw)
}
if (onRelayout) {
canvasRef.current.on('plotly_relayout', onRelayout)
}
if (onHover) {
canvasRef.current.on('plotly_hover', onHover)
}
firstUpdate.current = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// This callback redraws the plot whenever the canvas element changes. In
// order to properly detect changes in a reference, a reference callback is
// used. This is the recommended way to monitor reference changes as a simple
// useRef is not guaranteed to update:
// https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
const canvasRef = useCallback(node => {
// Do nothing if the canvas has not actually changed.
if (node === null || canvasRef.current === node) {
// Do nothing if the canvas has not actually changed or data is not ready
if (node === null || canvasRef.current === node || !data) {
return
}
// Redraw if moving to a new canvas. Also re-attach events.
if (canvasRef.current !== undefined) {
// When the canvas reference is instantiated for the first time, create a
// new plot.
if (canvasRef.current === undefined) {
console.log('Loaded!')
Plotly.newPlot(node, data, finalLayout, finalConfig)
if (firstUpdate.current) {
firstUpdate.current = false
}
setLoading(false)
// When the reference changes for the second time, react instead to save
// some time
} else {
console.log('Redraw!')
let oldLayout = canvasRef.current.layout
let oldData = canvasRef.current.data
// Update canvas and redraw on it
Plotly.react(node, oldData, oldLayout, finalConfig)
}
// Re-attach events whenever the canvas changes
if (onRelayouting) {
node.on('plotly_relayouting', onRelayouting)
}
if (onRedraw) {
node.on('plotly_redraw', onRedraw)
}
if (onRelayout) {
node.on('plotly_relayout', onRelayout)
}
if (onHover) {
node.on('plotly_hover', onHover)
}
// (Re-)attach events whenever the canvas changes
if (onRelayouting) {
node.on('plotly_relayouting', onRelayouting)
}
if (onRedraw) {
node.on('plotly_redraw', onRedraw)
}
if (onRelayout) {
node.on('plotly_relayout', onRelayout)
}
if (onHover) {
node.on('plotly_hover', onHover)
}
// Update canvas element
canvasRef.current = node
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, onHover, onRedraw, onRelayout, onRelayouting])
// Update plots when data or config is updated. useLayoutEffect is used so
// that React rendering and Plotly rendering are synced.
......@@ -255,15 +252,20 @@ export default function Plot({data, layout, config, floatTitle, capture, aspectR
{tooltip: 'Capture image', onClick: handleCapture, content: <CameraAlt/>}
]
return (
<Floatable className={clsx(className, style.root)} float={float} onFloat={() => setFloat(!float)} aspectRatio={aspectRatio}>
// Even if the plots are still loading, all the html elements need to be
// placed in the DOM. During loading, they are placed underneath the
// palceholder with visibility=hidden. This way Plotly still has access to
// these HTML nodes and their sizes when the plots are loading.
return <Box className={clsx(className, styles.root)} position='relative' width='100%'>
{loading && <Placeholder className={styles.placeHolder} variant="rect" aspectRatio={aspectRatio}></Placeholder>}
<Floatable className={styles.plot} float={float} onFloat={() => setFloat(!float)} aspectRatio={aspectRatio}>
{float && <Typography variant="h6">{floatTitle}</Typography>}
<div ref={canvasRef} style={{width: '100%', height: '100%'}}></div>
<div className={style.header}>
<div className={styles.header}>
<Actions actions={actions}></Actions>
</div>
</Floatable>
)
</Box>
}
Plot.propTypes = {
......
......@@ -35,6 +35,7 @@ import {
} from '@material-ui/icons'
import { StructureViewer } from '@lauri-codes/materia'
import Floatable from './Floatable'
import Placeholder from '../visualization/Placeholder'
import Actions from '../Actions'
import { mergeObjects } from '../../utils'
import { withErrorHandler, ErrorCard } from '../ErrorHandler'
......@@ -44,7 +45,7 @@ import _ from 'lodash'
* Used to show atomistic systems in an interactive 3D viewer based on the
* 'materia'-library.
*/
function Structure({className, classes, system, options, viewer, captureName, aspectRatio, positionsOnly, loading, sizeLimit}) {
function Structure({className, classes, system, options, viewer, captureName, aspectRatio, positionsOnly, sizeLimit}) {
// States
const [anchorEl, setAnchorEl] = React.useState(null)
const [fullscreen, setFullscreen] = useState(false)
......@@ -55,11 +56,11 @@ function Structure({className, classes, system, options, viewer, captureName, as
const [showPrompt, setShowPrompt] = useState(false)
const [accepted, setAccepted] = useState(false)
const [nAtoms, setNAtoms] = useState(false)
const [loading, setLoading] = useState(true)
// Variables
const open = Boolean(anchorEl)
const refViewer = useRef(null)
const refCanvas = useRef(null)