From 5d4424f1d7c484d24c24ce80b5ab9c4fb50ae55d Mon Sep 17 00:00:00 2001
From: Alvin Noe Ladines <ladinesalvinnoe@gmail.com>
Date: Thu, 19 Mar 2020 18:39:22 +0100
Subject: [PATCH] (re-)Implemented properties and users tabs

---
 .../components/dft/DFTSearchAggregations.js   |  10 +-
 .../dft/DFTSearchByPropertyAggregations.js    |  53 +++
 gui/src/components/domains.js                 |   5 +
 .../components/search/QuantityHistogram.js    |  35 +-
 gui/src/components/search/Search.js           |  49 ++
 gui/src/components/search/SearchContext.js    |   3 +-
 gui/src/components/search/UploadsChart.js     | 434 ++++++++++++++++++
 nomad/app/api/repo.py                         |   5 +-
 nomad/datamodel/datamodel.py                  |   2 +-
 nomad/datamodel/dft.py                        | 132 ++++++
 nomad/search.py                               |   4 +-
 11 files changed, 722 insertions(+), 10 deletions(-)
 create mode 100644 gui/src/components/dft/DFTSearchByPropertyAggregations.js
 create mode 100644 gui/src/components/search/UploadsChart.js

diff --git a/gui/src/components/dft/DFTSearchAggregations.js b/gui/src/components/dft/DFTSearchAggregations.js
index f2d0dbee1e..a281d29324 100644
--- a/gui/src/components/dft/DFTSearchAggregations.js
+++ b/gui/src/components/dft/DFTSearchAggregations.js
@@ -36,13 +36,15 @@ class DFTSearchAggregations extends React.Component {
         <Grid item xs={4}>
           <Quantity quantity="dft.code_name" title="Code" scale={0.25} metric={usedMetric} />
         </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} />
-        </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} />
+        </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} />
         </Grid>
       </Grid>
     )
diff --git a/gui/src/components/dft/DFTSearchByPropertyAggregations.js b/gui/src/components/dft/DFTSearchByPropertyAggregations.js
new file mode 100644
index 0000000000..4ebc08fa32
--- /dev/null
+++ b/gui/src/components/dft/DFTSearchByPropertyAggregations.js
@@ -0,0 +1,53 @@
+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'
+import { withApi } from '../api'
+
+class DFTSearchByPropertyAggregations extends React.Component {
+  static propTypes = {
+    info: PropTypes.object
+  }
+
+  static contextType = SearchContext.type
+
+  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={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} />
+        </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} />
+        </Grid>
+        <Grid item xs={4}>
+          <Quantity quantity="dft.labels_springer_classification" title="Springer classification" scale={1} metric={usedMetric} />
+        </Grid>
+      </Grid>
+    )
+  }
+}
+
+export default withApi(false, false)(DFTSearchByPropertyAggregations)
diff --git a/gui/src/components/domains.js b/gui/src/components/domains.js
index 5c3e6c631d..8520a02891 100644
--- a/gui/src/components/domains.js
+++ b/gui/src/components/domains.js
@@ -5,6 +5,7 @@ import DFTEntryCards from './dft/DFTEntryCards'
 import EMSSearchAggregations from './ems/EMSSearchAggregations'
 import EMSEntryOverview from './ems/EMSEntryOverview'
 import EMSEntryCards from './ems/EMSEntryCards'
