labo-components
Version:
383 lines (337 loc) • 15.7 kB
JSX
import moment from 'moment';
import React from 'react';
import PropTypes from "prop-types";
import Select from 'react-select';
import IDUtil from '../../util/IDUtil';
import TimeUtil from '../../util/TimeUtil';
import {Bar, BarChart, CartesianGrid, Label, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts';
import SearchAPI from "../../api/SearchAPI";
import CustomTooltip from './helpers/CustomTooltip';
import Loading from '../../components/shared/Loading';
import FlexRouter from '../../util/FlexRouter';
import LocalStorageHandler from '../../util/LocalStorageHandler';
//implemented using recharts: http://recharts.org/en-US/examples
//FIXME part of this code is duplicate with whatever is in the QueryComparisonLineChart!
export default class ComparisonHistogram extends React.Component {
constructor(props) {
super(props);
this.state = {
viewMode: 'absolute', // Sets default view mode to absolute.
referenceSelection: 'to the collection', // default is that the collection is the reference
relData: null,
absData: this.getJoinedData(this.props.data, 'year') || null,
searchIds: this.getSearchIds(this.props.data) || null,
isLoading: false,
dateUnit: 'year'
};
PropTypes.checkPropTypes(ComparisonHistogram.propTypes, this.props, 'prop', this.constructor.name);
}
async getQueryData(key) {
const chartData = this.getChartDataByKey(key);
let query = null;
this.setDateUnit(chartData.query, this.state.dateUnit);
//get the values for each query
query = {
...chartData.query
};
//FIXME this is not right, fix this with the query model
return new Promise(function(resolve, reject) {
SearchAPI.search(
query,
chartData.collectionConfig,
data => resolve(data),
false
)
})
}
async getReferenceData(key) {
const chartData = this.getChartDataByKey(key);
let query = null;
this.setDateUnit(chartData.query, this.state.dateUnit);
if (this.state.referenceSelection === 'to the collection')
{
//get the values for the whole collection as reference
query = {
...chartData.query,
term: '',
selectedFacets: {},
fieldCategory: []
};
}
else
{
//get the values for the query without search term as reference
query = {
...chartData.query,
term: ''
};
}
//FIXME this is not right, fix this with the query model
return new Promise(function(resolve, reject) {
SearchAPI.search(
query,
chartData.collectionConfig,
data => resolve(data),
false
)
})
}
setDateUnit = (query, dateUnit) => {
//set the desired date unit in the date histogram facets
const df = query.desiredFacets;
df.filter(facet => facet.type === 'date_histogram').forEach(function(facet, index, df){df[index].dateUnit = dateUnit})
}
getSearchIds = dataSets => dataSets.map(item => item.query.searchId)
/*
Gets an object with query info with queryId as key.
Every object has the data as an array {queryId: 'xxx', <dateUnit>: 'xxxx'}
*/
__getGraphData = (searchResultsList, returnedType, dateUnit) => {
let dataArr = [];
let dataObj = {};
searchResultsList.forEach(searchResults => {
const dateBuckets = searchResults.aggregations[searchResults.query.dateRange.field].filter(
dateInfo => dateInfo && dateInfo.hasOwnProperty('date_millis') // make sure this exists
).map(dateInfo => { //add two fields
const extraInfo = {date : TimeUtil.dateToInterval(dateInfo.date_millis, dateUnit)}
extraInfo[searchResults.searchId] = dateInfo.doc_count
return Object.assign(dateInfo, extraInfo)
});
if(returnedType === 'arr') {
dataArr.push(dateBuckets)
} else {
dataObj[searchResults.searchId] = dateBuckets
}
});
return returnedType === 'arr' ? dataArr : dataObj;
};
getJoinedData = (dataSet, dateUnit) => {
const relGraphData = this.__getGraphData(dataSet, 'arr', dateUnit); //Note: this is a list of arrays!
//first get a range for the dates in the datasets
const __calcDate= (queryResultList, mathFunc) => {
// returns an array with 1 array per query with the years only
let calcDates = queryResultList.map(dateList => {
const milliList = dateList.map(d => d.date_millis);
return mathFunc(...milliList);
});
return mathFunc(...calcDates);
};
const minDate = moment(new Date(__calcDate(relGraphData, Math.min)));
const maxDate = moment(new Date(__calcDate(relGraphData, Math.max)));
let numberOfIntervals = maxDate.diff(minDate, dateUnit + 's') + 1;
//generates an array of dates based on the min & max dates
const __validRange = Array.from(new Array(numberOfIntervals), (val,index) => {let newDate = minDate.clone(); newDate.add(index, dateUnit); return newDate});
//now organise the data for each of these dates
return __validRange.map(date => {
let tempObj = {};
let dateInterval = TimeUtil.dateToInterval(date.toDate(), dateUnit);
relGraphData.forEach(set => {
const matchData = set.find(it => it.date === dateInterval);
if (matchData !== undefined) {
tempObj = {...tempObj, ...matchData};
} else {
tempObj = {...tempObj, ...{'date': dateInterval}};
}
});
return tempObj
});
};
getRelValues = (absoluteValues, relativeValues, dateUnit) => {
const relVals = this.__getGraphData(relativeValues, 'obj', dateUnit);
return absoluteValues.map(point => {
const relPoint = {date: point["date"], "date_millis": point["date_millis"]};
Object.keys(point).forEach(prop => {
if (prop !== 'date' && prop !== 'key' && prop !== 'date_millis' && prop !== 'key_as_string' && prop !== 'doc_count') {
const tt = relVals[prop].find(obj => obj.date === point.date);
relPoint[prop] = (tt[prop] === 0 || point[prop] === 0
? 0
: point[prop] / tt[prop] * 100
);
}
});
return relPoint
})
};
onRelativeDataReceived = (relativeData) => { //array containing instances of SearchResults
const relativeValues = this.getRelValues(this.state.absData, relativeData, this.state.dateUnit);
this.setState({
viewMode: 'relative',
relData: relativeValues,
isLoading: false
}
);
};
onQueryDataReceived = (queryData) => { //array containing instances of SearchResults
const absValues = this.getJoinedData(queryData, this.state.dateUnit);
if(this.state.viewMode === 'relative')
{
this.state.absData = absValues;
this.processRelativeData(this.props.data);
}
else
{
this.setState({
viewMode: 'absolute',
absData: absValues,
isLoading: false
}
);
}
};
getChartDataByKey = (key) => {
return this.props.data[key];
}
viewQueryIntervalOnSearchPage = e => {
if (e.tooltipPayload && e.tooltipPayload[0].dataKey)
{
//retrieve query to match selected bar
let query = JSON.parse(JSON.stringify(this.props.data.find(element => element.searchId == e.tooltipPayload[0].dataKey).query));
if(query){
//modify date to match selected bar
const intervalLimits = TimeUtil.dateToStartAndEndInterval(e.date_millis, this.state.dateUnit, "YYYY-MM-DD");
if(intervalLimits.length === 2){
query.searchId = null;
query.dateRange.start = intervalLimits[0];
query.dateRange.end = intervalLimits[1];
//store the query in the cache
LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-priority-query');
LocalStorageHandler.storeJSONInLocalStorage(
'stored-search-results',
{query: query} //only the query, the Search page will fill the results
);
// hide the Search histogram as the minimum interval is 1 year, so it is meaningless and takes up space
LocalStorageHandler.storeJSONInLocalStorage(
'state-show-timeline',
false
);
//open the Search page
FlexRouter.gotoSingleSearch('cache');
}
}
}
};
async processRelativeData(data) {
const promises = Object.keys(data).map(this.getReferenceData.bind(this));
await Promise.all(promises).catch(d => console.log(d)).then(
dataPerQuery => this.onRelativeDataReceived(dataPerQuery)
);
}
async processQueryData(data, dateUnit) {
const promises = Object.keys(data).map(this.getQueryData.bind(this));
await Promise.all(promises).catch(d => console.log(d)).then(
dataPerQuery => this.onQueryDataReceived(dataPerQuery)
);
}
switchViewMode = () => {
//need new data if viewmode is absolute (means new viewmode will be relative) and EITHER:
//- there is no relative data
// OR
// - the relative data is not relative to the collection (when switching mode to relative the default is 'to the collection')
const newDataNeeded = this.state.viewMode === 'absolute' && (this.state.relData === null ||(this.state.referenceSelection!= "to the collection"))
this.setState({
isLoading: newDataNeeded,
viewMode: this.state.viewMode === 'relative' ? 'absolute' : 'relative'
}, () => {
if(newDataNeeded) {
this.state.referenceSelection = "to the collection" //switching from absolute to relative so set to the default ref
this.processRelativeData(this.props.data, this.state.dateUnit)
}
});
};
switchReference = (e) => {
this.state.referenceSelection = e.target.checked ? 'to the query facets': 'to the collection';
this.setState({isLoading: true})
this.processRelativeData(this.props.data, this.state.dateUnit);
}
onDateUnitChange = (e) => {
this.setState({isLoading: true})
this.state.dateUnit = e.value;
this.processQueryData(this.props.data, this.state.dateUnit);
}
renderStackBars = dataKeys => dataKeys.map((searchId, index) => (
<Bar
key={searchId}
isAnimationActive={true}
dataKey={searchId}
fill={this.props.queryStats[searchId].color}
stackId="a"
name=""
onClick={this.viewQueryIntervalOnSearchPage}
/>)
);
renderReferenceSelector = () => (
<div className="ms_toggle_btn ms_toggle_btn_2" >
<input id="toggle-2" className="checkbox-toggle checkbox-toggle-round" type="checkbox" onClick={this.switchReference}/>
<label htmlFor="toggle-2" data-on="to the query facets" data-off="to the collection"/>
</div>
);
renderDateUnitSelector = (selectedDateUnit, dateOptions) => (
<Select
className="basic-single date_unit_selector"
classNamePrefix="select"
defaultValue={selectedDateUnit ? dateOptions.find(option => option.value == selectedDateUnit) : dateOptions[3]}
name="date-unit"
options={dateOptions}
onChange={this.onDateUnitChange}
/>
);
render() {
const dateOptions = [
{value: 'day', label: 'day'},
{value: 'week', label: 'week'},
{value: 'month', label: 'month'},
{value: 'year', label: 'year'}]
const random = Math.floor(Math.random() * 1000) + 1;
const dataToPrint = this.state.viewMode === 'relative' ? this.state.relData ? this.state.relData : null : this.state.absData;
const yaxisLabel = this.state.viewMode === 'relative' ? '% compared to ' + this.state.dateUnit : 'Number of records';
const bars = this.state.searchIds ? this.renderStackBars(this.state.searchIds) : null;
const loadingMsg = this.state.isLoading ? <Loading message="Loading graph..."/> : null;
const referenceSelector = this.state.viewMode === 'relative' ? this.renderReferenceSelector() : null;
const dateUnitSelector = this.renderDateUnitSelector(this.state.dateUnit, dateOptions);
return (
<div className={IDUtil.cssClassName('query-comparison-histogram')}>
<div className="ms_toggle_btns">
<div className="ms_toggle_btn ms_toggle_btn_1" >
<input id="toggle-1" className="checkbox-toggle checkbox-toggle-round" type="checkbox" onClick={this.switchViewMode}/>
<label htmlFor="toggle-1" data-on="Relative" data-off="Absolute"/>
</div>
{referenceSelector}
</div>
{loadingMsg}
<ResponsiveContainer width="100%" minHeight="400px" height="40%">
<BarChart
key={random}
width={1200}
height={250}
data={dataToPrint}
margin={{top: 5, right: 20, bottom: 5, left: 0}}>
<CartesianGrid strokeDasharray="3 3"/>
<XAxis dataKey="date" height={100}>
<Label
value= {this.state.dateUnit + " (selected date field varies per query)"}
position="outside"
offset={0}
style={{fontSize: 1.4 + 'rem', fontWeight:'bold'}}
/>
</XAxis>
<YAxis width={100} >
<Label
value={yaxisLabel}
offset={30}
position="insideBottomLeft"
angle={-90}
style={{fontSize: 1.4 + 'rem', fontWeight:'bold', height: 460 + 'px', width: 100 + 'px' }}/>
</YAxis>
<Tooltip content={<CustomTooltip active={false} payload={[]} queryStats={this.props.queryStats} viewMode={this.state.viewMode}/>}/>
{bars}
</BarChart>
</ResponsiveContainer>
{dateUnitSelector}
</div>
)
}
}
ComparisonHistogram.propTypes = {
data: PropTypes.array.isRequired,
queryStats: PropTypes.object.isRequired
};