Commit 16dd821a authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Refactored QuantityHistogram.

parent 0ae29760
......@@ -25,4 +25,4 @@ nomad.yaml
./gunicorn.conf
build/
dist/
setup.json
\ No newline at end of file
setup.json
......@@ -12,6 +12,7 @@
# generated
public/metainfo/
public/meta.json
str/searchQuantities.json
# misc
.DS_Store
......
......@@ -204,8 +204,6 @@ class Api {
}
constructor(keycloak) {
this.statistics = {}
this._swaggerClient = Swagger(`${apiBase}/swagger.json`)
this.keycloak = keycloak
......@@ -403,7 +401,7 @@ class Api {
.finally(this.onFinishLoading)
}
async search(search, statisticsToRefresh = []) {
async search(search) {
this.onStartLoading()
return this.swagger()
.then(client => client.apis.repo.search({
......@@ -411,32 +409,6 @@ class Api {
...search}))
.catch(handleApiError)
.then(response => response.body)
.then(response => {
// fill absent statistics values with values from prior searches
// this helps to keep consistent values, e.g. in the metadata search view
if (response.statistics) {
const empty = {}
const refreshList = ['total', 'authors', 'atoms'].concat(statisticsToRefresh)
Object.keys(response.statistics.total.all).forEach(metric => {
empty[metric] = 0
})
Object.keys(response.statistics)
.filter(key => !refreshList.includes(key))
.forEach(key => {
if (!this.statistics[key]) {
this.statistics[key] = new Set()
}
const values = this.statistics[key]
Object.keys(response.statistics[key]).forEach(value => values.add(value))
values.forEach(value => {
if (!response.statistics[key][value]) {
response.statistics[key][value] = empty
}
})
})
}
return response
})
.finally(this.onFinishLoading)
}
......
import React, { useContext, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Grid } from '@material-ui/core'
import { Quantity } from '../search/QuantityHistogram'
import QuantityHistogram from '../search/QuantityHistogram'
import { searchContext } from '../search/SearchContext'
export function DFTMethodVisualizations(props) {
......@@ -28,11 +28,11 @@ export function DFTMethodVisualizations(props) {
return (
<Grid container spacing={2}>
<Grid item xs={8}>
<Quantity quantity="dft.code_name" title="Code" scale={0.25} metric={metric} sort columns={2} />
<QuantityHistogram quantity="dft.code_name" title="Code" initialScale={0.25} columns={2} />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.basis_set" title="Basis set" scale={0.25} metric={metric} sort />
<Quantity quantity="dft.xc_functional" title="XC functionals" scale={0.5} metric={metric} sort />
<QuantityHistogram quantity="dft.basis_set" title="Basis set" initialScale={0.25} />
<QuantityHistogram quantity="dft.xc_functional" title="XC functionals" initialScale={0.5} />
</Grid>
</Grid>
)
......@@ -44,9 +44,8 @@ DFTMethodVisualizations.propTypes = {
export function DFTSystemVisualizations(props) {
const {info} = props
const {response: {statistics, metric}, setStatisticsToRefresh, setStatistics} = useContext(searchContext)
const {response: {statistics, metric}, setStatistics} = useContext(searchContext)
useEffect(() => {
setStatisticsToRefresh('dft.labels_springer_compound_class')
setStatistics(['dft.labels_springer_compound_class', 'dft.system', 'dft.crystal_system', 'dft.compound_type'])
}, [])
......@@ -67,14 +66,14 @@ export function DFTSystemVisualizations(props) {
return (
<Grid container spacing={2}>
<Grid item xs={4}>
<Quantity quantity="dft.compound_type" title="Compound type" scale={1} metric={metric} sort />
<QuantityHistogram quantity="dft.compound_type" title="Compound type" initialScale={0.25} />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.system" title="System type" scale={0.25} metric={metric} sort />
<Quantity quantity="dft.crystal_system" title="Crystal system" scale={1} metric={metric} sort />
<QuantityHistogram quantity="dft.system" title="System type" initialScale={0.25} />
<QuantityHistogram quantity="dft.crystal_system" title="Crystal system" />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.labels_springer_compound_class" title="Springer compound" scale={1} metric={metric} />
<QuantityHistogram quantity="dft.labels_springer_compound_class" title="Springer compound" />
</Grid>
</Grid>
)
......@@ -84,51 +83,75 @@ DFTSystemVisualizations.propTypes = {
info: PropTypes.object
}
const searchable_quantities_categories = {
energy_quantities: [
'energy_total',
'energy_total_T0',
'energy_free',
'energy_electrostatic',
'energy_X',
'energy_XC',
'energy_sum_eigenvalues'
],
electronic_quantities: [
'dos_values',
'eigenvalues_values',
'volumetric_data_values',
'electronic_kinetic_energy',
'total_charge',
'atomic_multipole_values'
],
forces_quantities: [
'atom_forces_free',
'atom_forces_raw',
'atom_forces_T0',
'atom_forces',
'stress_tensor'
],
vibrational_quantities: [
'thermodynamical_property_heat_capacity_C_v',
'vibrational_free_energy_at_constant_volume',
'band_energies'
],
magnetic_quantities: [
'spin_S2'
],
optical_quantities: [
'excitation_energies',
'oscillator_strengths',
'transition_dipole_moments'
]
}
const energy_quantities = [
'energy_total',
'energy_total_T0',
'energy_free',
'energy_electrostatic',
'energy_X',
'energy_XC',
'energy_sum_eigenvalues'
]
const electronic_quantities = [
'dos_values',
'eigenvalues_values',
'volumetric_data_values',
'electronic_kinetic_energy',
'total_charge',
'atomic_multipole_values'
]
const forces_quantities = [
'atom_forces_free',
'atom_forces_raw',
'atom_forces_T0',
'atom_forces',
'stress_tensor'
]
const vibrational_quantities = [
'thermodynamical_property_heat_capacity_C_v',
'vibrational_free_energy_at_constant_volume',
'band_energies'
]
const magnetic_quantities = [
'spin_S2'
]
const optical_quantities = [
'excitation_energies',
'oscillator_strengths',
'transition_dipole_moments'
]
const labels = {
'energy_total': 'Total energy',
'energy_total_T0': 'Total energy (0K)',
'energy_free': 'Free energy',
'energy_electrostatic': 'Electrostatic',
'energy_X': 'Exchange',
'energy_XC': 'Exchange-correlation',
'energy_sum_eigenvalues': 'Band energy',
'dos_values': 'DOS',
'eigenvalues_values': 'Eigenvalues',
'volumetric_data_values': 'Volumetric data',
'electronic_kinetic_energy': 'Kinetic energy',
'total_charge': 'Charge',
'atom_forces_free': 'Free atomic forces',
'atom_forces_raw': 'Raw atomic forces',
'atom_forces_T0': 'Atomic forces (0K)',
'atom_forces': 'Atomic forces',
'stress_tensor': 'Stress tensor',
'thermodynamical_property_heat_capacity_C_v': 'Heat capacity',
'vibrational_free_energy_at_constant_volume': 'Free energy (const=V)',
'band_energies': 'Band energies',
'spin_S2': 'Spin momentum operator',
'excitation_energies': 'Excitation energies',
'oscillator_strengths': 'Oscillator strengths',
'transition_dipole_moments': 'Transition dipole moments',
'atomic_multipole_values': 'Atomic multipole values'}
export function DFTPropertyVisualizations(props) {
const {info} = props
const {response: {statistics, metric}, setStatisticsToRefresh, setStatistics} = useContext(searchContext)
const {response: {statistics, metric}, setStatistics} = useContext(searchContext)
useEffect(() => {
setStatisticsToRefresh('dft.labels_springer_classification')
setStatistics([
'dft.searchable_quantities',
'dft.labels_springer_classification'
......@@ -149,35 +172,20 @@ export function DFTPropertyVisualizations(props) {
statistics.code_name = filteredCodeNames
}
const data = (category) => {
const results = {}
const data = statistics['dft.searchable_quantities']
if (!data) {
return null
}
searchable_quantities_categories[category].forEach(value => {
if (data[value]) {
results[value] = data[value]
}
})
return results
}
return (
<Grid container spacing={2}>
<Grid item xs={4}>
<Quantity quantity="dft.searchable_quantities" data={data('energy_quantities')} title="Energy" scale={1} metric={metric} sort tooltips />
<Quantity quantity="dft.searchable_quantities" data={data('electronic_quantities')} title="Electronic" scale={1} metric={metric} sort tooltips />
<QuantityHistogram quantity="dft.searchable_quantities" values={energy_quantities} valueLabels={labels} title="Energy" initialScale={0.5} tooltips />
<QuantityHistogram quantity="dft.searchable_quantities" values={electronic_quantities} valueLabels={labels} title="Electronic" initialScale={0.5} tooltips />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.searchable_quantities" data={data('forces_quantities')} title="Forces" scale={1} metric={metric} sort tooltips />
<Quantity quantity="dft.searchable_quantities" data={data('vibrational_quantities')} title="Vibrational" scale={1} metric={metric} sort tooltips />
<Quantity quantity="dft.searchable_quantities" data={data('optical_quantities')} title="Optical" scale={1} metric={metric} sort tooltips />
<QuantityHistogram quantity="dft.searchable_quantities" values={forces_quantities} valueLabels={labels} title="Forces" initialScale={0.5} tooltips />
<QuantityHistogram quantity="dft.searchable_quantities" values={vibrational_quantities} valueLabels={labels} title="Vibrational" initialScale={0.5} tooltips />
<QuantityHistogram quantity="dft.searchable_quantities" values={optical_quantities} valueLabels={labels} title="Optical" initialScale={1} tooltips />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.labels_springer_classification" title="Springer classification" scale={1} metric={metric} tooltips />
<Quantity quantity="dft.searchable_quantities" data={data('magnetic_quantities')} title="Magnetic" scale={1} metric={metric} sort tooltips />
<QuantityHistogram quantity="dft.labels_springer_classification" title="Springer classification" initialScale={1} tooltips />
<QuantityHistogram quantity="dft.searchable_quantities" values={magnetic_quantities} valueLabels={labels} title="Magnetic" initialScale={1} tooltips />
</Grid>
</Grid>
)
......
import React, { useContext, useEffect } from 'react'
import { Grid } from '@material-ui/core'
import { Quantity } from '../search/QuantityHistogram'
import QuantityHistogram from '../search/QuantityHistogram'
import { searchContext } from '../search/SearchContext'
export default function EMSVisualizations(props) {
const {state: {usedMetric}, setStatistics} = useContext(searchContext)
const {setStatistics} = useContext(searchContext)
useEffect(() => {
setStatistics(['ems.method', 'ems.probing_method', 'ems.sample_microstructure', 'ems.sample_constituents'])
}, [])
return (
<Grid container spacing={2}>
<Grid item xs={6}>
<Quantity quantity="ems.method" title="Method" scale={1} metric={usedMetric} />
<Quantity quantity="ems.probing_method" title="Probing" scale={1} metric={usedMetric} />
<QuantityHistogram quantity="ems.method" title="Method" />
<QuantityHistogram quantity="ems.probing_method" title="Probing" />
</Grid>
<Grid item xs={6}>
<Quantity quantity="ems.sample_microstructure" title="Sample structure" scale={1} metric={usedMetric} />
<Quantity quantity="ems.sample_constituents" title="Sample constituents" scale={1} metric={usedMetric} />
<QuantityHistogram quantity="ems.sample_microstructure" title="Sample structure" />
<QuantityHistogram quantity="ems.sample_constituents" title="Sample constituents" />
</Grid>
</Grid>
)
......
import React, { useContext } from 'react'
import React, { useRef, useState, useEffect, useContext } from 'react'
import PropTypes from 'prop-types'
import { withStyles, Select, MenuItem, Card, CardContent, CardHeader, makeStyles } from '@material-ui/core'
import { Select, MenuItem, Card, CardContent, CardHeader, makeStyles } from '@material-ui/core'
import * as d3 from 'd3'
import { scaleBand, scalePow } from 'd3-scale'
import { formatQuantity, nomadPrimaryColor, nomadSecondaryColor } from '../../config.js'
import { searchContext } from './SearchContext'
import { searchContext } from './SearchContext.js'
import * as searchQuantities from '../../searchQuantities.json'
const unprocessedLabel = 'not processed'
const unavailableLabel = 'unavailable'
......@@ -17,135 +18,66 @@ function split(array, cols) {
return [array.slice(0, size), ...split(array.slice(size), cols - 1)]
}
const _mapping = {
'energy_total': 'Total energy',
'energy_total_T0': 'Total energy (0K)',
'energy_free': 'Free energy',
'energy_electrostatic': 'Electrostatic',
'energy_X': 'Exchange',
'energy_XC': 'Exchange-correlation',
'energy_sum_eigenvalues': 'Band energy',
'dos_values': 'DOS',
'eigenvalues_values': 'Eigenvalues',
'volumetric_data_values': 'Volumetric data',
'electronic_kinetic_energy': 'Kinetic energy',
'total_charge': 'Charge',
'atom_forces_free': 'Free atomic forces',
'atom_forces_raw': 'Raw atomic forces',
'atom_forces_T0': 'Atomic forces (0K)',
'atom_forces': 'Atomic forces',
'stress_tensor': 'Stress tensor',
'thermodynamical_property_heat_capacity_C_v': 'Heat capacity',
'vibrational_free_energy_at_constant_volume': 'Free energy (const=V)',
'band_energies': 'Band energies',
'spin_S2': 'Spin momentum operator',
'excitation_energies': 'Excitation energies',
'oscillator_strengths': 'Oscillator strengths',
'transition_dipole_moments': 'Transition dipole moments'}
class QuantityHistogramUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
data: PropTypes.object,
metric: PropTypes.string.isRequired,
value: PropTypes.string,
onChanged: PropTypes.func.isRequired,
defaultScale: PropTypes.number,
sort: PropTypes.bool,
tooltips: PropTypes.bool,
columns: PropTypes.number
}
static styles = theme => ({
root: {},
content: {
paddingTop: 0,
position: 'relative'
},
tooltip: {
textAlign: 'center',
position: 'absolute',
pointerEvents: 'none',
opacity: 0
},
tooltipContent: {
// copy of the material ui popper style
display: 'inline-block',
color: '#fff',
padding: '4px 8px',
fontSize: '0.625rem',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
lineHeight: '1.4em',
borderRadius: '4px',
backgroundColor: '#616161'
}
})
constructor(props) {
super(props)
this.container = React.createRef()
}
state = {
scalePower: this.props.defaultScale || 0.25
}
componentDidMount() {
this.updateChart()
}
componentDidUpdate() {
this.updateChart()
const useStyles = makeStyles(theme => ({
root: {
marginTop: theme.spacing(2)
},
content: {
paddingTop: 0,
position: 'relative'
},
tooltip: {
textAlign: 'center',
position: 'absolute',
pointerEvents: 'none',
opacity: 0
},
tooltipContent: {
// copy of the material ui popper style
display: 'inline-block',
color: '#fff',
padding: '4px 8px',
fontSize: '0.625rem',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
lineHeight: '1.4em',
borderRadius: '4px',
backgroundColor: '#616161'
}
handleItemClicked(item) {
if (this.props.value === item.key) {
this.props.onChanged(null)
} else {
this.props.onChanged(item.key)
}
}))
export default function QuantityHistogram({
quantity, initialScale = 1, valueLabels = {}, title, values, numberOfValues,
columns = 1, tooltips
}) {
title = title || quantity
values = values || (searchQuantities[quantity] && searchQuantities[quantity].statistic_values)
numberOfValues = numberOfValues || (values && values.length) || (searchQuantities[quantity] && searchQuantities[quantity].statistic_size)
const {response: {statistics, metric}, query, setQuery} = useContext(searchContext)
const statisticsData = statistics[quantity]
const classes = useStyles()
const containerRef = useRef()
const [scale, setScale] = useState(initialScale)
const handleItemClicked = item => {
setQuery({[quantity]: (query[quantity] === item.key) ? null : item.key})
}
updateChart() {
const {classes, sort, tooltips} = this.props
useEffect(() => {
let data = null
if (!this.props.data) {
return
}
const data = Object.keys(this.props.data)
.map(key => ({
key: key,
name: _mapping[key] || key,
value: this.props.data[key][this.props.metric]
if (!statistics[quantity]) {
data = []
} else if (values) {
data = values.map(value => ({
key: value,
name: valueLabels[value] || value,
value: statisticsData[value] ? statisticsData[value][metric] : 0
}))
if (sort) {
data.sort((a, b) => {
const nameA = a.name
const nameB = b.name
if (nameA === nameB) {
return 0
}
if (nameA === unprocessedLabel) {
return 1
}
if (nameB === unprocessedLabel) {
return -1
}
if (nameA === unavailableLabel) {
return 1
}
if (nameB === unavailableLabel) {
return -1
}
return nameA.localeCompare(nameB)
})
} else {
data = Object.keys(statisticsData)
.map(value => ({
key: value,
name: valueLabels[value] || value,
value: statisticsData[value][metric]
}))
// keep the data sorting, but put unavailable and not processed to the end
const unavailableIndex = data.findIndex(d => d.name === unavailableLabel)
const unprocessedIndex = data.findIndex(d => d.name === unprocessedLabel)
......@@ -157,21 +89,23 @@ class QuantityHistogramUnstyled extends React.Component {
}
}
const columns = this.props.columns || 1
for (let i = data.length; i < numberOfValues; i++) {
data.push({key: `empty${i}`, name: '', value: 0})
}
const columnSize = Math.ceil(data.length / columns)
for (let i = data.length; i < columnSize * columns; i++) {
data.push({key: `empty${i}`, name: '', value: 0})
}
const columnsData = split(data, columns)
const {scalePower} = this.state
const selected = this.props.value
const selected = query[quantity]
const containerWidth = this.container.current.offsetWidth
const containerWidth = containerRef.current.offsetWidth
const width = containerWidth / columns - (12 * (columns - 1))
const height = columnSize * 32
const x = scalePow().range([0, width]).exponent(scalePower)
const x = scalePow().range([0, width]).exponent(scale)
// we use at least the domain 0..1, because an empty domain causes a weird layout
const max = d3.max(data, d => d.value) || 1
......@@ -180,7 +114,7 @@ class QuantityHistogramUnstyled extends React.Component {
const rectColor = d => selected === d.key ? nomadPrimaryColor.main : nomadSecondaryColor.light
const textColor = d => selected === d.key ? '#FFF' : '#000'
const container = d3.select(this.container.current)
const container = d3.select(containerRef.current)
const tooltip = container.select('.' + classes.tooltip)
.style('width', width + 'px')
.style('opacity', 0)
......@@ -203,14 +137,17 @@ class QuantityHistogramUnstyled extends React.Component {
columnsData.forEach((data, i) => {
const y = scaleBand().rangeRound([0, height]).padding(0.1)
y.domain(data.map(d => d.name))
y.domain(data.map(d => d.key))
const items = svg.select('#column' + i)
.selectAll('.item')
.data(data, d => d.name)
.data(data, d => d.key)
items.exit().remove()
items
.on('click', d => handleItemClicked(d))
let item = items.enter()