Merge pull request #302 from acelaya-forks/feature/number-formatting

Feature/number formatting
This commit is contained in:
Alejandro Celaya 2020-09-19 10:16:54 +02:00 committed by GitHub
commit 2951d0d75e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 80 additions and 25 deletions

View file

@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
#### Changed
* [#150](https://github.com/shlinkio/shlink-web-client/issues/150) The list of short URLs is now ordered by the creation date, showing newest results first.
* [#248](https://github.com/shlinkio/shlink-web-client/issues/248) Numbers displayed application-wide are now prettified.
* [#40](https://github.com/shlinkio/shlink-web-client/issues/40) Migrated project to TypeScript.
#### Deprecated

View file

@ -1,7 +1,13 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import { pageIsEllipsis, keyForPage, NumberOrEllipsis, progressivePagination } from '../utils/helpers/pagination';
import {
pageIsEllipsis,
keyForPage,
NumberOrEllipsis,
progressivePagination,
prettifyPageNumber,
} from '../utils/helpers/pagination';
import './SimplePaginator.scss';
interface SimplePaginatorProps {
@ -29,7 +35,7 @@ const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, se
disabled={pageIsEllipsis(pageNumber)}
active={currentPage === pageNumber}
>
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{pageNumber}</PaginationLink>
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{prettifyPageNumber(pageNumber)}</PaginationLink>
</PaginationItem>
))}
<PaginationItem disabled={currentPage >= pagesCount}>

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import { pageIsEllipsis, keyForPage, progressivePagination } from '../utils/helpers/pagination';
import { pageIsEllipsis, keyForPage, progressivePagination, prettifyPageNumber } from '../utils/helpers/pagination';
import { ShlinkPaginator } from '../utils/services/types';
import './Paginator.scss';
@ -28,7 +28,7 @@ const Paginator = ({ paginator, serverId }: PaginatorProps) => {
tag={Link}
to={`/server/${serverId}/list-short-urls/${pageNumber}`}
>
{pageNumber}
{prettifyPageNumber(pageNumber)}
</PaginationLink>
</PaginationItem>
));

View file

@ -1,4 +1,5 @@
import { max, min, range } from 'ramda';
import { prettify } from './numbers';
const DELTA = 2;
@ -29,4 +30,7 @@ export const progressivePagination = (currentPage: number, pageCount: number): N
export const pageIsEllipsis = (pageNumber: NumberOrEllipsis): pageNumber is Ellipsis => pageNumber === ELLIPSIS;
export const prettifyPageNumber = (pageNumber: NumberOrEllipsis): string =>
pageIsEllipsis(pageNumber) ? pageNumber : prettify(pageNumber);
export const keyForPage = (pageNumber: NumberOrEllipsis, index: number) => !pageIsEllipsis(pageNumber) ? `${pageNumber}` : `${pageNumber}_${index}`;

View file

@ -1,7 +1,9 @@
import bowser from 'bowser';
import { zipObj } from 'ramda';
import { ChartData, ChartTooltipItem } from 'chart.js';
import { Empty, hasValue } from '../utils';
import { Stats, UserAgent } from '../../visits/types';
import { prettify } from './numbers';
const DEFAULT = 'Others';
const BROWSERS_WHITELIST = [
@ -33,10 +35,31 @@ export const extractDomain = (url: string | Empty): string => {
return 'Direct';
}
const domain = url.includes('://') ? url.split('/')[2] : url.split('/')[0];
return domain.split(':')[0];
return url.split('/')[url.includes('://') ? 2 : 0]?.split(':')[0] ?? '';
};
export const fillTheGaps = (stats: Stats, labels: string[]): number[] =>
Object.values({ ...zipObj(labels, labels.map(() => 0)), ...stats });
export const renderDoughnutChartLabel = (
{ datasetIndex, index }: ChartTooltipItem,
{ labels, datasets }: ChartData,
) => {
const datasetLabel = index !== undefined && labels?.[index] || '';
const value = datasetIndex !== undefined && index !== undefined
&& datasets?.[datasetIndex]?.data?.[index]
|| '';
return `${datasetLabel}: ${prettify(Number(value))}`;
};
export const renderNonDoughnutChartLabel = (labelToPick: 'yLabel' | 'xLabel') => (
item: ChartTooltipItem,
{ datasets }: ChartData,
) => {
const { datasetIndex } = item;
const value = item[labelToPick];
const datasetLabel = datasetIndex !== undefined && datasets?.[datasetIndex]?.label || '';
return `${datasetLabel}: ${prettify(Number(value))}`;
};

View file

