From a133d368e3bd281e9c92c9a8d2324ffb38a10ccc Mon Sep 17 00:00:00 2001
From: Lauri Himanen <lauri.himanen@gmail.com>
Date: Fri, 11 Sep 2020 15:24:37 +0200
Subject: [PATCH] Added new components for plotting: Plot, DOS and
BandStructure. The DOS and BS plots are now also integrated to the
ArchiveBrowser where they are shown in the overview.
---
gui/package.json | 1 +
gui/public/index.html | 4 +-
gui/src/components/api.js | 80 ++++
gui/src/components/archive/ArchiveBrowser.js | 41 ++-
.../material/ElectronicStructureOverview.js | 139 +++++++
gui/src/components/material/MaterialPage.js | 61 ++++
gui/src/components/search/MaterialsList.js | 8 +
.../components/visualization/BandStructure.js | 244 +++++++++++++
.../components/visualization/BrillouinZone.js | 342 ++++++++++++++++++
gui/src/components/visualization/DOS.js | 127 +++++++
gui/src/components/visualization/Floatable.js | 140 +++++++
gui/src/components/visualization/Plot.js | 275 ++++++++++++++
gui/src/components/visualization/Structure.js | 128 +++----
gui/src/utils.js | 35 +-
gui/yarn.lock | 5 +
nomad/app/api/encyclopedia.py | 6 +-
16 files changed, 1551 insertions(+), 85 deletions(-)
create mode 100644 gui/src/components/material/ElectronicStructureOverview.js
create mode 100644 gui/src/components/material/MaterialPage.js
create mode 100644 gui/src/components/visualization/BandStructure.js
create mode 100644 gui/src/components/visualization/BrillouinZone.js
create mode 100644 gui/src/components/visualization/DOS.js
create mode 100644 gui/src/components/visualization/Floatable.js
create mode 100644 gui/src/components/visualization/Plot.js
diff --git a/gui/package.json b/gui/package.json
index ae986f8ee0..0451037b99 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -29,6 +29,7 @@
"pace": "^0.0.4",
"pace-js": "^1.0.2",
"piwik-react-router": "^0.12.1",
+ "plotly.js-cartesian-dist-min": "^1.54.7",
"qs": "^6.8.0",
"react": "^16.13.1",
"react-app-polyfill": "^1.0.1",
diff --git a/gui/public/index.html b/gui/public/index.html
index a111cb993a..bd3ef7cee4 100644
--- a/gui/public/index.html
+++ b/gui/public/index.html
@@ -5,6 +5,9 @@
window.paceOptions = {
restartOnPushState: true
}
+ // This needs to be defined for Plotly.js graphs, see
+ // https://github.com/plotly/plotly.js/blob/master/dist/README.md#partial-bundles
+ window.PlotlyConfig = {MathJaxConfig: 'local'}
</script>
<script src="https://unpkg.com/pace-js@1.0.2/pace.min.js"></script>
<link href="%PUBLIC_URL%/pace.css" rel="stylesheet" />
@@ -38,7 +41,6 @@
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
-
<title>NOMAD</title>
</head>
<body>
diff --git a/gui/src/components/api.js b/gui/src/components/api.js
index f3fccbfe7a..cad16c0b45 100644
--- a/gui/src/components/api.js
+++ b/gui/src/components/api.js
@@ -299,6 +299,86 @@ class Api {
.finally(this.onFinishLoading)
}
+ async encyclopediaBasic(materialId) {
+ this.onStartLoading()
+ return this.swagger()
+ .then(client => client.apis.encyclopedia.get_material({
+ material_id: materialId
+ }))
+ .catch(handleApiError)
+ .then(response => {
+ const result = response.body || response.text || response.data
+ if (typeof result === 'string') {
+ try {
+ return JSON.parse(result)
+ } catch (e) {
+ try {
+ return JSON.parse(result.replace(/\bNaN\b/g, '"NaN"'))
+ } catch (e) {
+ return result
+ }
+ }
+ } else {
+ return result
+ }
+ })
+ .finally(this.onFinishLoading)
+ }
+
+ async encyclopediaCalculations(materialId) {
+ this.onStartLoading()
+ return this.swagger()
+ .then(client => client.apis.encyclopedia.get_calculations({
+ material_id: materialId
+ }))
+ .catch(handleApiError)
+ .then(response => {
+ const result = response.body || response.text || response.data
+ if (typeof result === 'string') {
+ try {
+ return JSON.parse(result)
+ } catch (e) {
+ try {
+ return JSON.parse(result.replace(/\bNaN\b/g, '"NaN"'))
+ } catch (e) {
+ return result
+ }
+ }
+ } else {
+ return result
+ }
+ })
+ .finally(this.onFinishLoading)
+ }
+
+ async encyclopediaCalculation(materialId, calcId, payload) {
+ this.onStartLoading()
+ return this.swagger()
+ .then(client => client.apis.encyclopedia.get_calculation({
+ material_id: materialId,
+ calc_id: calcId,
+ payload: payload
+ }))
+ .catch(handleApiError)
+ .then(response => {
+ const result = response.body || response.text || response.data
+ if (typeof result === 'string') {
+ try {
+ return JSON.parse(result)
+ } catch (e) {
+ try {
+ return JSON.parse(result.replace(/\bNaN\b/g, '"NaN"'))
+ } catch (e) {
+ return result
+ }
+ }
+ } else {
+ return result
+ }
+ })
+ .finally(this.onFinishLoading)
+ }
+
async calcProcLog(uploadId, calcId) {
this.onStartLoading()
return this.swagger()
diff --git a/gui/src/components/archive/ArchiveBrowser.js b/gui/src/components/archive/ArchiveBrowser.js
index ab129ac571..600cc6a832 100644
--- a/gui/src/components/archive/ArchiveBrowser.js
+++ b/gui/src/components/archive/ArchiveBrowser.js
@@ -10,6 +10,8 @@ import { resolveRef, rootSections } from './metainfo'
import { Title, metainfoAdaptorFactory, DefinitionLabel } from './MetainfoBrowser'
import { Matrix, Number } from './visualizations'
import Structure from '../visualization/Structure'
+import BandStructure from '../visualization/BandStructure'
+import DOS from '../visualization/DOS'
import { StructureViewer } from '@lauri-codes/materia'
import Markdown from '../Markdown'
import { convert } from '../../utils'
@@ -305,6 +307,25 @@ QuantityValue.propTypes = ({
* title.
*/
function Overview({section, def}) {
+ // Styles
+ const useStyles = makeStyles(
+ {
+ bands: {
+ width: '30rem',
+ height: '30rem'
+ },
+ dos: {
+ width: '20',
+ height: '20'
+ },
+ bz: {
+ width: '20',
+ height: '20'
+ }
+ }
+ )
+ const style = useStyles()
+
// Structure visualization for section_system
if (def.name === 'section_system') {
let url = window.location.href
@@ -338,7 +359,25 @@ function Overview({section, def}) {
visualizedSystem.sectionPath = sectionPath
visualizedSystem.index = index
- return <Structure viewer={viewer} system={system} positionsOnly={positionsOnly}></Structure>
+ return <Structure
+ viewer={viewer}
+ system={system}
+ positionsOnly={positionsOnly}
+ ></Structure>
+ // Band structure plot for section_k_band or section_k_band_normalized
+ } else if (def.name === 'section_k_band' || def.name === 'section_k_band_normalized') {
+ return <BandStructure
+ className={style.bands}
+ data={section}
+ aspectRatio={1}
+ ></BandStructure>
+ // DOS plot for section_dos
+ } else if (def.name === 'section_dos') {
+ return <DOS
+ className={style.dos}
+ data={section}
+ aspectRatio={1 / 2}
+ ></DOS>
}
return null
}
diff --git a/gui/src/components/material/ElectronicStructureOverview.js b/gui/src/components/material/ElectronicStructureOverview.js
new file mode 100644
index 0000000000..790779ce55
--- /dev/null
+++ b/gui/src/components/material/ElectronicStructureOverview.js
@@ -0,0 +1,139 @@
+import React, { useEffect, useState, useCallback } from 'react'
+import PropTypes from 'prop-types'
+import {
+ Box,
+ Card,
+ CardHeader,
+ CardContent
+} from '@material-ui/core'
+import DOS from '../visualization/DOS'
+import BandStructure from '../visualization/BandStructure'
+import BrillouinZone from '../visualization/BrillouinZone'
+import { makeStyles } from '@material-ui/core/styles'
+import { withApi } from '../api'
+
+function ElectronicStructureOverview({data, range, className, classes, api, raiseError}) {
+ const [dos, setDos] = useState()
+ const [dosLayout, setDosLayout] = useState({yaxis: {range: range}})
+ const [bs, setBs] = useState()
+ const [bsLayout, setBsLayout] = useState({yaxis: {range: range}})
+
+ // Styles
+ const useStyles = makeStyles((theme) => {
+ return {
+ row: {
+ display: 'flex',
+ flexDirection: 'row',
+ width: '100%'
+ },
+ bz: {
+ flex: '1 1 25%'
+ },
+ bands: {
+ flex: '1 1 50%'
+ },
+ dos: {
+ flex: '1 1 25%'
+ }
+ }
+ })
+ const style = useStyles(classes)
+
+ // Load the data parallelly from API on first render
+ useEffect(() => {
+ if (data === undefined) {
+ return
+ }
+ // Check what data is available and request each in parallel
+ let representatives = data.calculations.representatives
+ let promises = []
+ let requestedProperties = ['electronic_dos', 'electronic_band_structure']
+ let availableProperties = []
+ for (let property of requestedProperties) {
+ if (representatives.hasOwnProperty(property)) {
+ promises.push(
+ api.encyclopediaCalculation(
+ data.basic.material_id,
+ representatives[property],
+ {'properties': [property]}
+ )
+ )
+ availableProperties.push(property)
+ }
+ }
+ Promise.allSettled(promises).then((results) => {
+ for (let i = 0; i < availableProperties.length; ++i) {
+ let property = availableProperties[i]
+ let result = results[i].value[property]
+ if (property === 'electronic_dos') {
+ setDos(result)
+ }
+ if (property === 'electronic_band_structure') {
+ setBs(result)
+ }
+ }
+ }).catch(error => {
+ console.log(error)
+ if (error.name === 'DoesNotExist') {
+ raiseError(error)
+ }
+ })
+ }, [data, api, raiseError])
+
+ // Synchronize panning between BS/DOS plots
+ const handleBSRelayouting = useCallback((event) => {
+ let update = {
+ yaxis: {
+ range: [event['yaxis.range[0]'], event['yaxis.range[1]']]
+ }
+ }
+ setDosLayout(update)
+ }, [])
+ const handleDOSRelayouting = useCallback((event) => {
+ let update = {
+ yaxis: {
+ range: [event['yaxis.range[0]'], event['yaxis.range[1]']]
+ }
+ }
+ setBsLayout(update)
+ }, [])
+
+ return (
+ <Card>
+ <CardHeader title="Electronic structure" />
+ <CardContent>
+ <Box className={style.row}>
+ <BrillouinZone data={bs} className={style.bz} aspectRatio={1 / 2}></BrillouinZone>
+ <BandStructure
+ data={bs}
+ layout={bsLayout}
+ className={style.bands}
+ aspectRatio={1}
+ onRelayouting={handleBSRelayouting}
+ ></BandStructure>
+ <DOS
+ data={dos}
+ layout={dosLayout}
+ className={style.dos}
+ aspectRatio={1 / 2}
+ onRelayouting={handleDOSRelayouting}
+ ></DOS>
+ </Box>
+ </CardContent>
+ </Card>
+ )
+}
+
+ElectronicStructureOverview.propTypes = {
+ data: PropTypes.object,
+ range: PropTypes.array,
+ className: PropTypes.string,
+ classes: PropTypes.object,
+ api: PropTypes.object,
+ raiseError: PropTypes.func
+}
+ElectronicStructureOverview.defaultProps = {
+ range: [-10, 20]
+}
+
+export default withApi(false, true)(ElectronicStructureOverview)
diff --git a/gui/src/components/material/MaterialPage.js b/gui/src/components/material/MaterialPage.js
new file mode 100644
index 0000000000..81d427d795
--- /dev/null
+++ b/gui/src/components/material/MaterialPage.js
@@ -0,0 +1,61 @@
+import React, { useEffect, useState } from 'react'
+import PropTypes from 'prop-types'
+import { Box } from '@material-ui/core'
+import ElectronicStructureOverview from './ElectronicStructureOverview'
+import { useRouteMatch, Route } from 'react-router-dom'
+import { withApi } from '../api'
+
+const MaterialPageContent = withApi(false, true)(({fixed, api, materialId, raiseError}) => {
+ const props = fixed ? {maxWidth: 1200} : {}
+ const [data, setData] = useState()
+
+ // Load the data parallelly from API on first render
+ useEffect(() => {
+ Promise.all([
+ api.encyclopediaBasic(materialId),
+ api.encyclopediaCalculations(materialId)
+ ]).then((results) => {
+ setData({
+ basic: results[0],
+ calculations: results[1]
+ })
+ }).catch(error => {
+ if (error.name === 'DoesNotExist') {
+ raiseError(error)
+ }
+ })
+ }, [api, materialId, raiseError])
+
+ return <Box padding={3} margin="auto" {...props}>
+ <ElectronicStructureOverview data={data}></ElectronicStructureOverview>
+ </Box>
+})
+MaterialPageContent.propTypes = ({
+ materialId: PropTypes.string,
+ api: PropTypes.func,
+ raiseError: PropTypes.func,
+ fixed: PropTypes.bool
+})
+function MaterialPage() {
+ const { path } = useRouteMatch()
+
+ return (
+ <Route
+ path={`${path}/:materialId?/:tab?`}
+ render={({match: {params: {materialId, tab = 'overview'}}}) => {
+ if (materialId) {
+ return (
+ <React.Fragment>
+ <MaterialPageContent fixed={true} materialId={materialId}>
+ </MaterialPageContent>
+ </React.Fragment>
+ )
+ } else {
+ return ''
+ }
+ }}
+ />
+ )
+}
+
+export default MaterialPage
diff --git a/gui/src/components/search/MaterialsList.js b/gui/src/components/search/MaterialsList.js
index 0160e662d1..94a2409c50 100644
--- a/gui/src/components/search/MaterialsList.js
+++ b/gui/src/components/search/MaterialsList.js
@@ -75,6 +75,11 @@ export default function MaterialsList(props) {
paginationText = `1-${results.length.toLocaleString()} of ${(total || 0).toLocaleString()}`
}
+ /* const handleViewMaterial = useCallback((event, materialId) => {
+ event.stopPropagation()
+ history.push(`/material/${materialId}/overview`)
+ }, [history]) */
+
const pagination = <TableCell colSpan={1000} classes={{root: classes.scrollCell}}>
<Toolbar className={classes.scrollBar}>
<span className={classes.scrollSpacer}> </span>
@@ -92,6 +97,9 @@ export default function MaterialsList(props) {
<IconButton href={`${appBase}/encyclopedia/#/material/${entry.encyclopedia.material.material_id}`}>
<DetailsIcon />
</IconButton>
+ {/* <IconButton onClick={event => handleViewMaterial(event, entry.encyclopedia.material.material_id)}>
+ <DetailsIcon />
+ </IconButton> */}
</Tooltip>
return <DataTable
diff --git a/gui/src/components/visualization/BandStructure.js b/gui/src/components/visualization/BandStructure.js
new file mode 100644
index 0000000000..4edf22e2eb
--- /dev/null
+++ b/gui/src/components/visualization/BandStructure.js
@@ -0,0 +1,244 @@
+import React, {useState, useEffect, useMemo} from 'react'
+import PropTypes from 'prop-types'
+import { makeStyles, useTheme } from '@material-ui/core/styles'
+import clsx from 'clsx'
+import {
+ Box
+} from '@material-ui/core'
+import Plot from '../visualization/Plot'
+import { convert, distance, mergeObjects } from '../../utils'
+
+export default function BandStructure({data, layout, aspectRatio, className, classes, onRelayout, onAfterPlot, onRedraw, onRelayouting}) {
+ const [finalData, setFinalData] = useState(undefined)
+ const [pathSegments, setPathSegments] = useState(undefined)
+
+ // Styles
+ const useStyles = makeStyles(
+ {
+ root: {
+ }
+ }
+ )
+ const style = useStyles(classes)
+ const theme = useTheme()
+
+ // Determine the final plotted data based on the received data. Will work with
+ // normalized and unnormalized data.
+ useEffect(() => {
+ if (data === undefined) {
+ return
+ }
+
+ // Determine if data is normalized
+ const norm = data.section_k_band_segment_normalized === undefined ? '' : '_normalized'
+ const segmentName = 'section_k_band_segment' + norm
+ const energyName = 'band_energies' + norm
+ const kpointName = 'band_k_points' + norm
+
+ let plotData = []
+ let nChannels = data[segmentName][0][energyName].length
+ let nBands = data[segmentName][0][energyName][0][0].length
+
+ // Calculate distances if missing
+ let tempSegments = []
+ if (data[segmentName][0].k_path_distances === undefined) {
+ let length = 0
+ for (let segment of data[segmentName]) {
+ const k_path_distances = []
+ const nKPoints = segment[energyName][0].length
+ let start = segment[kpointName][0]
+ let end = segment[kpointName].slice(-1)[0]
+ let segmentLength = distance(start, end)
+ for (let iKPoint = 0; iKPoint < nKPoints; ++iKPoint) {
+ const kPoint = segment[kpointName][iKPoint]
+ const dist = distance(start, kPoint)
+ k_path_distances.push(length + dist)
+ }
+ length += segmentLength
+ segment.k_path_distances = k_path_distances
+ tempSegments.push(k_path_distances)
+ }
+ } else {
+ for (let segment of data[segmentName]) {
+ tempSegments.push(segment.k_path_distances)
+ }
+ }
+ setPathSegments(tempSegments)
+
+ // Path
+ let path = []
+ for (let segment of data[segmentName]) {
+ path = path.concat(segment.k_path_distances)
+ tempSegments.push(segment.k_path_distances)
+ }
+
+ // Second spin channel
+ if (nChannels === 2) {
+ let bands = []
+ for (let iBand = 0; iBand < nBands; ++iBand) {
+ bands.push([])
+ }
+ for (let segment of data[segmentName]) {
+ for (let iBand = 0; iBand < nBands; ++iBand) {
+ let nKPoints = segment[energyName][0].length
+ for (let iKPoint = 0; iKPoint < nKPoints; ++iKPoint) {
+ bands[iBand].push(segment[energyName][0][iKPoint][iBand])
+ }
+ }
+ }
+
+ // Create plot data entry for each band
+ for (let band of bands) {
+ band = convert(band, 'joule', 'eV')
+ plotData.push(
+ {
+ x: path,
+ y: band,
+ type: 'scatter',
+ mode: 'lines',
+ line: {
+ color: theme.palette.secondary.main,
+ width: 2
+ }
+ }
+ )
+ }
+ }
+
+ // First spin channel
+ let bands = []
+ for (let iBand = 0; iBand < nBands; ++iBand) {
+ bands.push([])
+ }
+ for (let segment of data[segmentName]) {
+ for (let iBand = 0; iBand < nBands; ++iBand) {
+ let nKPoints = segment[energyName][0].length
+ for (let iKPoint = 0; iKPoint < nKPoints; ++iKPoint) {
+ bands[iBand].push(segment[energyName][0][iKPoint][iBand])
+ }
+ }
+ path = path.concat(segment.k_path_distances)
+ }
+
+ // Create plot data entry for each band
+ for (let band of bands) {
+ band = convert(band, 'joule', 'eV')
+ plotData.push(
+ {
+ x: path,
+ y: band,
+ type: 'scatter',
+ mode: 'lines',
+ line: {
+ color: theme.palette.primary.main,
+ width: 2
+ }
+ }
+ )
+ }
+
+ setFinalData(plotData)
+ }, [data, theme.palette.primary.main, theme.palette.secondary.main])
+
+ // Merge custom layout with default layout
+ const tmpLayout = useMemo(() => {
+ let defaultLayout = {
+ xaxis: {
+ tickfont: {
+ size: 14
+ }
+ },
+ yaxis: {
+ title: {
+ text: 'Energy (eV)'
+ }
+ }
+ }
+ mergeObjects(layout, defaultLayout)
+ return defaultLayout
+ }, [layout])
+
+ // Compute layout that depends on data.
+ const computedLayout = useMemo(() => {
+ if (data === undefined || pathSegments === undefined) {
+ return {}
+ }
+ // Set new layout that contains the segment labels
+ const norm = data.section_k_band_segment_normalized === undefined ? '' : '_normalized'
+ const segmentName = 'section_k_band_segment' + norm
+ const labelName = 'band_segm_labels' + norm
+ let labels = []
+ let labelKPoints = []
+ for (let iSegment = 0; iSegment < data[segmentName].length; ++iSegment) {
+ let segment = data[segmentName][iSegment]
+ if (iSegment === 0) {
+ // If label is not defined, use empty string
+ const startLabel = segment[labelName] ? segment[labelName][0] : ''
+ labels.push(startLabel)
+ labelKPoints.push(pathSegments[iSegment][0])
+ }
+ const endLabel = segment[labelName] ? segment[labelName][1] : ''
+ labels.push(endLabel)
+ labelKPoints.push(pathSegments[iSegment].slice(-1)[0])
+ }
+ let shapes = []
+ for (let iShape = 1; iShape < labelKPoints.length - 1; ++iShape) {
+ let labelKPoint = labelKPoints[iShape]
+ shapes.push({
+ type: 'line',
+ x0: labelKPoint,
+ y0: 0,
+ x1: labelKPoint,
+ y1: 1,
+ yref: 'paper',
+ line: {
+ color: '#999',
+ width: 1,
+ dash: '10px,10px'
+ }
+ })
+ }
+ let ticks = {
+ shapes: shapes,
+ xaxis: {
+ tickmode: 'array',
+ tickvals: labelKPoints,
+ ticktext: labels
+ }
+ }
+ return ticks
+ }, [data, pathSegments])
+
+ // Merge the given layout and layout computed from data
+ const finalLayout = useMemo(() => {
+ return mergeObjects(computedLayout, tmpLayout, 'shallow')
+ }, [computedLayout, tmpLayout])
+
+ return (
+ <Box className={clsx(style.root, className)}>
+ <Plot
+ data={finalData}
+ layout={finalLayout}
+ aspectRatio={aspectRatio}
+ floatTitle={'Band structure'}
+ onRelayout={onRelayout}
+ onAfterPlot={onAfterPlot}
+ onRedraw={onRedraw}
+ onRelayouting={onRelayouting}
+ >
+ </Plot>
+ </Box>
+ )
+}
+
+BandStructure.propTypes = {
+ data: PropTypes.object, // section_band_structure or section_band_structure_normalized
+ layout: PropTypes.object,
+ aspectRatio: PropTypes.number,
+ classes: PropTypes.object,
+ className: PropTypes.string,
+ onAfterPlot: PropTypes.func,
+ onRedraw: PropTypes.func,
+ onRelayout: PropTypes.func,
+ onRelayouting: PropTypes.func
+}
diff --git a/gui/src/components/visualization/BrillouinZone.js b/gui/src/components/visualization/BrillouinZone.js
new file mode 100644
index 0000000000..b0e39e65de
--- /dev/null
+++ b/gui/src/components/visualization/BrillouinZone.js
@@ -0,0 +1,342 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react'
+import clsx from 'clsx'
+import PropTypes from 'prop-types'
+import { makeStyles } from '@material-ui/core/styles'
+import {
+ Box,
+ Checkbox,
+ Menu,
+ MenuItem,
+ IconButton,
+ Tooltip,
+ Typography,
+ FormControlLabel
+} from '@material-ui/core'
+import {
+ MoreVert,
+ Fullscreen,
+ FullscreenExit,
+ CameraAlt,
+ Replay
+} from '@material-ui/icons'
+import { BrillouinZoneViewer } from '@lauri-codes/materia'
+import Floatable from './Floatable'
+
+export default function BrillouinZone({className, classes, options, viewer, system, positionsOnly, sizeLimit, captureName, aspectRatio}) {
+ // States
+ const [anchorEl, setAnchorEl] = React.useState(null)
+ const [fullscreen, setFullscreen] = useState(false)
+ const [showBonds, setShowBonds] = useState(true)
+ const [showLatticeConstants, setShowLatticeConstants] = useState(true)
+ const [showCell, setShowCell] = useState(true)
+ const [error, setError] = useState(null)
+
+ // Variables
+ const open = Boolean(anchorEl)
+ const refViewer = useRef(null)
+ const refCanvas = useRef(null)
+
+ // Styles
+ const useStyles = makeStyles((theme) => {
+ return {
+ root: {
+ },
+ container: {
+ display: 'flex',
+ width: '100%',
+ height: '100%',
+ flexDirection: 'column',
+ backgroundColor: 'white'
+ },
+ header: {
+ display: 'flex',
+ flexDirection: 'row',
+ zIndex: 1
+ },
+ spacer: {
+ flex: 1
+ },
+ viewerCanvas: {
+ flex: 1,
+ zIndex: 0,
+ minHeight: 0, // added min-height: 0 to allow the item to shrink to fit inside the container.
+ marginBottom: theme.spacing(2),
+ display: error === null ? 'block' : 'none'
+ },
+ errorContainer: {
+ flex: 1,
+ zIndex: 0,
+ minHeight: 0, // added min-height: 0 to allow the item to shrink to fit inside the container.
+ marginBottom: theme.spacing(2),
+ alignItems: 'center',
+ justifyContent: 'center',
+ display: error === null ? 'none' : 'flex'
+ },
+ errorMessage: {
+ flex: '0 0 70%',
+ color: '#aaa',
+ textAlign: 'center'
+ },
+ iconButton: {
+ backgroundColor: 'white'
+ }
+ }
+ })
+ let style = useStyles(classes)
+
+ // 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 measuredRef = useCallback(node => {
+ refCanvas.current = node
+ if (node === null) {
+ return
+ }
+ if (refViewer.current === null) {
+ return
+ }
+ refViewer.current.changeHostElement(node, true, true)
+ }, [])
+
+ // Run only on first render to initialize the viewer. See the viewer
+ // documentation for details on the meaning of different options:
+ // https://lauri-codes.github.io/materia/viewers/structureviewer
+ useEffect(() => {
+ let viewerOptions
+ if (options === undefined) {
+ viewerOptions = {
+ view: {
+ autoResize: true,
+ autoFit: true,
+ fitMargin: 0.5
+ },
+ bonds: {
+ enabled: true
+ },
+ latticeConstants: {
+ size: 0.7,
+ font: 'Titillium Web,sans-serif',
+ a: {color: '#f44336'},
+ b: {color: '#4caf50'},
+ c: {color: '#5c6bc0'}
+ },
+ controls: {
+ enableZoom: true,
+ enablePan: true,
+ enableRotate: true
+ },
+ renderer: {
+ backgroundColor: ['#ffffff', 1],
+ shadows: {
+ enabled: false
+ }
+ }
+ }
+ } else {
+ viewerOptions = options
+ }
+
+ if (viewer === undefined) {
+ refViewer.current = new BrillouinZoneViewer(undefined, viewerOptions)
+ } else {
+ refViewer.current = viewer
+ refViewer.current.setOptions(viewerOptions, false, false)
+ }
+ if (refCanvas.current !== null) {
+ refViewer.current.changeHostElement(refCanvas.current, false, false)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ // Called only on first render to load the given structure.
+ useEffect(() => {
+ if (system === undefined) {
+ return
+ }
+
+ if (positionsOnly) {
+ refViewer.current.setPositions(system.positions)
+ return
+ }
+
+ let nAtoms = system.species.length
+ if (nAtoms >= sizeLimit) {
+ setError('Visualization is disabled due to large system size.')
+ return
+ }
+
+ // Systems with cell are centered on the cell center and orientation is defined
+ // by the cell vectors.
+ let cell = system.cell
+ if (cell !== undefined) {
+ refViewer.current.setOptions({layout: {
+ viewCenter: 'COC',
+ viewRotation: {
+ align: {
+ top: 'c',
+ right: 'b'
+ },
+ rotations: [
+ [0, 1, 0, 60],
+ [1, 0, 0, 30]
+ ]
+ }
+ }})
+ // Systems without cell are centered on the center of positions
+ } else {
+ refViewer.current.setOptions({layout: {
+ viewCenter: 'COP',
+ viewRotation: {
+ rotations: [
+ [0, 1, 0, 60],
+ [1, 0, 0, 30]
+ ]
+ }
+ }})
+ }
+ refViewer.current.load(system)
+ refViewer.current.fitToCanvas()
+ refViewer.current.saveReset()
+ refViewer.current.reset()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ // Viewer settings
+ useEffect(() => {
+ refViewer.current.setOptions({bonds: {enabled: showBonds}})
+ }, [showBonds])
+
+ useEffect(() => {
+ refViewer.current.setOptions({latticeConstants: {enabled: showLatticeConstants}})
+ }, [showLatticeConstants])
+
+ useEffect(() => {
+ refViewer.current.setOptions({cell: {enabled: showCell}})
+ }, [showCell])
+
+ // Memoized callbacks
+ const openMenu = useCallback((event) => {
+ setAnchorEl(event.currentTarget)
+ }, [])
+
+ const closeMenu = useCallback(() => {
+ setAnchorEl(null)
+ }, [])
+
+ const toggleFullscreen = useCallback(() => {
+ setFullscreen(!fullscreen)
+ }, [fullscreen])
+
+ const takeScreencapture = useCallback(() => {
+ refViewer.current.takeScreenShot(captureName)
+ }, [captureName])
+
+ const handleReset = useCallback(() => {
+ refViewer.current.reset()
+ refViewer.current.fitToCanvas()
+ refViewer.current.render()
+ }, [])
+
+ const content = <Box className={style.container}>
+ <div className={style.header}>
+ {fullscreen && <Typography variant="h6">Structure</Typography>}
+ <div className={style.spacer}></div>
+ <Tooltip title="Reset view">
+ <IconButton className={style.iconButton} onClick={handleReset} disabled={error}>
+ <Replay />
+ </IconButton>
+ </Tooltip>
+ <Tooltip
+ title="Toggle fullscreen">
+ <IconButton className={style.iconButton} onClick={toggleFullscreen} disabled={error}>
+ {fullscreen ? <FullscreenExit /> : <Fullscreen />}
+ </IconButton>
+ </Tooltip>
+ <Tooltip title="Capture image">
+ <IconButton className={style.iconButton} onClick={takeScreencapture} disabled={error}>
+ <CameraAlt />
+ </IconButton>
+ </Tooltip>
+ <Tooltip title="Options">
+ <IconButton className={style.iconButton} onClick={openMenu} disabled={error}>
+ <MoreVert />
+ </IconButton>
+ </Tooltip>
+ <Menu
+ id='settings-menu'
+ anchorEl={anchorEl}
+ getContentAnchorEl={null}
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
+ transformOrigin={{ vertical: 'top', horizontal: 'right' }}
+ keepMounted
+ open={open}
+ onClose={closeMenu}
+ >
+ <MenuItem key='show-bonds'>
+ <FormControlLabel
+ control={
+ <Checkbox
+ checked={showBonds}
+ onChange={(event) => { setShowBonds(!showBonds) }}
+ color='primary'
+ />
+ }
+ label='Show bonds'
+ />
+ </MenuItem>
+ <MenuItem key='show-axis'>
+ <FormControlLabel
+ control={
+ <Checkbox
+ checked={showLatticeConstants}
+ onChange={(event) => { setShowLatticeConstants(!showLatticeConstants) }}
+ color='primary'
+ />
+ }
+ label='Show lattice constants'
+ />
+ </MenuItem>
+ <MenuItem key='show-cell'>
+ <FormControlLabel
+ control={
+ <Checkbox
+ checked={showCell}
+ onChange={(event) => { setShowCell(!showCell) }}
+ color='primary'
+ />
+ }
+ label='Show simulation cell'
+ />
+ </MenuItem>
+ </Menu>
+ </div>
+ <div className={style.viewerCanvas} ref={measuredRef}></div>
+ <div className={style.errorContainer}><div className={style.errorMessage}>{error}</div></div>
+ </Box>
+
+ return (
+ <Box className={clsx(style.root, className)} >
+ <Floatable float={fullscreen} onFloat={toggleFullscreen} aspectRatio={aspectRatio}>
+ {content}
+ </Floatable>
+ </Box>
+ )
+}
+
+BrillouinZone.propTypes = {
+ viewer: PropTypes.object, // Optional shared viewer instance.
+ system: PropTypes.object, // The system to display as section_system
+ options: PropTypes.object, // Viewer options
+ captureName: PropTypes.string, // Name of the file that the user can download
+ aspectRatio: PropTypes.number, // Fixed aspect ratio for the viewer canvas
+ sizeLimit: PropTypes.number, // Maximum number of atoms to attempt to display
+ positionsOnly: PropTypes.bool, // Whether to update only positions. This is much faster than loading the entire structure.
+ classes: PropTypes.object,
+ className: PropTypes.string
+}
+BrillouinZone.defaultProps = {
+ aspectRatio: 4 / 3,
+ captureName: 'structure',
+ sizeLimit: 300
+}
diff --git a/gui/src/components/visualization/DOS.js b/gui/src/components/visualization/DOS.js
new file mode 100644
index 0000000000..ae8a2aaaeb
--- /dev/null
+++ b/gui/src/components/visualization/DOS.js
@@ -0,0 +1,127 @@
+import React, {useState, useEffect, useMemo} from 'react'
+import PropTypes from 'prop-types'
+import { makeStyles, useTheme } from '@material-ui/core/styles'
+import clsx from 'clsx'
+import {
+ Box
+} from '@material-ui/core'
+import Plot from '../visualization/Plot'
+import { convert, mergeObjects } from '../../utils'
+
+export default function DOS({data, layout, aspectRatio, className, classes, onRelayout, onAfterPlot, onRedraw, onRelayouting}) {
+ const [finalData, setFinalData] = useState(undefined)
+
+ // Merge custom layout with default layout
+ const tmpLayout = useMemo(() => {
+ let defaultLayout = {
+ yaxis: {
+ title: {
+ text: 'Energy (eV)'
+ }
+ }
+ }
+ return mergeObjects(layout, defaultLayout)
+ }, [layout])
+
+ // Styles
+ const useStyles = makeStyles(
+ {
+ root: {
+ }
+ }
+ )
+ const style = useStyles(classes)
+ const theme = useTheme()
+
+ // The plotted data is loaded only after the first render as a side effect to
+ // avoid freezing the UI
+ useEffect(() => {
+ if (data === undefined) {
+ return
+ }
+ const norm = data.dos_energies_normalized === undefined ? '' : '_normalized'
+ const energyName = 'dos_energies' + norm
+ const valueName = 'dos_values' + norm
+ const plotData = []
+ if (data !== undefined) {
+ let nChannels = data[valueName].length
+ let energies = convert(data[energyName], 'joule', 'eV')
+ if (nChannels === 2) {
+ plotData.push(
+ {
+ x: data[valueName][1],
+ y: energies,
+ type: 'scatter',
+ mode: 'lines',
+ line: {
+ color: theme.palette.secondary.main,
+ width: 2
+ }
+ }
+ )
+ }
+ plotData.push(
+ {
+ x: data[valueName][0],
+ y: energies,
+ type: 'scatter',
+ mode: 'lines',
+ line: {
+ color: theme.palette.primary.main,
+ width: 2
+ }
+ }
+ )
+ }
+ setFinalData(plotData)
+ }, [data, theme.palette.primary.main, theme.palette.secondary.main])
+
+ // Compute layout that depends on data.
+ const computedLayout = useMemo(() => {
+ if (data === undefined) {
+ return {}
+ }
+ const norm = data.dos_energies_normalized !== undefined
+ let defaultLayout = {
+ xaxis: {
+ title: {
+ text: norm ? 'states/eV/m<sup>3</sup>/atom' : 'states/eV/cell'
+ }
+ }
+ }
+ return defaultLayout
+ }, [data])
+
+ // Merge the given layout and layout computed from data
+ const finalLayout = useMemo(() => {
+ return mergeObjects(computedLayout, tmpLayout)
+ }, [computedLayout, tmpLayout])
+
+ return (
+ <Box className={clsx(style.root, className)}>
+ <Plot
+ data={finalData}
+ layout={finalLayout}
+ aspectRatio={aspectRatio}
+ floatTitle="Density of states"
+ onRelayout={onRelayout}
+ onAfterPlot={onAfterPlot}
+ onRedraw={onRedraw}
+ onRelayouting={onRelayouting}
+ >
+ </Plot>
+ </Box>
+ )
+}
+
+DOS.propTypes = {
+ data: PropTypes.object, // section_dos
+ layout: PropTypes.object,
+ aspectRatio: PropTypes.number,
+ classes: PropTypes.object,
+ className: PropTypes.string,
+ onAfterPlot: PropTypes.func,
+ onRedraw: PropTypes.func,
+ onRelayout: PropTypes.func,
+ onRelayouting: PropTypes.func
+}
diff --git a/gui/src/components/visualization/Floatable.js b/gui/src/components/visualization/Floatable.js
new file mode 100644
index 0000000000..3e04403dd1
--- /dev/null
+++ b/gui/src/components/visualization/Floatable.js
@@ -0,0 +1,140 @@
+import React from 'react'
+import { makeStyles } from '@material-ui/core/styles'
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogContent,
+ DialogActions
+} from '@material-ui/core'
+import PropTypes from 'prop-types'
+import clsx from 'clsx'
+
+/**
+ * Component that wraps it's children in a container that can be 'floated',
+ * i.e. displayed on an html element that is positioned relative to the
+ * viewport and is above all other elements.
+ */
+export default function Floatable({className, classes, float, children, aspectRatio, onFloat}) {
+ // Styles
+ const useStyles = makeStyles((theme) => {
+ // Calculate the fullscreen size
+ const actionsHeight = 52.5 // Size of the actions element that is shown when floating
+ const padding = 20 // Padding arount the floating object
+ const maxWidth = 1280 // Maximum width for the floating window
+ const margin = 0.1 * (window.innerHeight) + actionsHeight
+ const windowHeight = window.innerHeight - margin
+ const windowWidth = Math.min(window.innerWidth, maxWidth) - margin
+ const windowRatio = windowWidth / windowHeight
+ let width
+ let height
+ if (windowRatio > aspectRatio) {
+ width = (windowHeight) * aspectRatio + 2 * padding + 'px'
+ height = windowHeight + actionsHeight + 2 * padding
+ } else {
+ width = windowWidth + 2 * padding
+ height = (windowWidth) / aspectRatio + actionsHeight + 2 * padding + 'px'
+ }
+
+ return {
+ root: {
+ },
+ containerOuter: {
+ width: '100%',
+ height: 0,
+ paddingBottom: `${100 / aspectRatio}%`, // CSS hack for fixed aspect ratio
+ position: 'relative',
+ boxSizing: 'border-box'
+ },
+ containerInner: {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ display: 'flex',
+ flexDirection: 'column',
+ boxSizing: 'border-box'
+ },
+ dialogContent: {
+ boxSizing: 'border-box',
+ padding: padding
+ },
+ 'dialogContent:first-child': {
+ paddingTop: `${padding} !important`
+ },
+ dialogActions: {
+ height: actionsHeight,
+ boxSizing: 'border-box'
+ },
+ dialogRoot: {
+ width: width,
+ height: height,
+ margin: 0,
+ padding: 0,
+ maxWidth: 'none',
+ maxHeight: 'none',
+ boxSizing: 'border-box'
+ }
+ }
+ })
+ const style = useStyles(classes)
+
+ return (
+ <Box className={clsx(style.root, className)}>
+ <Box className={style.containerOuter}>
+ <Box className={style.containerInner}>
+ {float ? '' : children}
+ </Box>
+ </Box>
+ <Dialog fullWidth={false} maxWidth={false} open={float}
+ classes={{paper: style.dialogRoot}}
+ >
+ <DialogContent className={[style.dialogContent, style['dialogContent:first-child']].join('_')}>
+ <Box className={style.containerOuter}>
+ <Box className={style.containerInner}>
+ {float ? children : ''}
+ </Box>
+ </Box>
+ </DialogContent>
+ <DialogActions className={style.dialogActions}>
+ <Button onClick={() => onFloat(float)}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ </Box>
+ )
+}
+
+Floatable.propTypes = {
+ float: PropTypes.bool.isRequired,
+ /**
+ * Fixed aspect ratio that is enforced for this component.
+ */
+ aspectRatio: PropTypes.number.isRequired,
+ /**
+ * Callback that is called whenever this component requests a change in the
+ * float property. The callback accepts one parameter: 'float' that is a
+ * boolean indicating the current float status.
+ */
+ onFloat: PropTypes.any,
+ /**
+ * Child components
+ */
+ children: PropTypes.any,
+ /**
+ * CSS class for the root element.
+ */
+ className: PropTypes.string,
+ /**
+ * CSS classes for this component.
+ */
+ classes: PropTypes.object
+ /**
+ * Controls the float status.
+ */
+}
+Floatable.defaultProps = {
+ float: false
+}
diff --git a/gui/src/components/visualization/Plot.js b/gui/src/components/visualization/Plot.js
new file mode 100644
index 0000000000..1800e34f57
--- /dev/null
+++ b/gui/src/components/visualization/Plot.js
@@ -0,0 +1,275 @@
+import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'
+import PropTypes from 'prop-types'
+import { makeStyles } from '@material-ui/core/styles'
+import { cloneDeep } from 'lodash'
+
+import {
+ IconButton,
+ Tooltip,
+ Typography
+} from '@material-ui/core'
+import {
+ MoreVert,
+ Fullscreen,
+ FullscreenExit,
+ CameraAlt,
+ Replay
+} from '@material-ui/icons'
+import Floatable from './Floatable'
+import Plotly from 'plotly.js-cartesian-dist-min'
+import clsx from 'clsx'
+import { mergeObjects } from '../../utils'
+
+export default function Plot({data, layout, config, menu, floatTitle, capture, aspectRatio, className, classes, onRelayout, onAfterPlot, onRedraw, onRelayouting}) {
+ // States
+ const [float, setFloat] = useState(false)
+ const [initialLayout, setInitialLayout] = useState()
+ const [captureSettings, setCaptureSettings] = useState()
+ const firstUpdate = useRef(true)
+ const dataInitialized = useRef(false)
+
+ React.useEffect(() => {
+ let defaultCapture = {
+ format: 'svg',
+ width: 1280,
+ height: 960 / aspectRatio,
+ filename: 'plot'
+ }
+ let settings = mergeObjects(capture, defaultCapture)
+ setCaptureSettings(settings)
+ }, [capture, aspectRatio])
+
+ // Styles
+ const useStyles = makeStyles((theme) => {
+ return {
+ header: {
+ paddingRight: 20,
+ display: 'flex',
+ flexDirection: 'row',
+ zIndex: 1
+ },
+ root: {
+ },
+ spacer: {
+ flex: 1
+ },
+ iconButton: {
+ backgroundColor: 'white'
+ }
+ }
+ })
+
+ const style = useStyles(classes)
+
+ // Set the final menu
+ const finalMenu = useMemo(() => {
+ let defaultMenu = {
+ reset: {
+ visible: true,
+ disabled: false
+ },
+ fullscreen: {
+ visible: true,
+ disabled: false
+ },
+ capture: {
+ visible: true,
+ disabled: false
+ },
+ dropdown: {
+ visible: false,
+ disabled: false,
+ items: undefined
+ }
+ }
+ return mergeObjects(menu, defaultMenu)
+ }, [menu])
+
+ // Set the final layout
+ const finalLayout = useMemo(() => {
+ let defaultLayout = {
+ dragmode: 'pan',
+ hovermode: false,
+ showlegend: false,
+ autosize: true,
+ margin: {
+ l: 60,
+ r: 20,
+ t: 20,
+ b: 50
+ },
+ xaxis: {
+ linecolor: '#333',
+ linewidth: 1,
+ mirror: true,
+ ticks: 'outside',
+ showline: true,
+ fixedrange: true,
+ title: {
+ font: {
+ family: 'Titillium Web,sans-serif',
+ size: 16,
+ color: '#333'
+ },
+ tickfont: {
+ family: 'Titillium Web,sans-serif',
+ size: 14,
+ color: '#333'
+ }
+ }
+ },
+ yaxis: {
+ linecolor: '#333',
+ linewidth: 1,
+ mirror: true,
+ ticks: 'outside',
+ showline: true,
+ title: {
+ font: {
+ family: 'Titillium Web,sans-serif',
+ size: 16,
+ color: '#333'
+ }
+ },
+ tickfont: {
+ family: 'Titillium Web,sans-serif',
+ size: 14,
+ color: '#333'
+ }
+ }
+ }
+ return mergeObjects(layout, defaultLayout)
+ }, [layout])
+
+ // Set the final config
+ const finalConfig = useMemo(() => {
+ let defaultConfig = {
+ scrollZoom: true,
+ displayModeBar: false
+ }
+ return mergeObjects(config, defaultConfig)
+ }, [config])
+
+ // Initialize the plot object on first render
+ useEffect(() => {
+ Plotly.newPlot(canvasRef.current, data, finalLayout, finalConfig)
+ if (firstUpdate.current) {
+ firstUpdate.current = false
+ }
+ if (data) {
+ setInitialLayout(cloneDeep(finalLayout))
+ dataInitialized.current = true
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ // 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 => {
+ canvasRef.current = node
+ if (node === null) {
+ return
+ }
+ Plotly.react(canvasRef.current, data, finalLayout, finalConfig)
+ }, [data, finalLayout, finalConfig])
+
+ // Update data
+ useEffect(() => {
+ if (firstUpdate.current) {
+ return
+ }
+ Plotly.react(canvasRef.current, data, finalLayout, finalConfig)
+ if (data) {
+ if (!dataInitialized.current) {
+ setInitialLayout(cloneDeep(finalLayout))
+ dataInitialized.current = true
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [data, finalConfig])
+
+ // Update layout
+ useEffect(() => {
+ if (firstUpdate.current) {
+ return
+ }
+ Plotly.relayout(canvasRef.current, finalLayout)
+ }, [canvasRef, finalLayout])
+
+ // For resetting the view. We need to pass a deep clone of the initialLayout,
+ // as Plotly will modify the given object in-place
+ const handleReset = useCallback(() => {
+ if (initialLayout) {
+ Plotly.relayout(canvasRef.current, cloneDeep(initialLayout))
+ }
+ }, [canvasRef, initialLayout])
+
+ // Handles plot capturing
+ const handleCapture = useCallback(() => {
+ Plotly.downloadImage(canvasRef.current, captureSettings)
+ }, [canvasRef, captureSettings])
+
+ return (
+ <Floatable className={clsx(className, style.root)} float={float} onFloat={() => setFloat(!float)} aspectRatio={aspectRatio}>
+ <div className={style.header}>
+ {float && <Typography variant="h6">{floatTitle}</Typography>}
+ <div className={style.spacer}></div>
+ { finalMenu.reset.visible === true
+ ? <Tooltip title="Reset view">
+ <IconButton className={style.iconButton} onClick={handleReset} disabled={finalMenu.reset.disabled}> <Replay />
+ </IconButton>
+ </Tooltip>
+ : ''
+ }
+ { finalMenu.fullscreen.visible === true
+ ? <Tooltip
+ title="Toggle fullscreen">
+ <IconButton className={style.iconButton} onClick={() => setFloat(!float)} disabled={finalMenu.fullscreen.disabled}>
+ {float ? <FullscreenExit /> : <Fullscreen />}
+ </IconButton>
+ </Tooltip>
+ : ''
+ }
+ { finalMenu.capture.visible === true
+ ? <Tooltip title="Capture image">
+ <IconButton className={style.iconButton} onClick={handleCapture} disabled={finalMenu.capture.disabled}>
+ <CameraAlt />
+ </IconButton>
+ </Tooltip>
+ : ''
+ }
+ { finalMenu.dropdown.visible === true
+ ? <Tooltip title="Options">
+ <IconButton className={style.iconButton} onClick={() => {}} disabled={finalMenu.dropdown.disabled}>
+ <MoreVert />
+ </IconButton>
+ </Tooltip>
+ : ''
+ }
+ </div>
+ <div ref={canvasRef} style={{width: '100%', height: '100%'}}></div>
+ </Floatable>
+ )
+}
+
+Plot.propTypes = {
+ data: PropTypes.array, // Plotly.js data object
+ layout: PropTypes.object, // Plotly.js layout object
+ config: PropTypes.object, // Plotly.js config object
+ menu: PropTypes.object, // Menu settings
+ capture: PropTypes.object, // Capture settings
+ aspectRatio: PropTypes.number, // Fixed aspect ratio for the viewer canvas
+ className: PropTypes.string,
+ classes: PropTypes.string,
+ floatTitle: PropTypes.string, // The title of the plot shown in floating mode
+ onRelayout: PropTypes.func,
+ onAfterPlot: PropTypes.func,
+ onRedraw: PropTypes.func,
+ onRelayouting: PropTypes.func
+}
+Plot.defaultProps = {
+ aspectRatio: 9 / 16,
+ floatTitle: ''
+}
diff --git a/gui/src/components/visualization/Structure.js b/gui/src/components/visualization/Structure.js
index cfa8ffcb59..a756e7547a 100644
--- a/gui/src/components/visualization/Structure.js
+++ b/gui/src/components/visualization/Structure.js
@@ -6,13 +6,9 @@ import {
Checkbox,
Menu,
MenuItem,
- Button,
IconButton,
Tooltip,
Typography,
- Dialog,
- DialogContent,
- DialogActions,
FormControlLabel
} from '@material-ui/core'
import {
@@ -23,8 +19,9 @@ import {
Replay
} from '@material-ui/icons'
import { StructureViewer } from '@lauri-codes/materia'
+import Floatable from './Floatable'
-export default function Structure(props) {
+export default function Structure({className, classes, system, options, viewer, captureName, aspectRatio, positionsOnly, sizeLimit}) {
// States
const [anchorEl, setAnchorEl] = React.useState(null)
const [fullscreen, setFullscreen] = useState(false)
@@ -35,25 +32,12 @@ export default function Structure(props) {
// Variables
const open = Boolean(anchorEl)
- const viewer = useRef(null)
+ const refViewer = useRef(null)
const refCanvas = useRef(null)
// Styles
const useStyles = makeStyles((theme) => {
return {
- root: {
- width: '100%',
- height: 0,
- paddingBottom: `${(1 / props.aspectRatio) * 100}%`, // CSS hack for fixed aspect ratio
- position: 'relative'
- },
- rootInner: {
- position: 'absolute',
- top: 0,
- right: 0,
- bottom: 0,
- left: 0
- },
container: {
display: 'flex',
width: '100%',
@@ -95,7 +79,7 @@ export default function Structure(props) {
}
}
})
- const classes = useStyles(props)
+ const style = useStyles(classes)
// 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
@@ -106,19 +90,19 @@ export default function Structure(props) {
if (node === null) {
return
}
- if (viewer.current === null) {
+ if (refViewer.current === null) {
return
}
- viewer.current.changeHostElement(node, true, true)
+ refViewer.current.changeHostElement(node, true, true)
}, [])
// Run only on first render to initialize the viewer. See the viewer
// documentation for details on the meaning of different options:
// https://lauri-codes.github.io/materia/viewers/structureviewer
useEffect(() => {
- let options
- if (props.options === undefined) {
- options = {
+ let viewerOptions
+ if (options === undefined) {
+ viewerOptions = {
view: {
autoResize: true,
autoFit: true,
@@ -147,42 +131,42 @@ export default function Structure(props) {
}
}
} else {
- options = props.options
+ viewerOptions = options
}
- if (props.viewer === undefined) {
- viewer.current = new StructureViewer(undefined, options)
+ if (viewer === undefined) {
+ refViewer.current = new StructureViewer(undefined, viewerOptions)
} else {
- viewer.current = props.viewer
- viewer.current.setOptions(options, false, false)
+ refViewer.current = viewer
+ refViewer.current.setOptions(options, false, false)
}
if (refCanvas.current !== null) {
- viewer.current.changeHostElement(refCanvas.current, false, false)
+ refViewer.current.changeHostElement(refCanvas.current, false, false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Called only on first render to load the given structure.
useEffect(() => {
- if (props.system === undefined) {
+ if (system === undefined) {
return
}
- if (props.positionsOnly) {
- viewer.current.setPositions(props.system.positions)
+ if (positionsOnly) {
+ refViewer.current.setPositions(system.positions)
return
}
- let nAtoms = props.system.species.length
- if (nAtoms >= props.sizeLimit) {
+ let nAtoms = system.species.length
+ if (nAtoms >= sizeLimit) {
setError('Visualization is disabled due to large system size.')
return
}
// Systems with cell are centered on the cell center and orientation is defined
// by the cell vectors.
- let cell = props.system.cell
+ let cell = system.cell
if (cell !== undefined) {
- viewer.current.setOptions({layout: {
+ refViewer.current.setOptions({layout: {
viewCenter: 'COC',
viewRotation: {
align: {
@@ -197,7 +181,7 @@ export default function Structure(props) {
}})
// Systems without cell are centered on the center of positions
} else {
- viewer.current.setOptions({layout: {
+ refViewer.current.setOptions({layout: {
viewCenter: 'COP',
viewRotation: {
rotations: [
@@ -207,24 +191,24 @@ export default function Structure(props) {
}
}})
}
- viewer.current.load(props.system)
- viewer.current.fitToCanvas()
- viewer.current.saveReset()
- viewer.current.reset()
+ refViewer.current.load(system)
+ refViewer.current.fitToCanvas()
+ refViewer.current.saveReset()
+ refViewer.current.reset()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Viewer settings
useEffect(() => {
- viewer.current.setOptions({bonds: {enabled: showBonds}})
+ refViewer.current.setOptions({bonds: {enabled: showBonds}})
}, [showBonds])
useEffect(() => {
- viewer.current.setOptions({latticeConstants: {enabled: showLatticeConstants}})
+ refViewer.current.setOptions({latticeConstants: {enabled: showLatticeConstants}})
}, [showLatticeConstants])
useEffect(() => {
- viewer.current.setOptions({cell: {enabled: showCell}})
+ refViewer.current.setOptions({cell: {enabled: showCell}})
}, [showCell])
// Memoized callbacks
@@ -241,37 +225,37 @@ export default function Structure(props) {
}, [fullscreen])
const takeScreencapture = useCallback(() => {
- viewer.current.takeScreenShot(props.captureName)
- }, [props.captureName])
+ refViewer.current.takeScreenShot(captureName)
+ }, [captureName])
const handleReset = useCallback(() => {
- viewer.current.reset()
- viewer.current.fitToCanvas()
- viewer.current.render()
+ refViewer.current.reset()
+ refViewer.current.fitToCanvas()
+ refViewer.current.render()
}, [])
- const content = <Box className={classes.container}>
- <div className={classes.header}>
+ const content = <Box className={style.container}>
+ <div className={style.header}>
{fullscreen && <Typography variant="h6">Structure</Typography>}
- <div className={classes.spacer}></div>
+ <div className={style.spacer}></div>
<Tooltip title="Reset view">
- <IconButton className={classes.iconButton} onClick={handleReset} disabled={error}>
+ <IconButton className={style.iconButton} onClick={handleReset} disabled={error}>
<Replay />
</IconButton>
</Tooltip>
<Tooltip
title="Toggle fullscreen">
- <IconButton className={classes.iconButton} onClick={toggleFullscreen} disabled={error}>
+ <IconButton className={style.iconButton} onClick={toggleFullscreen} disabled={error}>
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Tooltip>
<Tooltip title="Capture image">
- <IconButton className={classes.iconButton} onClick={takeScreencapture} disabled={error}>
+ <IconButton className={style.iconButton} onClick={takeScreencapture} disabled={error}>
<CameraAlt />
</IconButton>
</Tooltip>
<Tooltip title="Options">
- <IconButton className={classes.iconButton} onClick={openMenu} disabled={error}>
+ <IconButton className={style.iconButton} onClick={openMenu} disabled={error}>
<MoreVert />
</IconButton>
</Tooltip>
@@ -323,34 +307,20 @@ export default function Structure(props) {
</MenuItem>
</Menu>
</div>
- <div className={classes.viewerCanvas} ref={measuredRef}></div>
- <div className={classes.errorContainer}><div className={classes.errorMessage}>{error}</div></div>
+ <div className={style.viewerCanvas} ref={measuredRef}></div>
+ <div className={style.errorContainer}><div className={style.errorMessage}>{error}</div></div>
</Box>
return (
- <Box className={classes.root}>
- <Box className={classes.rootInner}>
- {fullscreen ? '' : content}
- </Box>
- <Dialog maxWidth="lg" fullWidth open={fullscreen}>
- <DialogContent>
- <Box className={classes.root}>
- <Box className={classes.rootInner}>
- {fullscreen ? content : ''}
- </Box>
- </Box>
- </DialogContent>
- <DialogActions>
- <Button onClick={() => setFullscreen(false)}>
- Close
- </Button>
- </DialogActions>
- </Dialog>
- </Box>
+ <Floatable float={fullscreen} onFloat={toggleFullscreen} aspectRatio={aspectRatio}>
+ {content}
+ </Floatable>
)
}
Structure.propTypes = {
+ className: PropTypes.string,
+ classes: PropTypes.object,
viewer: PropTypes.object, // Optional shared viewer instance.
system: PropTypes.object, // The system to display as section_system
options: PropTypes.object, // Viewer options
diff --git a/gui/src/utils.js b/gui/src/utils.js
index 0e0212e72b..25c3bcb68c 100644
--- a/gui/src/utils.js
+++ b/gui/src/utils.js
@@ -1,4 +1,5 @@
import { unit } from 'mathjs'
+import { cloneDeep, merge } from 'lodash'
export const isEquivalent = (a, b) => {
// Create arrays of property names
@@ -37,7 +38,7 @@ export const capitalize = (s) => {
* Used to convert numeric values from one unit to another. Works on
* n-dimensional arrays and implemented as a relatively simple for loop for
* performance. If conversion times become an issue, it might be worthwhile to
- * look at vectorization with SIMD.
+ * look at vectorization with WebAssembly.
*
* @param {*} value The values to convert
* @param {*} from Original unit.
@@ -75,6 +76,38 @@ export function convert(value, from, to) {
return newValue
}
+/**
+ * Used to calculate the distance between two n-dimensional points,
+ *
+ * @param {*} a First point
+ * @param {*} b Second point
+ *
+ * @return {*} Euclidean distance between the given two points.
+ */
+export function distance(a, b) {
+ return a
+ .map((x, i) => Math.abs(x - b[i]) ** 2) // square the difference
+ .reduce((sum, now) => sum + now) ** // sum
+ (1 / 2)
+}
+
+/**
+ * Used to merge two Javascript objects into a new third object by recursively
+ * overwriting and extending the target object with properties from the source
+ * object.
+ *
+ * @param {*} target The values to convert
+ * @param {*} source Original unit.
+ *
+ * @return {*} A copy of the original data with units converted.
+ */
+export function mergeObjects(source, target, copy = false) {
+ // First create a deep clone that will be used as the returned object
+ let cloned = cloneDeep(target)
+ let val = merge(cloned, source)
+ return val
+}
+
export function arraysEqual(_arr1, _arr2) {
if (!Array.isArray(_arr1) || !Array.isArray(_arr2) || _arr1.length !== _arr2.length) {
return false
diff --git a/gui/yarn.lock b/gui/yarn.lock
index 42128dd226..50868f94c6 100644
--- a/gui/yarn.lock
+++ b/gui/yarn.lock
@@ -9041,6 +9041,11 @@ pkg-up@^2.0.0:
dependencies:
find-up "^2.1.0"
+plotly.js-cartesian-dist-min@^1.54.7:
+ version "1.54.7"
+ resolved "https://registry.yarnpkg.com/plotly.js-cartesian-dist-min/-/plotly.js-cartesian-dist-min-1.54.7.tgz#f44fc89a3221d89486efe8d25a19efb5a35156d1"
+ integrity sha512-8aCk2HQ9rYQZp0ivpkq1wHtByAiPEPBFIw+jMC5elV3/WLXuZvNjwRqnXECLMQiuEilEsEZhmWisp1/QvQ06JA==
+
pn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
diff --git a/nomad/app/api/encyclopedia.py b/nomad/app/api/encyclopedia.py
index 48863244fe..270954d826 100644
--- a/nomad/app/api/encyclopedia.py
+++ b/nomad/app/api/encyclopedia.py
@@ -141,7 +141,7 @@ material_result = api.model("material_result", {
class EncMaterialResource(Resource):
@api.response(404, "The material does not exist")
@api.response(200, "Metadata send", fields.Raw)
- @api.doc("material/<material_id>")
+ @api.doc("get_material")
@api.expect(material_query)
@api.marshal_with(material_result, skip_none=True)
@authenticate()
@@ -860,7 +860,7 @@ class EncCalculationsResource(Resource):
@api.response(404, "Suggestion not found")
@api.response(400, "Bad request")
@api.response(200, "Metadata send", fields.Raw)
- @api.doc("enc_calculations")
+ @api.doc("get_calculations")
@authenticate()
def get(self, material_id):
"""Used to return all calculations related to the given material. Also
@@ -1205,7 +1205,7 @@ class EncCalculationResource(Resource):
@api.response(200, "Metadata send", fields.Raw)
@api.expect(calculation_property_query, validate=False)
@api.marshal_with(calculation_property_result, skip_none=True)
- @api.doc("enc_calculation")
+ @api.doc("get_calculation")
@authenticate()
def post(self, material_id, calc_id):
"""Used to return calculation details. Some properties are not
--
GitLab