Commit ab463485 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Refactored the search visualization tabs.

parent a2752c46
Pipeline #72695 failed with stages
in 31 minutes and 39 seconds
import React from 'react'
import PropTypes from 'prop-types'
import { Grid } from '@material-ui/core'
import { Quantity } from '../search/QuantityHistogram'
import SearchContext from '../search/SearchContext'
export default class DFTMethodVisualizations extends React.Component {
static propTypes = {
info: PropTypes.object
}
static contextType = SearchContext.type
componentDidMount() {
const {setStatisticsToRefresh} = this.context
setStatisticsToRefresh('dft.labels_springer_compound_class')
}
render() {
const {info} = this.props
const {state: {response: {statistics}, usedMetric}} = this.context
if (statistics.code_name && info) {
// filter based on known codes, since elastic search might return 0 aggregations on
// obsolete code names
const filteredCodeNames = {}
const defaultValue = {
code_runs: 0
}
defaultValue[usedMetric] = 0
info.codes.forEach(key => {
filteredCodeNames[key] = statistics.code_name[key] || defaultValue
})
statistics.code_name = filteredCodeNames
}
return (
<Grid container spacing={24}>
<Grid item xs={8}>
<Quantity quantity="dft.code_name" title="Code" scale={0.25} metric={usedMetric} sort columns={2} />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.basis_set" title="Basis set" scale={0.25} metric={usedMetric} sort />
<Quantity quantity="dft.xc_functional" title="XC functionals" scale={0.5} metric={usedMetric} sort />
</Grid>
</Grid>
)
}
}
......@@ -3,9 +3,8 @@ import PropTypes from 'prop-types'
import { Grid } from '@material-ui/core'
import { Quantity } from '../search/QuantityHistogram'
import SearchContext from '../search/SearchContext'
import { withApi } from '../api'
class DFTSearchByPropertyAggregations extends React.Component {
export default class DFTPropertyVisualizations extends React.Component {
static propTypes = {
info: PropTypes.object
}
......@@ -38,21 +37,19 @@ class DFTSearchByPropertyAggregations extends React.Component {
return (
<Grid container spacing={24}>
<Grid item xs={4}>
<Quantity quantity="dft.quantities_energy" title="Energy" scale={1} metric={usedMetric} />
<Quantity quantity="dft.quantities_forces" title="Forces" scale={1} metric={usedMetric} />
<Quantity quantity="dft.quantities_electronic" title="Electronic" scale={1} metric={usedMetric} />
<Quantity quantity="dft.quantities_energy" title="Energy" scale={1} metric={usedMetric} sort tooltips />
<Quantity quantity="dft.quantities_electronic" title="Electronic" scale={1} metric={usedMetric} sort tooltips />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.quantities_magnetic" title="Magnetic" scale={1} metric={usedMetric} />
<Quantity quantity="dft.quantities_vibrational" title="Vibrational" scale={1} metric={usedMetric} />
<Quantity quantity="dft.quantities_optical" title="Optical" scale={1} metric={usedMetric} />
<Quantity quantity="dft.quantities_forces" title="Forces" scale={1} metric={usedMetric} sort tooltips />
<Quantity quantity="dft.quantities_vibrational" title="Vibrational" scale={1} metric={usedMetric} sort tooltips />
<Quantity quantity="dft.quantities_optical" title="Optical" scale={1} metric={usedMetric} sort tooltips />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.labels_springer_classification" title="Springer classification" scale={1} metric={usedMetric} />
<Quantity quantity="dft.labels_springer_classification" title="Springer classification" scale={1} metric={usedMetric} tooltips />
<Quantity quantity="dft.quantities_magnetic" title="Magnetic" scale={1} metric={usedMetric} sort tooltips />
</Grid>
</Grid>
)
}
}
export default withApi(false, false)(DFTSearchByPropertyAggregations)
......@@ -3,9 +3,8 @@ import PropTypes from 'prop-types'
import { Grid } from '@material-ui/core'
import { Quantity } from '../search/QuantityHistogram'
import SearchContext from '../search/SearchContext'
import { withApi } from '../api'
class DFTSearchAggregations extends React.Component {
export default class DFTSystemVisualizations extends React.Component {
static propTypes = {
info: PropTypes.object
}
......@@ -38,21 +37,16 @@ class DFTSearchAggregations extends React.Component {
return (
<Grid container spacing={24}>
<Grid item xs={4}>
<Quantity quantity="dft.code_name" title="Code" scale={0.25} metric={usedMetric} />
<Quantity quantity="dft.compound_type" title="Compound type" scale={1} metric={usedMetric} sort />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.basis_set" title="Basis set" scale={0.25} metric={usedMetric} />
<Quantity quantity="dft.xc_functional" title="XC functionals" scale={0.5} metric={usedMetric} />
<Quantity quantity="dft.compound_type" title="Compound type" scale={1} metric={usedMetric} />
<Quantity quantity="dft.system" title="System type" scale={0.25} metric={usedMetric} sort />
<Quantity quantity="dft.crystal_system" title="Crystal system" scale={1} metric={usedMetric} sort />
</Grid>
<Grid item xs={4}>
<Quantity quantity="dft.system" title="System type" scale={0.25} metric={usedMetric} />
<Quantity quantity="dft.crystal_system" title="Crystal system" scale={1} metric={usedMetric} />
<Quantity quantity="dft.labels_springer_compound_class" title="Springer compound class" scale={1} metric={usedMetric} />
<Quantity quantity="dft.labels_springer_compound_class" title="Springer compound" scale={1} metric={usedMetric} />
</Grid>
</Grid>
)
}
}
export default withApi(false, false)(DFTSearchAggregations)
import React from 'react'
import DFTSearchAggregations from './dft/DFTSearchAggregations'
import DFTEntryOverview from './dft/DFTEntryOverview'
import DFTEntryCards from './dft/DFTEntryCards'
import EMSSearchAggregations from './ems/EMSSearchAggregations'
import EMSSearchAggregations from './ems/EMSVisualizations'
import EMSEntryOverview from './ems/EMSEntryOverview'
import EMSEntryCards from './ems/EMSEntryCards'
import DFTSearchByPropertyAggregations from './dft/DFTSearchByPropertyAggregations'
import DFTSystemVisualizations from './dft/DFTSystemVisualizations'
import DFTPropertyVisualizations from './dft/DFTPropertyVisualizations'
import DFTMethodVisualizations from './dft/DFTMethodVisualizations'
import EMSVisualizations from './ems/EMSVisualizations'
/* eslint-disable react/display-name */
......@@ -20,21 +22,26 @@ export const domains = ({
entryTitle: data => data.dft && data.dft.code_name ? data.dft.code_name + ' run' : 'Code run',
searchPlaceholder: 'enter atoms, codes, functionals, or other quantity values',
/**
* A component that is used to render the search aggregations. The components needs
* to work with props: aggregations (the aggregation data from the api),
* searchValues (currently selected search values), metric (the metric key to use),
* onChange (callback to propagate searchValue changes).
*/
SearchAggregations: DFTSearchAggregations,
/**
* A component that is used to render the search aggregations by property.
*/
SearchByPropertyAggregations: DFTSearchByPropertyAggregations,
/**
* Metrics are used to show values for aggregations. Each metric has a key (used
* for API calls), a label (used in the select form), and result string (to show
* the overall amount in search results).
* A set of components and metadata that is used to present tabs of search visualizations
* in addition to the globally available elements and users view.
*/
searchVisualizations: {
'system': {
render: props => <DFTSystemVisualizations {...props}/>,
label: 'System',
description: 'Shows histograms on system metadata'
},
'method': {
render: props => <DFTMethodVisualizations {...props}/>,
label: 'Method',
description: 'Shows histograms on method metadata'
},
'properties': {
render: props => <DFTPropertyVisualizations {...props}/>,
label: 'Properties',
description: 'Shows histograms on the availability of key properties'
}
},
searchMetrics: {
code_runs: {
label: 'Entries',
......@@ -157,6 +164,13 @@ export const domains = ({
entryLabelPlural: 'entries',
entryTitle: () => 'Experiment',
searchPlaceholder: 'enter atoms, experimental methods, or other quantity values',
searchVisualizations: {
'metadata': {
render: props => <EMSVisualizations {...props} />,
label: 'Metadata',
description: 'Shows histograms on system metadata'
}
},
/**
* A component that is used to render the search aggregations. The components needs
* to work with props: aggregations (the aggregation data from the api),
......
......@@ -3,7 +3,7 @@ import { Grid } from '@material-ui/core'
import { Quantity } from '../search/QuantityHistogram'
import SearchContext from '../search/SearchContext'
class EMSSearchAggregations extends React.Component {
export default class EMSVisualizations extends React.Component {
static contextType = SearchContext.type
render() {
......@@ -23,5 +23,3 @@ class EMSSearchAggregations extends React.Component {
)
}
}
export default EMSSearchAggregations
......@@ -9,6 +9,14 @@ import SearchContext from '../search/SearchContext'
const unprocessedLabel = 'not processed'
const unavailableLabel = 'unavailable'
function split(array, cols) {
if (cols === 1) {
return [array]
}
const size = Math.ceil(array.length / cols)
return [array.slice(0, size), ...split(array.slice(size), cols - 1)]
}
const _mapping = {
'energy_total': 'Total energy',
'energy_total_T0': 'Total energy (0K)',
......@@ -35,19 +43,6 @@ const _mapping = {
'oscillator_strengths': 'Oscillator strengths',
'transition_dipole_moments': 'Transition dipole moments'}
function mapKey(key, short = true) {
let name = key
const maxLength = 17
if (key in _mapping) {
name = _mapping[key]
}
if (name.length > maxLength && short) {
return name.substring(0, maxLength) + '...'
} else {
return name
}
}
class QuantityHistogramUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
......@@ -57,20 +52,27 @@ class QuantityHistogramUnstyled extends React.Component {
metric: PropTypes.string.isRequired,
value: PropTypes.string,
onChanged: PropTypes.func.isRequired,
defaultScale: PropTypes.number
defaultScale: PropTypes.number,
sort: PropTypes.bool,
tooltips: PropTypes.bool,
columns: PropTypes.number
}
static styles = theme => ({
root: {},
content: {
paddingTop: 0
paddingTop: 0,
position: 'relative'
},
tooltip: {
textAlign: 'center',
position: 'absolute'
position: 'absolute',
pointerEvents: 'none',
opacity: 0
},
tooltipContent: {
display: 'inline',
// copy of the material ui popper style
display: 'inline-block',
color: '#fff',
padding: '4px 8px',
fontSize: '0.625rem',
......@@ -84,7 +86,6 @@ class QuantityHistogramUnstyled extends React.Component {
constructor(props) {
super(props)
this.container = React.createRef()
this.svgEl = React.createRef()
}
state = {
......@@ -110,147 +111,204 @@ class QuantityHistogramUnstyled extends React.Component {
}
updateChart() {
const {classes, sort, tooltips} = this.props
if (!this.props.data) {
return
}
const {classes} = this.props
const {scalePower} = this.state
const selected = this.props.value
const width = this.container.current.offsetWidth
const left = this.container.current.offsetLeft
const top = this.container.current.offsetTop
const height = Object.keys(this.props.data).length * 32
const data = Object.keys(this.props.data)
.map(key => ({
key: key,
name: mapKey(key),
name: _mapping[key] || key,
value: this.props.data[key][this.props.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)
if (unavailableIndex !== -1) {
data.push(data.splice(unavailableIndex, 1)[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 {
// 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)
if (unavailableIndex !== -1) {
data.push(data.splice(unavailableIndex, 1)[0])
}
if (unprocessedIndex !== -1) {
data.push(data.splice(unprocessedIndex, 1)[0])
}
}
if (unprocessedIndex !== -1) {
data.push(data.splice(unprocessedIndex, 1)[0])
const columns = this.props.columns || 1
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 containerWidth = this.container.current.offsetWidth
const width = containerWidth / columns - (12 * (columns - 1))
const height = columnSize * 32
const y = scaleBand().rangeRound([0, height]).padding(0.1)
const x = scalePow().range([0, width]).exponent(scalePower)
// 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
x.domain([0, max])
y.domain(data.map(d => d.name))
const tooltip = d3.select(this.container.current)
.append('div')
.attr('class', classes.tooltip)
.style('width', width + 'px')
.style('opacity', 0)
const tooltipContent = tooltip
.append('div')
.attr('class', classes.tooltipContent)
let svg = d3.select(this.svgEl.current)
svg.attr('width', width)
svg.attr('height', height)
let withData = svg
.selectAll('.item')
.data(data, data => data.name)
withData.exit().remove()
const rectColor = d => selected === d.key ? nomadPrimaryColor.main : nomadSecondaryColor.light
const textColor = d => selected === d.key ? '#FFF' : '#000'
let item = withData.enter()
const container = d3.select(this.container.current)
const tooltip = container.select('.' + classes.tooltip)
.style('width', width + 'px')
.style('opacity', 0)
const tooltipContent = container.select('.' + classes.tooltipContent)
const svg = container.select('svg')
.attr('width', containerWidth)
.attr('height', height)
const columnsG = svg
.selectAll('.column')
.data(columnsData.map((_, i) => `column${i}`))
columnsG.exit().remove()
columnsG
.enter()
.append('g')
.attr('class', 'item')
item
.append('rect')
.attr('class', 'bar')
.attr('x', x(0))
.attr('y', d => y(d.name))
.attr('width', d => x(d.value) - x(0))
.attr('height', y.bandwidth())
.style('fill', rectColor)
// .style('stroke', '#000')
// .style('stroke-width', '1px')
.style('shape-rendering', 'geometricPrecision')
item
.append('text')
.attr('class', 'name')
.attr('dy', '.75em')
.attr('x', x(0) + 4)
.attr('y', d => y(d.name) + 4)
.attr('text-anchor', 'start')
.style('fill', textColor)
.text(d => d.name)
item
.append('text')
.attr('class', 'value')
.attr('dy', y.bandwidth())
.attr('y', d => y(d.name) - 4)
.attr('x', d => width - 4)
.attr('text-anchor', 'end')
.style('fill', textColor)
.text(d => formatQuantity(d.value))
item
.style('cursor', 'pointer')
.on('click', d => this.handleItemClicked(d))
item
.on('mouseover', function(d) {
tooltip.transition()
.duration(200)
.style('opacity', 0.9)
tooltip
.style('left', left + 'px')
.style('top', (y(d.name) + top + 32) + 'px')
tooltipContent.html(mapKey(d.key, false))
})
.on('mouseout', function(d) {
tooltip.transition()
.duration(500)
.style('opacity', 0)
})
const t = d3.transition().duration(500)
item = withData.transition(t)
item
.select('.bar')
.attr('y', d => y(d.name))
.attr('width', d => x(d.value) - x(0))
.attr('height', y.bandwidth())
.style('fill', rectColor)
item
.select('.name')
.text(d => d.name)
.attr('y', d => y(d.name) + 4)
.style('fill', textColor)
item
.select('.value')
.text(d => formatQuantity(d.value))
.attr('y', d => y(d.name) - 4)
.attr('x', width - 4)
.style('fill', textColor)
.attr('id', d => d)
.attr('class', 'column')
.attr('transform', (d, i) => `translate(${i * (width + 12)}, 0)`)
columnsData.forEach((data, i) => {
const y = scaleBand().rangeRound([0, height]).padding(0.1)
y.domain(data.map(d => d.name))
const items = svg.select('#column' + i)
.selectAll('.item')
.data(data, d => d.name)
items.exit().remove()
let item = items.enter()
.append('g')
.attr('class', 'item')
.attr('display', d => d.name === '' ? 'none' : 'show')
item
.append('rect')
.attr('x', x(0))
.attr('y', d => y(d.name))
.attr('width', width)
.attr('class', 'background')
.style('opacity', 0)
.attr('height', y.bandwidth())
item
.append('rect')
.attr('class', 'bar')
.attr('x', x(0))
.attr('y', d => y(d.name))
.attr('width', d => x(d.value) - x(0))
.attr('height', y.bandwidth())
.style('fill', rectColor)
// .style('stroke', '#000')
// .style('stroke-width', '1px')
.style('shape-rendering', 'geometricPrecision')
item
.append('text')
.attr('class', 'name')
.attr('dy', '.75em')
.attr('x', x(0) + 4)
.attr('y', d => y(d.name) + 4)
.attr('text-anchor', 'start')
.style('fill', textColor)
.text(d => d.name)
item
.append('text')
.attr('class', 'value')
.attr('dy', y.bandwidth())
.attr('y', d => y(d.name) - 4)
.attr('x', d => width - 4)
.attr('text-anchor', 'end')
.style('fill', textColor)
.text(d => formatQuantity(d.value))
item
.style('cursor', 'pointer')
.on('click', d => this.handleItemClicked(d))
item
.on('mouseover', function(d) {
d3.select(this).select('.background')
.style('opacity', 0.08)
if (tooltips) {
tooltip.transition()
.duration(200)
.style('opacity', 1)
tooltip
.style('left', i * (width + 12) + 'px')