src/domain/metric/component/WordCloud.js
// @flow
import React from 'react';
import _ from 'lodash';
import Measure from 'react-measure';
import Cloud from './Cloud';
import Card from 'infra/component/Card';
import { syncToPromise, cancelablePromise } from 'infra/service/util';
import type { TimeRangeMetric } from '../service/timeRangeMetric'
export const WordCloudTypes = {
POSTS: 'POSTS',
COMMENTS: 'COMMENTS',
ALL: 'ALL',
};
export type WordCloudType = $Keys<typeof WordCloudTypes>
type WordCount = { word: string, count: number }
const calculate = (type: WordCloudType, metric: TimeRangeMetric): Promise<WordCount[]> => {
let promises = [];
switch (type) {
case WordCloudTypes.POSTS:
promises = [metric.postsMetric.wordCount()];
break;
case WordCloudTypes.COMMENTS:
promises = [metric.commentsMetric.wordCount()];
break;
case WordCloudTypes.ALL:
default:
promises = [metric.postsMetric.wordCount(), metric.commentsMetric.wordCount()];
}
return Promise.all(promises)
.then(res => syncToPromise(() => {
const allCount: { [string]: { word: string, count: number } } = {};
res.reduce((pre, cur) => [...pre, ...cur], []).forEach(wor => {
if (!allCount[wor.word]) allCount[wor.word] = { word: wor.word, count: 0 };
allCount[wor.word].count += wor.count;
});
return _.values(allCount);
}));
};
const generateWidth = (rawWidth?: number): number => {
if (!rawWidth) return 400;
if (rawWidth === 400) return 273;
return rawWidth;
};
const generateHeight = (data: any[]): number => {
const length = data.length;
let height = 400;
if (length < 30) {
height = 200;
} else if (length < 50) {
height = 250;
} else if (length < 80) {
height = 300;
} else if (length < 120) {
height = 350;
}
return height;
};
const generateTitle = (type: WordCloudType): string => {
switch (type) {
case WordCloudTypes.ALL:
return 'Word Cloud from Posts and Comments';
case WordCloudTypes.POSTS:
return 'Word Cloud from Posts';
case WordCloudTypes.COMMENTS:
return 'Word Cloud from Comments';
default:
return 'Word Cloud';
}
};
class WordCloud extends React.Component {
props: {
metric: TimeRangeMetric,
type: WordCloudType,
}
state : {
loading: boolean,
data: { text: string, value: number }[],
}
cancelable: { promise: Promise<WordCount[]>, cancel: Function }
generateData: Function
constructor(props: Object) {
super(props);
this.generateData = this.generateData.bind(this);
this.state = {
loading: true,
data: [],
};
}
componentWillMount() {
this.generateData(this.props);
}
componentWillReceiveProps(nextProps: Object) {
this.generateData(nextProps);
}
generateData(props: Object) {
this.setState({ loading: true, data: [] });
const { metric, type } = {
type: WordCloudTypes.ALL,
...props,
};
this.cancelable = cancelablePromise(calculate(type, metric));
this.cancelable.promise
.then(res => {
const rawData = res.map(wor => ({ text: wor.word, value: wor.count }));
const data = _.sortBy(rawData, 'value')
.reverse()
.filter(dat => dat.value > 1)
.slice(0, 300);
return data;
})
.then(data => this.setState({ loading: false, data }))
.catch(err => {
if (err.message !== 'cancelled') this.setState({ loading: false });
});
}
componentWillUnmount() {
this.cancelable.cancel();
}
render() {
const { type } = this.props;
const { loading, data } = this.state;
let content;
if (!loading && data.length > 0) {
content = (<Measure>
{ dimensions => (
<Cloud
width={generateWidth(dimensions.width)}
height={generateHeight(data)}
data={data}
/>
)}
</Measure>);
} else if (loading) {
content = <div>Counting words...</div>
} else {
content = <div>Not enough data.</div>
}
return (
<Card title={generateTitle(type)}>
{content}
</Card>
);
};
}
export default WordCloud;