labo-components
Version:
227 lines (208 loc) • 9.76 kB
JSX
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
};