labo-components
Version:
427 lines (373 loc) • 17 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 ComponentUtil from '../../util/ComponentUtil';
import {LineChart, Label, Line, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer} from 'recharts';
import SearchAPI from '../../api/SearchAPI';
import ElasticsearchDataUtil from "../../util/ElasticsearchDataUtil";
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 ComparisonHistogram!
export default class QueryComparisonLineChart extends React.Component {
constructor(props) {
super(props);
this.state = {
viewMode: this.props.data.total ? 'inspector' : 'absolute',
referenceSelection: 'to the collection', // default is that the collection is the reference
absData: this.getJoinedData(this.props.data, 'year') || null,
searchIds: this.getSearchIds(this.props.data) || null,
relData : null,
isLoading: false,
dateUnit: 'year'
};
PropTypes.checkPropTypes(QueryComparisonLineChart.propTypes, this.props, 'prop', this.constructor.name);
}
shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.data !== this.props.data
|| nextState.viewMode !== this.state.viewMode
|| this.state.isLoading !== nextState.isLoading
);
}
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
}
);
}
};
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
}
);
};
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
})
};
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)
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);
}
/*
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
});
};
getChartDataByKey = (key) => {
return this.props.data[key];
}
viewQueryIntervalOnSearchPage = (e, searchId, clickedIndex) => {
if (searchId)
{
//retrieve query to match selected bar
let query = JSON.parse(JSON.stringify(this.props.data.find(element => element.searchId == searchId).query));
let date_millis = this.state.absData[clickedIndex].date_millis;
if(query && date_millis){
//modify date to match selected bar
const intervalLimits = TimeUtil.dateToStartAndEndInterval(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.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');
}
}
}
}
renderLines = dataKeys => dataKeys.map((searchId, index) => (
<Line
key={searchId}
isAnimationActive={true}
name=""
label={<LabelAsPoint key={searchId} onClick={this.viewQueryIntervalOnSearchPage}/>} //the LabelAsPoint class handles the onclick of a dot
type="monotone"
dataKey={searchId}
stroke={this.props.queryStats[searchId].color}
dot={{stroke: this.props.queryStats[searchId].color, strokeWidth: 2}}
activeDot = {false}
/>));
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}
/>
);
//TODO better ID!! (include some unique part based on the query)
render() {
const dateOptions = [
{value: 'day', label: 'day'},
{value: 'week', label: 'week'},
{value: 'month', label: 'month'},
{value: 'year', label: 'year'}]
const lines = this.renderLines(this.state.searchIds);
const timelineData = this.state.viewMode === 'relative' ? this.state.relData : this.state.absData;
const yaxisLabel = this.state.viewMode === 'relative' ? '% compared to ' + this.state.dateUnit : 'Number of records';
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-line-chart')}>
<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%">
<LineChart
width={1200}
height={250}
data={timelineData}
margin={{top: 5, right: 20, bottom: 5, left: 0}}>
<CartesianGrid stroke="#cacaca"/>
<XAxis dataKey="date" height={100} tickSize={10}>
<Label
value= {this.state.dateUnit + " (selected date field varies per query)"}
position="outside"
offset={100}
style={{fontSize: 1.4 + 'rem', fontWeight:'bold'}}
/>
</XAxis>
<YAxis width={100} tickFormatter={ComponentUtil.formatNumber}>
<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}
/>
}/>
{lines}
</LineChart>
</ResponsiveContainer>
{dateUnitSelector}
</div>
)
}
}
QueryComparisonLineChart.propTypes = {
data: PropTypes.array.isRequired,
queryStats: PropTypes.object
};
class LabelAsPoint extends React.Component {
constructor(props) {
super(props);
}
render() {
const {x, y} = this.props;
return (
<circle
className="dot"
onClick={(e) => {
this.props.onClick(e, this.props.content.key, this.props.index);
}}
cx={x}
cy={y}
r={4}
fill="transparent"/>
);
}
}