UNPKG

labo-components

Version:
227 lines (208 loc) 9.76 kB
import React from 'react'; import IDUtil from '../../util/IDUtil'; import ComponentUtil from '../../util/ComponentUtil'; import {LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip, Label, ResponsiveContainer, BarChart, Legend, Bar} from 'recharts'; import TimeUtil from '../../util/TimeUtil'; import SearchAPI from '../../api/SearchAPI'; import PropTypes from 'prop-types'; import Loading from '../../components/shared/Loading'; //TODO visualise the out of range dates somehow //TODO move same functions as in Histogram into something else export default class QuerySingleLineChart extends React.Component { constructor(props) { super(props); this.state = { viewMode: 'absolute', // Sets default view mode to absolute. relativeData : null, //loaded for the first time after switching to 'relative' isLoading: false } } calcDateInRange = aggregation => { let startMillis = null; let endMillis = null; if(this.props.query.dateRange) { startMillis = this.props.query.dateRange.start endMillis = this.props.query.dateRange.end } if ((startMillis != null && aggregation.date_millis < startMillis) || endMillis != null && aggregation.date_millis > endMillis) { return false; } return true; }; switchViewMode = () => { if (this.state.viewMode === 'absolute') { if (this.state.relativeData === null) { this.fetchRelativeData({ ...this.props.query, term: '', selectedFacets: {}, dateRange: { ...this.props.query.dateRange, end: null, start: null } }); } else { // there is already relative Data available to draw graph. this.setState({viewMode: 'relative', isLoading: false}) } } else { this.setState({viewMode: 'absolute', isLoading: false}) } }; fetchRelativeData = query => { this.setState({ isLoading : true }, () => { SearchAPI.search(query, this.props.collectionConfig, this.onRelativeDataFetched, false); }); }; onRelativeDataFetched = resultsObj => { //instance of SearchResults if (resultsObj && resultsObj.aggregations && !resultsObj.error) { const totalDateRangeCounts = resultsObj.aggregations[resultsObj.query.dateRange.field]; const commonData = totalDateRangeCounts.filter(aggr => { return this.props.data.find(absAggr => absAggr.key === aggr.key) != null; }); this.setState({relativeData: commonData, viewMode: 'relative', isLoading: false}); } }; calcRelativePercentage = (absCount, totalCount) => { return absCount !== 0 && totalCount !== 0 ? ((absCount / totalCount) * 100) : 0; }; filterByCurrentRange = e => { if( e.payload && e.payload.date && e.payload.count) { this.props.onClick(e.payload.date); } }; //TODO better ID!! (include some unique part based on the query) render() { const strokeColors = ['#8884d8', 'green']; let dataPrettyfied = null; if(this.props.data) { if(this.state.viewMode === 'absolute') { dataPrettyfied = this.props.data.map((absData, i) => { const point = {}; point["dataType"] = 'absolute'; point["strokeColor"] = strokeColors[0]; //this.calcDateInRange(absData) ? strokeColors[0] : strokeColors[1]; point["date"] = TimeUtil.getYearFromDate(absData.date_millis); point["count"] = absData ? absData.doc_count : 0; return point; }); } else if(this.state.relativeData) { dataPrettyfied = this.props.data.map((absData, i) => { const relData = this.state.relativeData.find(x => x.key === absData.key); const point = {}; point["dataType"] = 'relative'; point["strokeColor"] = strokeColors[0]; //this.calcDateInRange(absData) ? strokeColors[0] : strokeColors[1]; point["date"] = TimeUtil.getYearFromDate(absData.date_millis); point["count"] = relData ? this.calcRelativePercentage(absData.doc_count, relData.doc_count) : 0; //FIXME this should never happen, but still... return point; }); } else { console.error('this should never happen') } } if(dataPrettyfied) { const loadingMsg = this.state.isLoading ? <Loading message="Loading graph..."/> : null ; const hitsOutsideRange = this.props.data.filter(aggr => !this.calcDateInRange(aggr)).reduce((acc, cur) => acc += cur.doc_count, 0); const totalHits = this.props.data.reduce((acc, cur) => acc += cur.doc_count, 0); let legendTitle = 'Timeline chart of query results '; if(hitsOutsideRange > 0) { legendTitle += '(' + ComponentUtil.formatNumber(hitsOutsideRange) + " / " + ComponentUtil.formatNumber(totalHits) + ' hits out of range)'; } const prettySelectedFieldName = this.props.collectionConfig.toPrettyFieldName(this.props.query.dateRange.field); return ( <div className={IDUtil.cssClassName('query-line-chart')}> {loadingMsg} <span className="ms_toggle_btn" > <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"/> </span> <ResponsiveContainer width="100%" minHeight="360px" height="40%"> <LineChart key={this.state.viewMode} width={600} height={300} data={dataPrettyfied} margin={{top: 5, right: 30, left: 20, bottom: 5}}> <Legend verticalAlign="top" height={36}/> <CartesianGrid strokeDasharray="1 6"/> <XAxis dataKey="date" height={100}> <Label value={prettySelectedFieldName} offset={0} position="outside" style={{fontSize: 1.4 + 'rem', fontWeight:'bold'}}/> </XAxis> <YAxis tickFormatter={ComponentUtil.formatNumber} width={100} > <Label value={this.state.viewMode === 'absolute' ? "Number of records" : "Percentage of records"} offset={10} position="insideBottomLeft" angle={-90} style={{fontSize: 1.4 + 'rem', fontWeight:'bold', height: 460 + 'px', width: 100 + 'px' }}/> </YAxis> <Tooltip content={<CustomTooltip viewMode={this.state.viewMode}/>}/> <Line isAnimationActive={true} dataKey="count" stroke={dataPrettyfied[0].strokeColor} activeDot={{ onClick: (event, payload) => this.filterByCurrentRange(payload), r: 10 }} name={legendTitle}/> </LineChart> </ResponsiveContainer> </div> ) } else { return ( <div className={IDUtil.cssClassName('query-line-chart')}> Loading data... </div> ) } } } 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.index); }} cx={x} cy={y} r={4} fill="transparent"/> ); } } // Custom tooltip. // TODO: Make it a separated component more customizable. class CustomTooltip extends React.Component{ render() { const {active} = this.props; if (active) { const {payload, label, viewMode} = this.props; if(payload && label){ if (viewMode === 'relative') { let relativeValue = payload[0].value ? parseFloat(payload[0].value.toFixed(2)) : 0; return ( <div className="ms__custom-tooltip"> <h4>{viewMode} value</h4> <p>Year: <span className="rightAlign">{`${label}`}</span></p> <p>Percentage: <span className="rightAlign">{ComponentUtil.formatNumber(relativeValue)}%</span></p> </div> ); } else { return ( <div className="ms__custom-tooltip"> <h4>{viewMode} value</h4> <p>Year: <span className="rightAlign">{`${label}`}</span> </p> <p>Total: <span className="rightAlign">{ComponentUtil.formatNumber(payload[0].value)}</span></p> </div> ); } } } return null; } } CustomTooltip.propTypes = { payload: PropTypes.array, label: PropTypes.number, viewMode: PropTypes.string.isRequired, //relative or absolute };