Commit 2aca1cb9 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Merge remote-tracking branch 'origin/newgui' into v0.8.0

parents bb0540a2 189b7971
......@@ -386,7 +386,7 @@ class Api {
.finally(this.onFinishLoading)
}
async search(search) {
async search(search, statisticsToRefresh = []) {
this.onStartLoading()
return this.swagger()
.then(client => client.apis.repo.search({
......@@ -399,11 +399,12 @@ class Api {
// 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 => !['total', 'authors', 'atoms'].includes(key))
.filter(key => !refreshList.includes(key))
.forEach(key => {
if (!this.statistics[key]) {
this.statistics[key] = new Set()
......
......@@ -12,6 +12,11 @@ class DFTSearchAggregations extends React.Component {
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
......
......@@ -12,6 +12,11 @@ class DFTSearchByPropertyAggregations extends React.Component {
static contextType = SearchContext.type
componentDidMount() {
const {setStatisticsToRefresh} = this.context
setStatisticsToRefresh('dft.labels_springer_classification')
}
render() {
const {info} = this.props
const {state: {response: {statistics}, usedMetric}} = this.context
......
......@@ -35,11 +35,17 @@ const _mapping = {
'oscillator_strengths': 'Oscillator strengths',
'transition_dipole_moments': 'Transition dipole moments'}
function mapKey(name) {
if (name in _mapping) {
return _mapping[name]
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
}
return name
}
class QuantityHistogramUnstyled extends React.Component {
......@@ -82,10 +88,10 @@ class QuantityHistogramUnstyled extends React.Component {
}
handleItemClicked(item) {
if (this.props.value === item.name) {
if (this.props.value === item.key) {
this.props.onChanged(null)
} else {
this.props.onChanged(item.name)
this.props.onChanged(item.key)
}
}
......@@ -102,6 +108,7 @@ class QuantityHistogramUnstyled extends React.Component {
const data = Object.keys(this.props.data)
.map(key => ({
key: key,
name: mapKey(key),
value: this.props.data[key][this.props.metric]
}))
......@@ -142,19 +149,21 @@ class QuantityHistogramUnstyled extends React.Component {
svg.attr('height', height)
let withData = svg
.selectAll('g')
.selectAll('.item')
.data(data, data => data.name)
withData.exit().remove()
const rectColor = d => selected === d.name ? nomadPrimaryColor.main : nomadSecondaryColor.light
const textColor = d => selected === d.name ? '#FFF' : '#000'
const rectColor = d => selected === d.key ? nomadPrimaryColor.main : nomadSecondaryColor.light
const textColor = d => selected === d.key ? '#FFF' : '#000'
let item = withData.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))
......@@ -188,12 +197,55 @@ class QuantityHistogramUnstyled extends React.Component {
.style('cursor', 'pointer')
.on('click', d => this.handleItemClicked(d))
svg.select('.tooltip').remove()
let tooltip = svg.append('g')
tooltip
.attr('class', 'tooltip')
.style('visibility', 'hidden')
tooltip.append('rect')
.attr('class', 'tooltipbox')
.attr('rx', 6)
.attr('ry', 6)
.attr('width', 120)
.attr('height', y.bandwidth())
.attr('fill', 'grey')
.style('opacity', 1.0)
tooltip.append('text')
.attr('class', 'tooltiptext')
.attr('dy', '1.2em')
.attr('font-family', 'Arial, Helvetica, sans-serif')
.attr('font-size', '12px')
.attr('fill', 'white')
item
.on('mouseover', function(d, i) {
const xPosition = x(0)
const yPosition = y(d.name)
const key = mapKey(d.key, false)
svg.select('.tooltip')
.style('visibility', 'visible')
.attr('transform', `translate( ${xPosition}, ${yPosition})`)
.select('.tooltiptext')
.text(key)
const width = 10 * key.length
svg.select('.tooltip')
.select('.tooltipbox')
.attr('width', width)
})
.on('mouseout', function() {
svg.select('.tooltip')
.style('visibility', 'hidden')
})
const t = d3.transition().duration(500)
item = withData.transition(t)
item
.select('rect')
.select('.bar')
.attr('y', d => y(d.name))
.attr('width', d => x(d.value) - x(0))
.attr('height', y.bandwidth())
......
......@@ -17,6 +17,7 @@ import GroupList from './GroupList'
import ApiDialogButton from '../ApiDialogButton'
import SearchIcon from '@material-ui/icons/Search'
import UploadsChart from './UploadsChart'
import UploadersList from './UploadersList'
class Search extends React.Component {
static tabs = {
......@@ -255,11 +256,14 @@ class UsersVisualization extends React.Component {
const {open} = this.props
return <KeepState visible={open} render={() =>
<Card>
<CardContent>
<UploadsChart metricsDefinitions={domain.searchMetrics}/>
</CardContent>
</Card>
<div>
<Card>
<CardContent>
<UploadsChart metricsDefinitions={domain.searchMetrics}/>
</CardContent>
</Card>
<UploadersList />
</div>
}/>
}
}
......
......@@ -34,6 +34,7 @@ class SearchContext extends React.Component {
this.handleQueryChange = this.handleQueryChange.bind(this)
this.handleMetricChange = this.handleMetricChange.bind(this)
this.handleDomainChange = this.handleDomainChange.bind(this)
this.handleStatisticsToRefreshChange = this.handleStatisticsToRefreshChange.bind(this)
this.state.query = this.props.initialQuery || {}
if (this.props.initialRequest) {
this.state.request = {...this.state.request, ...this.props.initialRequest}
......@@ -56,7 +57,8 @@ class SearchContext extends React.Component {
metric: this.defaultMetric,
usedMetric: this.defaultMetric,
domain: domains.dft,
query: {}
query: {},
statisticsToRefresh: []
}
handleRequestChange(changes) {
......@@ -99,9 +101,15 @@ class SearchContext extends React.Component {
}
}
handleStatisticsToRefreshChange(statistics) {
let currentValue = this.state.statisticsToRefresh
currentValue.push(statistics)
this.setState({statisticsToRefresh: currentValue})
}
update() {
const {api, raiseError} = this.props
const {request, query, metric, domain} = this.state
const {request, query, metric, domain, statisticsToRefresh} = this.state
const search = {
...request,
...query,
......@@ -109,7 +117,7 @@ class SearchContext extends React.Component {
metrics: metric === this.defaultMetric ? [] : [metric],
...(this.props.query || {})}
api.search(search)
api.search(search, statisticsToRefresh)
.then(response => {
// find the first statistic to determine which metric is used
const {statistics} = response
......@@ -153,7 +161,8 @@ class SearchContext extends React.Component {
setRequest: this.handleRequestChange,
setQuery: this.handleQueryChange,
setMetric: this.handleMetricChange,
setDomain: this.handleDomainChange
setDomain: this.handleDomainChange,
setStatisticsToRefresh: this.handleStatisticsToRefreshChange
}
return <SearchContext.type.Provider value={value} >
{children}
......
import React from 'react'
import PropTypes from 'prop-types'
import Grid from '@material-ui/core/Grid'
import { Quantity } from './QuantityHistogram'
import { withStyles } from '@material-ui/core'
import SearchContext from './SearchContext'
import { compose } from 'recompose'
import { withApi } from '../api'
class UploadersList extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired
}
static styles = theme => ({
root: {
marginTop: theme.spacing.unit * 2
}
})
static contextType = SearchContext.type
componentDidMount() {
const {state: {query}, setQuery, setStatisticsToRefresh} = this.context
setQuery({...query, statistics_order: '_count'})
setStatisticsToRefresh('uploader')
}
render() {
const {state: {usedMetric}} = this.context
return (
<Grid>
<Quantity quantity="uploader" title="Uploaders" scale={1} metric={usedMetric} />
</Grid>
)
}
}
export default compose(withApi(false, false), withStyles(UploadersList.styles))(UploadersList)
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Select, MenuItem } from '@material-ui/core'
import Button from '@material-ui/core/Button'
import RefreshIcon from '@material-ui/icons/Refresh'
import Grid from '@material-ui/core/Grid'
import TextField from '@material-ui/core/TextField'
import * as d3 from 'd3'
......@@ -9,14 +11,12 @@ 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,
......@@ -34,7 +34,6 @@ class UploadsHistogramUnstyled extends React.Component {
super(props)
this.state = {
scalePower: this.props.defaultScale || 1.0,
interval: this.props.interval || '1M',
time: null,
from_time: 0,
until_time: 0
......@@ -65,41 +64,6 @@ class UploadsHistogramUnstyled extends React.Component {
}
]
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()
......@@ -112,15 +76,9 @@ class UploadsHistogramUnstyled extends React.Component {
}
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.setState({interval: newInterval}, () => this.handleQueryChange())
this.props.onChanged(from_time.toISOString(), until_time.toISOString())
}
handleTimeChange(newTime, key, target) {
......@@ -130,82 +88,54 @@ class UploadsHistogramUnstyled extends React.Component {
} 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 (key === 'from_time') {
if (this.state.from_time !== date.getTime()) {
this.setState({from_time: date.getTime()})
}
} else if (key === 'until_time') {
if (this.state.until_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) {
handleItemClicked(item, deltaT) {
const selected = item.time
if (selected === this.state.time) {
this.props.onChanged(null, null, null)
this.props.onChanged(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) {
resolveDate(name, deltaT) {
const date = new Date(parseInt(name, 10))
const year = date.toLocaleDateString(undefined, {year: 'numeric'})
const quarter = Math.floor((date.getMonth() + 3) / 3)
const month = date.toLocaleDateString(undefined, {month: 'short'})
const week = (date) => {
const first = new Date(date.getFullYear(), 0, 1)
return Math.ceil((((date - first) / 86400000) + first.getDay() + 1) / 7)
}
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
const times = [31536000, 7776000, 2419200, 604800, 864000, 3600, 60, 1]
const diffs = times.map(t => Math.abs(t - (deltaT / 1000)))
tooltip.attr('transform', `translate( ${xPosition}, ${yPosition})`)
tooltip.attr('data-html', 'true')
tooltip.select('text').text(new Date(d.time).toISOString() + ': ' + d.value)
})
const intervals = [year, 'Q' + quarter, month, 'W' + week, day, hour, min, sec]
return intervals[diffs.indexOf(Math.min(...diffs))]
}
updateChart() {
......@@ -215,7 +145,7 @@ class UploadsHistogramUnstyled extends React.Component {
} else {
data = Object.keys(this.props.data).map(key => ({
time: parseInt(key, 10),
name: this.resolveDate(key),
// name: this.resolveDate(key),
value: this.props.data[key][this.props.metric]
}))
}
......@@ -226,10 +156,17 @@ class UploadsHistogramUnstyled extends React.Component {
this.handleTimeChange(this.state.until_time, 'until_time', 'picker')
}
let deltaT = 31536000000
if (data.length > 1) {
deltaT = data[1].time - data[0].time
}
data.forEach(d => { d.name = this.resolveDate(d.time, deltaT) })
const scalePower = this.state.scalePower
const width = this.container.current.offsetWidth
const height = this.props.height
const margin = Math.round(0.1 * height)
const margin = Math.round(0.15 * height)
const x = scaleBand().rangeRound([margin, width]).padding(0.1)
const y = scalePow().range([height - margin, margin]).exponent(scalePower)
......@@ -258,7 +195,7 @@ class UploadsHistogramUnstyled extends React.Component {
.attr('font-size', '12px')
.style('text-anchor', 'end')
const yAxis = d3.axisLeft(y)
const yAxis = d3.axisLeft(y).ticks(Math.min(max, 5), '.0s')
svg.select('.yaxis').remove()
svg.append('g')
.attr('transform', `translate(${margin}, 0)`)
......@@ -270,9 +207,8 @@ class UploadsHistogramUnstyled extends React.Component {
svg.append('text')
.attr('class', 'ylabel')
.attr('x', 0)
.attr('y', 0)
.attr('y', 10)
.attr('dy', '1em')
.attr('text-anchor', 'start')
.attr('font-size', '12px')
.text(`${shortLabel || label}`)
......@@ -294,16 +230,53 @@ class UploadsHistogramUnstyled extends React.Component {
item
.style('cursor', 'pointer')
.on('click', d => this.handleItemClicked(d))
.on('click', d => this.handleItemClicked(d, deltaT))
svg.select('.tooltip').remove()
svg.call(this.hover, item)
let tooltip = svg.append('g')
tooltip
.attr('class', 'tooltip')
.style('visibility', 'hidden')
tooltip.append('rect')
.attr('x', 0)
.attr('rx', 6)
.attr('ry', 6)
.attr('width', 100)
.attr('height', 40)
.attr('fill', 'grey')
.style('opacity', 1.0)
let tooltipText = tooltip.append('text')
.attr('dy', '1.2em')
.attr('font-family', 'Arial, Helvetica, sans-serif')
.attr('font-size', '10px')
.attr('fill', 'white')
.style('text-anchor', 'middle')
item
.on('mouseover', function(d) {
tooltip.style('visibility', 'visible')
const date = new Date(d.time)
const value = `${date.toLocaleDateString()}\n
${date.toLocaleTimeString()}
${d.value} ${shortLabel || label}`
tooltipText.selectAll('tspan')
.data((value).split(/\n/)).join('tspan')
.attr('x', 50)
.attr('y', (d, i) => `${i * 1.2}em`)
.text(d => d)
const xPosition = x(d.name) + 20
const yPosition = y(d.value) - 20
tooltip.attr('transform', `translate( ${xPosition}, ${yPosition})`)
})
.on('mouseout', () => tooltip.style('visibility', 'hidden'))
}