+import DFTSearchByPropertyAggregations from './dft/DFTSearchByPropertyAggregations'
 
 export const domains = ({
   dft: {
@@ -23,6 +24,10 @@ export const domains = ({
      * 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
diff --git a/gui/src/components/search/QuantityHistogram.js b/gui/src/components/search/QuantityHistogram.js
index 4b1b433597..b6e430da01 100644
--- a/gui/src/components/search/QuantityHistogram.js
+++ b/gui/src/components/search/QuantityHistogram.js
@@ -9,6 +9,39 @@ import SearchContext from '../search/SearchContext'
 const unprocessed_label = 'not processed'
 const unavailable_label = 'unavailable'
 
+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'}
+
+function map_key (name) {
+  if (name in _mapping) {
+    return _mapping[name]
+  }
+  return name
+}
+
 class QuantityHistogramUnstyled extends React.Component {
   static propTypes = {
     classes: PropTypes.object.isRequired,
@@ -69,7 +102,7 @@ class QuantityHistogramUnstyled extends React.Component {
 
     const data = Object.keys(this.props.data)
       .map(key => ({
-        name: key,
+        name: map_key(key),
         value: this.props.data[key][this.props.metric]
       }))
 
diff --git a/gui/src/components/search/Search.js b/gui/src/components/search/Search.js
index 00684af68f..287238d92b 100644
--- a/gui/src/components/search/Search.js
+++ b/gui/src/components/search/Search.js
@@ -16,6 +16,7 @@ import UploadList from './UploadsList'
 import GroupList from './GroupList'
 import ApiDialogButton from '../ApiDialogButton'
 import SearchIcon from '@material-ui/icons/Search'
+import UploadsChart from './UploadsChart'
 
 class Search extends React.Component {
   static tabs = {
@@ -95,6 +96,16 @@ class Search extends React.Component {
       render: props => <DomainVisualization {...props}/>,
       label: 'Meta data',
       description: 'Shows histograms on key metadata'
+    },
+    'property': {
+      render: props => <PropertyVisualization {...props}/>,
+      label: 'Properties',
+      description: 'Shows histograms on key properties'
+    },
+     'users': {
+      render: props => <UsersVisualization {...props}/>,
+      label: 'Users',
+      description: 'Show statistics on user metadata'
     }
   }
 
@@ -215,6 +226,44 @@ class DomainVisualization extends React.Component {
   }
 }
 
+class PropertyVisualization extends React.Component {
+  static propTypes = {
+    open: PropTypes.bool
+  }
+
+  static contextType = SearchContext.type
+
+  render() {
+    const {domain} = this.context.state
+    const {open} = this.props
+
+    return <KeepState visible={open} render={() =>
+      <domain.SearchByPropertyAggregations />
+    }/>
+  }
+}
+
+class UsersVisualization extends React.Component {
+  static propTypes = {
+    open: PropTypes.bool
+  }
+
+  static contextType = SearchContext.type
+
+  render () {
+  const {domain} = this.context.state
+  const {open} = this.props
+
+  return <KeepState visible={open} render={() =>
+    <Card>
+      <CardContent>
+        <UploadsChart metricsDefinitions={domain.searchMetrics}/>
+      </CardContent>
+    </Card>
+  }/>
+  }
+}
+
 class ElementsVisualization extends React.Component {
   static propTypes = {
     open: PropTypes.bool
diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js
index d189ae718d..3ef869c8ef 100644
--- a/gui/src/components/search/SearchContext.js
+++ b/gui/src/components/search/SearchContext.js
@@ -50,7 +50,8 @@ class SearchContext extends React.Component {
       order_by: 'upload_time',
       order: -1,
       page: 1,
-      per_page: 10
+      per_page: 10,
+      date_histogram: true
     },
     metric: this.defaultMetric,
     usedMetric: this.defaultMetric,
diff --git a/gui/src/components/search/UploadsChart.js b/gui/src/components/search/UploadsChart.js
new file mode 100644
index 0000000000..ca8df54c47
--- /dev/null
+++ b/gui/src/components/search/UploadsChart.js
@@ -0,0 +1,434 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { withStyles, Select, MenuItem } from '@material-ui/core'
+import Grid from '@material-ui/core/Grid'
+import TextField from '@material-ui/core/TextField'
+import * as d3 from 'd3'
+import { scaleBand, scalePow } from 'd3-scale'
+import { nomadSecondaryColor } from '../../config.js'
+import SearchContext from './SearchContext'
+import { compose } from 'recompose'
+import { withApi } from '../api'
+import { Quantity } from './QuantityHistogram'
+
+class UploadsHistogramUnstyled extends React.Component {
+  static propTypes = {
+    classes: PropTypes.object.isRequired,
+    height: PropTypes.number.isRequired,
+    data: PropTypes.object,
+    interval: PropTypes.string,
+    metric: PropTypes.string.isRequired,
+    metricsDefinitions: PropTypes.object.isRequired,
+    onChanged: PropTypes.func.isRequired,
+    defaultScale: PropTypes.number
+  }
+
+  static styles = theme => ({
+    root: {},
+    content: {
+      paddingTop: 10
+    }
+  })
+
+  constructor(props) {
+    super(props)
+    this.state = {
+      scalePower: this.props.defaultScale || 1.0,
+      interval: this.props.interval || '1M',
+      time: null,
+      from_time: 0,
+      until_time: 0
+    }
+
+    this.container = React.createRef()
+    this.svgEl = React.createRef()
+  }
+
+  startDate = '2013-01-01'
+
+  scales = [
+    {
+      label: 'Linear',
+      value: 1.0
+    },
+    {
+      label: '1/2',
+      value: 0.5
+    },
+    {
+      label: '1/4',
+      value: 0.25
+    },
+    {
+      label: '1/8',
+      value: 0.25
+    }
+  ]
+
+  intervals = [
+    {
+      label: 'Yearly',
+      value: '1y',
+      number: 31536000000
+    },
+    {
+      label: 'Monthly',
+      value: '1M',
+      number: 2678400000
+    },
+    {
+      label: 'Daily',
+      value: '1d',
+      number: 86400000
+    },
+    {
+      label: 'Hourly',
+      value: '1h',
+      number: 3600000
+    },
+    {
+      label: 'Minute',
+      value: '1m',
+      number: 60000
+    },
+    {
+      label: 'Second',
+      value: '1s',
+      number: 1000
+    }
+  ]
+
+  timeInterval = Object.assign({}, ...this.intervals.map(e => ( {[e.value]: e.number})))
+
+  componentDidMount() {
+    const from_time = new Date(this.startDate).getTime()
+    const until_time = new Date().getTime()
+    this.handleTimeChange(from_time, 'from_time', 'all')
+    this.handleTimeChange(until_time, 'until_time', 'all')
+  }
+
+  componentDidUpdate() {
+    this.updateChart()
+  }
+
+  handleQueryChange() {
+    const interval = this.state.interval
+    const from_time = new Date(this.state.from_time)
+    const until_time = new Date(this.state.until_time)
+    this.props.onChanged(from_time.toISOString(), until_time.toISOString(), interval)
+  }
+
+  handleIntervalChange(newInterval) {
+    // TODO: add a refresh button so directly updating interval is not necessary
+    this.state.interval = newInterval
+    //this.setState({interval: newInterval})
+    this.handleQueryChange()
+  }
+
+  handleTimeChange(newTime, key, target) {
+    let date
+    if (!newTime) {
+      date = key === 'from_time' ? new Date(this.startDate) : new Date()
+    } else {
+      date = new Date(newTime)
+    }
+    if (target === 'state' || target === 'all') {
+      key === 'from_time' ? this.setState({from_time: date.getTime()}) : this.setState({until_time: date.getTime()})
+    }
+    if (target === 'picker' || target === 'all') {
+      document.getElementById(key).value = date.toISOString().substring(0,10)
+    }
+  }
+
+  handleItemClicked(item) {
+    const selected = item.time
+    if (selected === this.state.time) {
+      this.props.onChanged(null, null, null)
+    } else {
+      const deltaT =  this.timeInterval[this.state.interval]
+      this.handleTimeChange(selected, 'from_time', 'all')
+      this.handleTimeChange(selected + deltaT, 'until_time', 'all')
+      this.handleQueryChange()
+    }
+  }
+
+  resolveDate (name) {
+    const date = new Date(parseInt(name, 10))
+    const year = date.toLocaleDateString(undefined, {year: 'numeric'})
+    const month = date.toLocaleDateString(undefined, {month: 'short'})
+    const day = date.toLocaleDateString(undefined, {day: 'numeric'})
+    const hour = date.toLocaleTimeString(undefined, {hour: 'numeric'})
+    const min = date.toLocaleTimeString(undefined, {minute: 'numeric'})
+    const sec= date.toLocaleTimeString(undefined, {second: 'numeric'})
+
+    const intervals = {
+      '1y': year,
+      '1M': month,
+      '1d': day,
+      '1h': hour,
+      '1m': min,
+      '1s': sec
+    }
+
+    return intervals[this.state.interval]
+  }
+
+  hover (svg, bar) {
+    const textOffset = 25
+
+    const tooltip = svg.append('g')
+    .attr('class', 'tooltip')
+    .style('display', 'none')
+
+    const hoverBox = tooltip.append('rect')
+    .attr('width', 10)
+    .attr('height', 20)
+    .attr('fill', 'white')
+    .style('opacity', 0.0)
+
+    const text = tooltip.append('text')
+    .attr('x', textOffset)
+    .attr('dy', '1.2em')
+    .style('text-anchor', 'start')
+    .attr('font-size', '12px')
+    //.attr('font-weight', 'bold')
+
+    bar
+    .on('mouseover', () => {
+      tooltip.style('display', null)
+      let { width } = text.node().getBBox()
+      hoverBox.attr('width', `${ width + textOffset }px`)
+    })
+    .on('mouseout', () => tooltip.style('display', 'none'))
+    .on('mousemove', function(d) {
+      let xPosition = d3.mouse(this)[0] - 15
+      let yPosition = d3.mouse(this)[1] - 25
+
+      tooltip.attr('transform', `translate( ${ xPosition }, ${ yPosition })`)
+      tooltip.attr('data-html', 'true')
+      tooltip.select('text').text( new Date(d.time).toISOString() + ': ' + d.value )
+    })
+  }
+
+  updateChart () {
+    let data = []
+    if (! this.props.data) {
+      return
+    } else {
+      data = Object.keys(this.props.data).map(key => ({
+        time: parseInt(key, 10),
+        name: this.resolveDate(key),
+        value: this.props.data[key][this.props.metric]
+    }))}
+
+
+    data.sort((a, b) => d3.ascending(a.time, b.time))
+    if (data.length > 0) {
+      this.handleTimeChange(this.state.from_time, 'from_time', 'picker')
+      this.handleTimeChange(this.state.until_time, 'until_time', 'picker')
+    }
+
+    const scalePower  = this.state.scalePower
+    const width = this.container.current.offsetWidth
+    const height = this.props.height
+    const margin = Math.round(0.1*height)
+
+    const x = scaleBand().rangeRound([margin, width]).padding(0.1)
+    const y = scalePow().range([height-margin, margin]).exponent(scalePower)
+
+    const max = d3.max(data, d => d.value) || 0
+    x.domain(data.map(d => d.name))
+    y.domain([0, max])
+
+    let svg = d3.select(this.svgEl.current)
+    svg.attr('width', width)
+    svg.attr('height', height)
+
+    const xAxis = d3.axisBottom(x)
+    svg.select('.xaxis').remove()
+    svg.append('g')
+    .attr('transform', `translate(0,${height-margin})`)
+    .attr('class', 'xaxis')
+    .call(xAxis)
+
+    svg.select('.xlabel').remove()
+    svg.append('text')
+    .attr('class', 'xlabel')
+    .attr("x", width)
+    .attr("y", height-4)
+    .attr('dy', ".35em")
+    .attr('font-size', '12px')
+    .style('text-anchor', 'end')
+
+    const yAxis = d3.axisLeft(y)
+    svg.select('.yaxis').remove()
+    svg.append('g')
+    .attr('transform', `translate(${margin}, 0)`)
+    .attr('class', 'yaxis')
+    .call(yAxis)
+
+    const {label, shortLabel} = this.props.metricsDefinitions[this.props.metric]
+    svg.select('.ylabel').remove()
+    svg.append('text')
+    .attr('class','ylabel')
+    .attr('x', 0)
+    .attr('y', 0)
+    .attr('dy', "1em")
+    .attr('text-anchor', 'start')
+    .attr('font-size', '12px')
+    .text(`${shortLabel ? shortLabel : label}`)
+
+    let withData = svg
+      .selectAll('.bar').remove().exit()
+      .data(data)
+
+    let item = withData.enter()
+      .append('g')
+
+    item
+      .append('rect')
+      .attr('class', 'bar')
+      .attr('x', d => x(d.name))
+      .attr('y', d => y(d.value))
+      .attr('width', x.bandwidth())
+      .attr('height', d => y(0) - y(d.value))
+      .style('fill', nomadSecondaryColor.light)
+
+    item
+      .style('cursor', 'pointer')
+      .on('click', d => this.handleItemClicked(d))
+
+    svg.select('.tooltip').remove()
+    svg.call(this.hover, item)
+}
+
+  render () {
+    return (
+      <div>
+        <Grid container justify='space-around' spacing={24}>
+          <Grid item xs={2}>
+            <Select
+              margin='none'
+              id='scales'
+              value={this.state.scalePower}
+              onChange={(event) => this.setState({scalePower: event.target.value})}
+              label= 'scale'
+            > {this.scales.map(item => (
+              <MenuItem
+                value={item.value}
+                key={item.label}> {item.label}
+              </MenuItem>))}
+            </Select>
+          </Grid>
+          <Grid item xs={3}>
+            <TextField
+              id='from_time'
+              label="from time"
+              type="date"
+              onChange={(event) => this.handleTimeChange(event.target.value, 'from_time', 'state')}
+              InputLabelProps={{
+                shrink: true,
+              }}
+            />
+          </Grid>
+          <Grid item xs={3}>
+            <Select
+                id='interval'
+                value={this.state.interval}
+                onChange={(event) => this.handleIntervalChange(event.target.value)}
+                label= 'interval'
+              > {this.intervals.map(item => (
+                <MenuItem value={item.value} key={item.value}>
+                  {item.label}
+                </MenuItem>))}
+            </Select>
+          </Grid>
+          <Grid item xs={3}>
+            <TextField
+              id='until_time'
+              label="until time"
+              type="date"
+              onChange={(event) => this.handleTimeChange(event.target.value, 'until_time', 'state')}
+              InputLabelProps={{
+                shrink: true,
+              }}
+            />
+          </Grid>
+        </Grid>
+        <div ref={this.container}>
+          <svg ref={this.svgEl}></svg>
+        </div>
+      </div>
+    )
+  }
+}
+
+export const UploadsHistogram = withStyles(UploadsHistogramUnstyled.styles)(UploadsHistogramUnstyled)
+
+class UploadersListUnstyled extends React.Component {
+
+  static propTypes = {
+    classes: PropTypes.object.isRequired
+  }
+
+  static styles = theme => ({
+    root: {
+      marginTop: theme.spacing.unit * 2
+    }
+  })
+
+  static contextType = SearchContext.type
+
+  render () {
+    const {state: {usedMetric}} = this.context
+
+    return (
+        <Grid>
+          <Quantity quantity="uploader" title="Top Uploaders" scale={1} metric={usedMetric} />
+        </Grid>
+    )
+  }
+}
+
+export const  UploadersList = withStyles(UploadersListUnstyled.styles)(UploadersListUnstyled)
+
+class UploadsChart extends React.Component {
+  static propTypes = {
+    classes: PropTypes.object.isRequired,
+    metricsDefinitions: PropTypes.object.isRequired
+  }
+  static styles = theme => ({
+    root: {
+      marginTop: theme.spacing.unit
+    }
+  })
+
+  static contextType = SearchContext.type
+
+  render() {
+    const {classes, metricsDefinitions, ...props} = this.props
+    const {state: {response, usedMetric, query, }, setQuery} = this.context
+
+    return (
+      <Grid container spacing={24}>
+        <Grid item xs={12}>
+          <UploadsHistogram
+            classes={{root: classes.root}}
+            height={250}
+            defaultScale={1}
+            data={response.statistics.date_histogram}
+            metric={usedMetric}
+            metricsDefinitions={metricsDefinitions}
+            interval={'1M'}
+            onChanged={(from_time, until_time, interval) => setQuery({...query, from_time: from_time, until_time: until_time, interval: interval})}
+            {...props} />
+        </Grid>
+        <Grid item xs={12}>
+          <UploadersList />
+        </Grid>
+      </Grid>
+      )
+  }
+}
+
+export default compose(withApi(false, false), withStyles(UploadsChart.styles))(UploadsChart)
\ No newline at end of file
diff --git a/nomad/app/api/repo.py b/nomad/app/api/repo.py
index 0cbc3847d9..07ad4b07ee 100644
--- a/nomad/app/api/repo.py
+++ b/nomad/app/api/repo.py
@@ -79,6 +79,8 @@ add_scroll_parameters(_search_request_parser)
 add_search_parameters(_search_request_parser)
 _search_request_parser.add_argument(
     'date_histogram', type=bool, help='Add an additional aggregation over the upload time')
+_search_request_parser.add_argument(
+    'interval', type=str, help='Interval to use for upload time aggregation.')
 _search_request_parser.add_argument(
     'metrics', type=str, action='append', help=(
         'Metrics to aggregate over all quantities and their values as comma separated list. '
@@ -168,6 +170,7 @@ class RepoCalcsResource(Resource):
             order_by = args.get('order_by', 'upload_time')
 
             date_histogram = args.get('date_histogram', False)
+            interval = args.get('interval', '1M')
             metrics: List[str] = request.args.getlist('metrics')
 
             with_statistics = args.get('statistics', False) or \
@@ -178,7 +181,7 @@ class RepoCalcsResource(Resource):
         search_request = search.SearchRequest()
         apply_search_parameters(search_request, args)
         if date_histogram:
-            search_request.date_histogram()
+            search_request.date_histogram(interval=interval)
 
         try:
             assert page >= 1
diff --git a/nomad/datamodel/datamodel.py b/nomad/datamodel/datamodel.py
index fc072d40ed..23c16ca618 100644
--- a/nomad/datamodel/datamodel.py
+++ b/nomad/datamodel/datamodel.py
@@ -352,7 +352,7 @@ class EntryMetadata(metainfo.MSection):
             Search(
                 description='Search uploader with exact names.',
                 metric_name='uploaders', metric='cardinality',
-                many_or='append', search_field='uploader.name.keyword'),
+                many_or='append', search_field='uploader.name.keyword', default_statistic=True, statistic_size=10),
             Search(
                 name='uploader_id', search_field='uploader.user_id')
         ])
diff --git a/nomad/datamodel/dft.py b/nomad/datamodel/dft.py
index e4d6d89200..b963ec0bae 100644
--- a/nomad/datamodel/dft.py
+++ b/nomad/datamodel/dft.py
@@ -45,6 +45,58 @@ basis_sets = {
     'planewaves': 'plane waves'
 }
 
+compound_types = [
+    'unary',
+    'binary',
+    'ternary',
+    'quaternary',
+    'quinary',
+    'sexinary',
+    'septenary',
+    'octanary',
+    'nonary',
+    'decinary'
+]
+
+_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'
+]
+
 version_re = re.compile(r'(\d+(\.\d+(\.\d+)?)?)')
 
 
@@ -60,6 +112,12 @@ def map_basis_set_to_basis_set_label(name):
     return basis_sets.get(key, name)
 
 
+def map_atoms_to_compound_type(atoms):
+    if len(atoms) > len(compound_types):
+        return '>decinary'
+    return compound_types[len(atoms) - 1]
+
+
 def simplify_version(version):
     match = version_re.search(version)
     if match is None:
@@ -107,6 +165,12 @@ class DFTMetadata(MSection):
         description='The system type of the simulated system.',
         a_search=Search(default_statistic=True))
 
+    compound_type = Quantity(
+        type=str, default='not processed',
+        description='The compound type of the simulated system.',
+        a_search=Search(statistic_size=11, default_statistic=True)
+    )
+
     crystal_system = Quantity(
         type=str, default='not processed',
         description='The crystal system type of the simulated system.',
@@ -155,6 +219,36 @@ class DFTMetadata(MSection):
         a_search=Search(
             metric_name='distinct_quantities', metric='cardinality', many_and='append'))
 
+    quantities_energy = Quantity(
+        type=str, shape=['0..*'],
+        description='Energy-related quantities.',
+        a_search=Search(many_and='append', default_statistic=True))
+
+    quantities_electronic = Quantity(
+        type=str, shape=['0..*'],
+        description='Electronic structure-related quantities.',
+        a_search=Search(many_and='append', default_statistic=True))
+
+    quantities_forces = Quantity(
+        type=str, shape=['0..*'],
+        description='Forces-related quantities.',
+        a_search=Search(many_and='append', default_statistic=True))
+
+    quantities_vibrational = Quantity(
+        type=str, shape=['0..*'],
+        description='Vibrational-related quantities.',
+        a_search=Search(many_and='append', default_statistic=True))
+
+    quantities_magnetic = Quantity(
+        type=str, shape=['0..*'],
+        description='Magnetic-related quantities.',
+        a_search=Search(many_and='append', default_statistic=True))
+
+    quantities_optical = Quantity(
+        type=str, shape=['0..*'],
+        description='Optical-related quantities.',
+        a_search=Search(many_and='append', default_statistic=True))
+
     geometries = Quantity(
         type=str, shape=['0..*'],
         description='Hashes for each simulated geometry',
@@ -170,6 +264,16 @@ class DFTMetadata(MSection):
         description='The labels taken from AFLOW prototypes and springer.',
         a_search='labels')
 
+    labels_springer_compound_class = Quantity(
+        type=str, shape=['0..*'],
+        description='Springer compund classification.',
+        a_search=Search(many_and='append', default_statistic=True, statistic_size=15))
+
+    labels_springer_classification = Quantity(
+        type=str, shape=['0..*'],
+        description='Springer classification by property.',
+        a_search=Search(many_and='append', default_statistic=True, statistic_size=15))
+
     optimade = SubSection(
         sub_section=OptimadeEntry,
         description='Metadata used for the optimade API.',
@@ -213,6 +317,7 @@ class DFTMetadata(MSection):
         atoms = list(set(normalized_atom_labels(set(atoms))))
         atoms.sort()
         entry.atoms = atoms
+        self.compound_type = map_atoms_to_compound_type(atoms)
 
         self.crystal_system = get_optional_backend_value(
             backend, 'crystal_system', 'section_symmetry', logger=logger)
@@ -243,6 +348,13 @@ class DFTMetadata(MSection):
         # metrics and quantities
         quantities = set()
         geometries = set()
+        quantities_energy = set()
+        quantities_electronic = set()
+        quantities_forces = set()
+        quantities_vibrational = set()
+        quantities_magnetic = set()
+        quantities_optical = set()
+
         n_quantities = 0
         n_calculations = 0
         n_total_energies = 0
@@ -250,6 +362,18 @@ class DFTMetadata(MSection):
 
         for meta_info, event, value in backend.traverse():
             quantities.add(meta_info)
+            if meta_info in _energy_quantities:
+                quantities_energy.add(meta_info)
+            elif meta_info in _electronic_quantities:
+                quantities_electronic.add(meta_info)
+            elif meta_info in _forces_quantities:
+                quantities_forces.add(meta_info)
+            elif meta_info in _vibrational_quantities:
+                quantities_vibrational.add(meta_info)
+            elif meta_info in _magnetic_quantities:
+                quantities_magnetic.add(meta_info)
+            elif meta_info in _optical_quantities:
+                quantities_optical.add(meta_info)
 
             if event == ParserEvent.add_value or event == ParserEvent.add_array_value:
                 n_quantities += 1
@@ -269,6 +393,12 @@ class DFTMetadata(MSection):
 
         self.quantities = list(quantities)
         self.geometries = list(geometries)
+        self.quantities_energy = list(quantities_energy)
+        self.quantities_electronic = list(quantities_electronic)
+        self.quantities_forces = list(quantities_forces)
+        self.quantities_vibrational = list(quantities_vibrational)
+        self.quantities_magnetic = list(quantities_magnetic)
+        self.quantities_optical = list(quantities_optical)
         self.n_quantities = n_quantities
         self.n_calculations = n_calculations
         self.n_total_energies = n_total_energies
@@ -285,6 +415,8 @@ class DFTMetadata(MSection):
             self.labels.append(Label(label=compound, type='compound_class', source='springer'))
         for classification in classifications:
             self.labels.append(Label(label=classification, type='classification', source='springer'))
+        self.labels_springer_compound_class = list(compounds)
+        self.labels_springer_classification = list(classifications)
 
         aflow_id = get_optional_backend_value(backend, 'prototype_aflow_id', 'section_prototype')
         aflow_label = get_optional_backend_value(backend, 'prototype_label', 'section_prototype')
diff --git a/nomad/search.py b/nomad/search.py
index 97d98a8056..b98501c981 100644
--- a/nomad/search.py
+++ b/nomad/search.py
@@ -337,11 +337,11 @@ class SearchRequest:
                 'metric:%s' % metric_quantity.metric_name,
                 A(metric_quantity.metric, field=field))
 
-    def date_histogram(self, metrics_to_use: List[str] = []):
+    def date_histogram(self, metrics_to_use: List[str] = [], interval: str = '1M'):
         '''
         Adds a date histogram on the given metrics to the statistics part.
         '''
-        histogram = A('date_histogram', field='upload_time', interval='1M', format='yyyy-MM-dd')
+        histogram = A('date_histogram', field='upload_time', interval=interval, format='yyyy-MM-dd')
         self._add_metrics(self._search.aggs.bucket('statistics:date_histogram', histogram), metrics_to_use)
 
         return self
-- 
GitLab