@@ -21,6 +21,7 @@ import * as React from 'react'; | |||
import { throttle } from 'lodash'; | |||
import CountUp from 'react-countup'; | |||
import { formatMeasure } from '../../../../helpers/measures'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../../helpers/urls'; | |||
import './Statistics.css'; | |||
@@ -83,9 +84,14 @@ export class StatisticCard extends React.PureComponent<StatisticCardProps, Stati | |||
render() { | |||
const { statistic } = this.props; | |||
const formattedString = formatMeasure(statistic.value, 'SHORT_INT'); | |||
const value = parseFloat(formattedString.slice(0, -1)); | |||
const suffix = formattedString.substr(-1); | |||
const formattedString = formatMeasure(statistic.value, 'SHORT_INT', { | |||
roundingFunc: Math.floor | |||
}); | |||
const value = parseFloat(formattedString); | |||
let suffix = formattedString.replace(value.toString(), ''); | |||
if (suffix === translate('short_number_suffix.g')) { | |||
suffix = ' ' + translate('billion'); | |||
} | |||
return ( | |||
<div className="sc-stat-card sc-big-spacer-top" ref={node => (this.container = node)}> | |||
<div className="sc-stat-icon"> |
@@ -18,19 +18,40 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { shallow, ShallowWrapper } from 'enzyme'; | |||
import Statistics, { StatisticCard } from '../Statistics'; | |||
const STATISTICS = { | |||
icon: 'stat-icon', | |||
text: 'my stat', | |||
value: 26666 | |||
}; | |||
const STATISTICS = { icon: 'stat-icon', text: 'my stat', value: 26666 }; | |||
it('should render', () => { | |||
expect(shallow(<Statistics statistics={[STATISTICS]} />)).toMatchSnapshot(); | |||
}); | |||
it('should render StatisticCard', () => { | |||
expect(shallow(<StatisticCard statistic={STATISTICS} />)).toMatchSnapshot(); | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should render big numbers correctly', () => { | |||
function checkCountUp(wrapper: ShallowWrapper, end: number, suffix: string) { | |||
expect(wrapper.find('CountUp').prop('end')).toBe(end); | |||
expect(wrapper.find('CountUp').prop('suffix')).toBe(suffix); | |||
} | |||
checkCountUp( | |||
shallowRender({ statistic: { ...STATISTICS, value: 999003632 } }), | |||
999, | |||
'short_number_suffix.m' | |||
); | |||
checkCountUp( | |||
shallowRender({ statistic: { ...STATISTICS, value: 999861538 } }), | |||
999, | |||
'short_number_suffix.m' | |||
); | |||
checkCountUp(shallowRender({ statistic: { ...STATISTICS, value: 1100021731 } }), 1.1, ' billion'); | |||
}); | |||
function shallowRender(props: Partial<StatisticCard['props']> = {}) { | |||
const wrapper = shallow(<StatisticCard statistic={STATISTICS} {...props} />); | |||
wrapper.setState({ viewable: true }); | |||
return wrapper; | |||
} |
@@ -33,6 +33,14 @@ exports[`should render StatisticCard 1`] = ` | |||
<div | |||
className="sc-stat-content" | |||
> | |||
<CountUp | |||
delay={0} | |||
duration={4} | |||
end={26} | |||
suffix="short_number_suffix.k" | |||
> | |||
<Component /> | |||
</CountUp> | |||
<span> | |||
my stat | |||
</span> |
@@ -60,7 +60,12 @@ describe('#formatMeasure()', () => { | |||
expect(formatMeasure(1529, 'SHORT_INT')).toBe('1.5k'); | |||
expect(formatMeasure(10000, 'SHORT_INT')).toBe('10k'); | |||
expect(formatMeasure(10678, 'SHORT_INT')).toBe('11k'); | |||
expect(formatMeasure(1234567890, 'SHORT_INT')).toBe('1G'); | |||
expect(formatMeasure(9467890, 'SHORT_INT')).toBe('9.5M'); | |||
expect(formatMeasure(994567890, 'SHORT_INT')).toBe('995M'); | |||
expect(formatMeasure(999000001, 'SHORT_INT')).toBe('999M'); | |||
expect(formatMeasure(999567890, 'SHORT_INT')).toBe('1G'); | |||
expect(formatMeasure(1234567890, 'SHORT_INT')).toBe('1.2G'); | |||
expect(formatMeasure(11234567890, 'SHORT_INT')).toBe('11G'); | |||
}); | |||
it('should format FLOAT', () => { | |||
@@ -130,8 +135,8 @@ describe('#formatMeasure()', () => { | |||
expect(formatMeasure(-1 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('-1min'); | |||
expect(formatMeasure(1529 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('1.5kd'); | |||
expect(formatMeasure(1234567 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('1Md'); | |||
expect(formatMeasure(1234567 * ONE_DAY + 2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('1Md'); | |||
expect(formatMeasure(1234567 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('1.2Md'); | |||
expect(formatMeasure(12345670 * ONE_DAY + 4 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('12Md'); | |||
}); | |||
it('should format RATING', () => { |
@@ -130,18 +130,44 @@ function intFormatter(value: number): string { | |||
return numberFormatter(value); | |||
} | |||
function shortIntFormatter(value: number): string { | |||
if (value >= 1e9) { | |||
return numberFormatter(value / 1e9) + translate('short_number_suffix.g'); | |||
} else if (value >= 1e6) { | |||
return numberFormatter(value / 1e6) + translate('short_number_suffix.m'); | |||
} else if (value >= 1e4) { | |||
return numberFormatter(value / 1e3) + translate('short_number_suffix.k'); | |||
} else if (value >= 1e3) { | |||
return numberFormatter(value / 1e3, 0, 1) + translate('short_number_suffix.k'); | |||
} else { | |||
return numberFormatter(value); | |||
const shortIntFormats = [ | |||
{ unit: 1e10, formatUnit: 1e9, fraction: 0, suffix: 'short_number_suffix.g' }, | |||
{ unit: 1e9, formatUnit: 1e9, fraction: 1, suffix: 'short_number_suffix.g' }, | |||
{ unit: 1e7, formatUnit: 1e6, fraction: 0, suffix: 'short_number_suffix.m' }, | |||
{ unit: 1e6, formatUnit: 1e6, fraction: 1, suffix: 'short_number_suffix.m' }, | |||
{ unit: 1e4, formatUnit: 1e3, fraction: 0, suffix: 'short_number_suffix.k' }, | |||
{ unit: 1e3, formatUnit: 1e3, fraction: 1, suffix: 'short_number_suffix.k' } | |||
]; | |||
function shortIntFormatter( | |||
value: number, | |||
option?: { roundingFunc?: (x: number) => number } | |||
): string { | |||
const roundingFunc = (option && option.roundingFunc) || undefined; | |||
for (let i = 0; i < shortIntFormats.length; i++) { | |||
const { unit, formatUnit, fraction, suffix } = shortIntFormats[i]; | |||
const nextFraction = unit / (shortIntFormats[i + 1] ? shortIntFormats[i + 1].unit / 10 : 1); | |||
const roundedValue = numberRound(value / unit, nextFraction, roundingFunc); | |||
if (roundedValue >= 1) { | |||
return ( | |||
numberFormatter( | |||
numberRound(value / formatUnit, Math.pow(10, fraction), roundingFunc), | |||
0, | |||
fraction | |||
) + translate(suffix) | |||
); | |||
} | |||
} | |||
return numberFormatter(value); | |||
} | |||
function numberRound( | |||
value: number, | |||
fraction: number = 1000, | |||
roundingFunc: (x: number) => number = Math.round | |||
) { | |||
return roundingFunc(value * fraction) / fraction; | |||
} | |||
function floatFormatter(value: number): string { |
@@ -18,6 +18,7 @@ any=Any | |||
ascending=Ascending | |||
assignee=Assignee | |||
author=Author | |||
billion=Billion | |||
bitbucket=Bitbucket | |||
back=Back | |||
backup=Backup |