Commit a133d368 authored by Lauri Himanen's avatar Lauri Himanen Committed by Markus Scheidgen
Browse files

Added new components for plotting: Plot, DOS and BandStructure. The DOS and BS...

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.
parent 5b9fc49c
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
"pace": "^0.0.4", "pace": "^0.0.4",
"pace-js": "^1.0.2", "pace-js": "^1.0.2",
"piwik-react-router": "^0.12.1", "piwik-react-router": "^0.12.1",
"plotly.js-cartesian-dist-min": "^1.54.7",
"qs": "^6.8.0", "qs": "^6.8.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-app-polyfill": "^1.0.1", "react-app-polyfill": "^1.0.1",
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
window.paceOptions = { window.paceOptions = {
restartOnPushState: true 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>
<script src="https://unpkg.com/pace-js@1.0.2/pace.min.js"></script> <script src="https://unpkg.com/pace-js@1.0.2/pace.min.js"></script>
<link href="%PUBLIC_URL%/pace.css" rel="stylesheet" /> <link href="%PUBLIC_URL%/pace.css" rel="stylesheet" />
...@@ -38,7 +41,6 @@ ...@@ -38,7 +41,6 @@
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> <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> <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<title>NOMAD</title> <title>NOMAD</title>
</head> </head>
<body> <body>
......
...@@ -299,6 +299,86 @@ class Api { ...@@ -299,6 +299,86 @@ class Api {
.finally(this.onFinishLoading) .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) { async calcProcLog(uploadId, calcId) {
this.onStartLoading() this.onStartLoading()
return this.swagger() return this.swagger()
......
...@@ -10,6 +10,8 @@ import { resolveRef, rootSections } from './metainfo' ...@@ -10,6 +10,8 @@ import { resolveRef, rootSections } from './metainfo'
import { Title, metainfoAdaptorFactory, DefinitionLabel } from './MetainfoBrowser' import { Title, metainfoAdaptorFactory, DefinitionLabel } from './MetainfoBrowser'
import { Matrix, Number } from './visualizations' import { Matrix, Number } from './visualizations'
import Structure from '../visualization/Structure' import Structure from '../visualization/Structure'
import BandStructure from '../visualization/BandStructure'
import DOS from '../visualization/DOS'
import { StructureViewer } from '@lauri-codes/materia' import { StructureViewer } from '@lauri-codes/materia'
import Markdown from '../Markdown' import Markdown from '../Markdown'
import { convert } from '../../utils' import { convert } from '../../utils'
...@@ -305,6 +307,25 @@ QuantityValue.propTypes = ({ ...@@ -305,6 +307,25 @@ QuantityValue.propTypes = ({
* title. * title.
*/ */
function Overview({section, def}) { 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 // Structure visualization for section_system
if (def.name === 'section_system') { if (def.name === 'section_system') {
let url = window.location.href let url = window.location.href
...@@ -338,7 +359,25 @@ function Overview({section, def}) { ...@@ -338,7 +359,25 @@ function Overview({section, def}) {
visualizedSystem.sectionPath = sectionPath visualizedSystem.sectionPath = sectionPath
visualizedSystem.index = index 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 return null
} }
......
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)
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
...@@ -75,6 +75,11 @@ export default function MaterialsList(props) { ...@@ -75,6 +75,11 @@ export default function MaterialsList(props) {
paginationText = `1-${results.length.toLocaleString()} of ${(total || 0).toLocaleString()}` 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}}> const pagination = <TableCell colSpan={1000} classes={{root: classes.scrollCell}}>
<Toolbar className={classes.scrollBar}> <Toolbar className={classes.scrollBar}>
<span className={classes.scrollSpacer}>&nbsp;</span> <span className={classes.scrollSpacer}>&nbsp;</span>
...@@ -92,6 +97,9 @@ export default function MaterialsList(props) { ...@@ -92,6 +97,9 @@ export default function MaterialsList(props) {
<IconButton href={`${appBase}/encyclopedia/#/material/${entry.encyclopedia.material.material_id}`}> <IconButton href={`${appBase}/encyclopedia/#/material/${entry.encyclopedia.material.material_id}`}>
<DetailsIcon /> <DetailsIcon />
</IconButton> </IconButton>
{/* <IconButton onClick={event => handleViewMaterial(event, entry.encyclopedia.material.material_id)}>
<DetailsIcon />
</IconButton> */}
</Tooltip> </Tooltip>
return <DataTable return <DataTable
......
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