UNPKG

labo-components

Version:
383 lines (337 loc) 15.7 kB
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 };