@ -3,8 +3,9 @@ import { Doughnut, HorizontalBar } from 'react-chartjs-2';
import { keys, values } from 'ramda';
import classNames from 'classnames';
import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js';
import { fillTheGaps } from '../../utils/helpers/visits';
import { fillTheGaps, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/visits';
import { Stats } from '../types';
import { prettify } from '../../utils/helpers/numbers';
import './DefaultChart.scss';
export interface DefaultChartProps {
@ -124,7 +125,13 @@ const DefaultChart = (
scales: !isBarChart ? undefined : {
xAxes: [
{
ticks: { beginAtZero: true, precision: 0, max } as any,
ticks: {
beginAtZero: true,
// @ts-expect-error
precision: 0,
callback: prettify,
max,
},
stacked: true,
},
],
@ -132,9 +139,11 @@ const DefaultChart = (
},
tooltips: {
intersect: !isBarChart,
// Do not show tooltip on items with empty label when in a bar chart
filter: ({ yLabel }) => !isBarChart || yLabel !== '',
callbacks: {
label: isBarChart ? renderNonDoughnutChartLabel('xLabel') : renderDoughnutChartLabel,
},
},
onHover: !isBarChart ? undefined : ((e: ChangeEvent<HTMLElement>, chartElement: HorizontalBar[] | Doughnut[]) => {
const { target } = e;

View file

@ -11,12 +11,13 @@ import {
import { Line } from 'react-chartjs-2';
import { always, cond, reverse } from 'ramda';
import moment from 'moment';
import { ChartData, ChartDataSets } from 'chart.js';
import { ChartData, ChartDataSets, ChartOptions } from 'chart.js';
import { NormalizedVisit, Stats } from '../types';
import { fillTheGaps } from '../../utils/helpers/visits';
import { fillTheGaps, renderNonDoughnutChartLabel } from '../../utils/helpers/visits';
import { useToggle } from '../../utils/helpers/hooks';
import { rangeOf } from '../../utils/utils';
import ToggleSwitch from '../../utils/ToggleSwitch';
import { prettify } from '../../utils/helpers/numbers';
import './LineChartCard.scss';
interface LineChartCardProps {
@ -137,13 +138,18 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S
highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, '#F77F28'),
].filter(Boolean) as ChartDataSets[],
};
const options = {
const options: ChartOptions = {
maintainAspectRatio: false,
legend: { display: false },
scales: {
yAxes: [
{
ticks: { beginAtZero: true, precision: 0 },
ticks: {
beginAtZero: true,
// @ts-expect-error
precision: 0,
callback: prettify,
},
},
],
xAxes: [
@ -154,7 +160,11 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S
},
tooltips: {
intersect: false,
// @ts-expect-error
axis: 'x',
callbacks: {
label: renderNonDoughnutChartLabel('yLabel'),
},
},
};

View file

@ -3,6 +3,7 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
import { keys, values } from 'ramda';
import DefaultChart from '../../../src/visits/helpers/DefaultChart';
import { prettify } from '../../../src/utils/helpers/numbers';
describe('<DefaultChart />', () => {
let wrapper: ShallowWrapper;
@ -69,7 +70,7 @@ describe('<DefaultChart />', () => {
expect(scales).toEqual({
xAxes: [
{
ticks: { beginAtZero: true, precision: 0 },
ticks: { beginAtZero: true, precision: 0, callback: prettify },
stacked: true,
},
],

View file

@ -6,11 +6,12 @@ import moment from 'moment';
import { Mock } from 'ts-mockery';
import LineChartCard from '../../../src/visits/helpers/LineChartCard';
import ToggleSwitch from '../../../src/utils/ToggleSwitch';
import { Visit } from '../../../src/visits/types';
import { NormalizedVisit } from '../../../src/visits/types';
import { prettify } from '../../../src/utils/helpers/numbers';
describe('<LineChartCard />', () => {
let wrapper: ShallowWrapper;
const createWrapper = (visits: Visit[] = [], highlightedVisits: Visit[] = []) => {
const createWrapper = (visits: NormalizedVisit[] = [], highlightedVisits: NormalizedVisit[] = []) => {
wrapper = shallow(<LineChartCard title="Cool title" visits={visits} highlightedVisits={highlightedVisits} />);
return wrapper;
@ -34,7 +35,7 @@ describe('<LineChartCard />', () => {
[[{ date: moment().subtract(7, 'month').format() }], 'monthly' ],
[[{ date: moment().subtract(1, 'year').format() }], 'monthly' ],
])('renders group menu and selects proper grouping item based on visits dates', (visits, expectedActiveItem) => {
const wrapper = createWrapper(visits.map((visit) => Mock.of<Visit>(visit)));
const wrapper = createWrapper(visits.map((visit) => Mock.of<NormalizedVisit>(visit)));
const items = wrapper.find(DropdownItem);
expect(items).toHaveLength(4);
@ -58,7 +59,7 @@ describe('<LineChartCard />', () => {
scales: {
yAxes: [
{
ticks: { beginAtZero: true, precision: 0 },
ticks: { beginAtZero: true, precision: 0, callback: prettify },
},
],
xAxes: [
@ -67,16 +68,16 @@ describe('<LineChartCard />', () => {
},
],
},
tooltips: {
tooltips: expect.objectContaining({
intersect: false,
axis: 'x',
},
}),
});
});
it.each([
[[ Mock.of<Visit>({}) ], [], 1 ],
[[ Mock.of<Visit>({}) ], [ Mock.of<Visit>({}) ], 2 ],
[[ Mock.of<NormalizedVisit>({}) ], [], 1 ],
[[ Mock.of<NormalizedVisit>({}) ], [ Mock.of<NormalizedVisit>({}) ], 2 ],
])('renders chart with expected data', (visits, highlightedVisits, expectedLines) => {
const wrapper = createWrapper(visits, highlightedVisits);
const chart = wrapper.find(Line);
@ -87,8 +88,8 @@ describe('<LineChartCard />', () => {
it('includes stats for visits with no dates if selected', () => {
const wrapper = createWrapper([
Mock.of<Visit>({ date: '2016-04-01' }),
Mock.of<Visit>({ date: '2016-01-01' }),
Mock.of<NormalizedVisit>({ date: '2016-04-01' }),
Mock.of<NormalizedVisit>({ date: '2016-01-01' }),
]);
expect((wrapper.find(Line).prop('data') as any).labels).toHaveLength(2);