shlink-web-client/shlink-web-component/visits/charts/SortableBarChartCard.tsx

142 lines
5.2 KiB
TypeScript
Raw Normal View History

2023-02-18 13:11:01 +03:00
import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda';
2023-02-18 12:40:37 +03:00
import type { FC, ReactNode } from 'react';
import { useState } from 'react';
import { SimplePaginator } from '../../../src/common/SimplePaginator';
import { roundTen } from '../../../src/utils/helpers/numbers';
import type { Order } from '../../../src/utils/helpers/ordering';
import { OrderingDropdown } from '../../../src/utils/OrderingDropdown';
import { PaginationDropdown } from '../../../src/utils/PaginationDropdown';
import { rangeOf } from '../../../src/utils/utils';
2023-02-18 12:40:37 +03:00
import type { Stats, StatsRow } from '../types';
2023-02-18 13:11:01 +03:00
import { ChartCard } from './ChartCard';
2023-02-18 12:40:37 +03:00
import type { HorizontalBarChartProps } from './HorizontalBarChart';
import { HorizontalBarChart } from './HorizontalBarChart';
interface SortableBarChartCardProps extends Omit<HorizontalBarChartProps, 'max'> {
2021-09-18 13:07:05 +03:00
title: Function | string;
sortingItems: Record<string, string>;
withPagination?: boolean;
extraHeaderContent?: (activeCities?: string[]) => ReactNode;
}
2018-10-29 00:54:08 +03:00
2022-03-26 14:17:42 +03:00
const toLowerIfString = (value: any) => (type(value) === 'String' ? toLower(value) : value);
const pickKeyFromPair = ([key]: StatsRow) => key;
const pickValueFromPair = ([, value]: StatsRow) => value;
export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
stats,
highlightedStats,
title,
sortingItems,
extraHeaderContent,
withPagination = true,
...rest
}) => {
2022-03-26 14:17:42 +03:00
const [order, setOrder] = useState<Order<string>>({});
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(50);
2022-03-26 14:17:42 +03:00
const getSortedPairsForStats = (statsToSort: Stats, sorting: Record<string, string>) => {
const pairs = toPairs(statsToSort);
const sortedPairs = !order.field ? pairs : sortBy(
2022-10-05 00:27:11 +03:00
pipe<StatsRow[], string | number, string | number>(
2022-03-26 14:17:42 +03:00
order.field === Object.keys(sorting)[0] ? pickKeyFromPair : pickValueFromPair,
2020-08-22 09:10:31 +03:00
toLowerIfString,
),
2020-08-22 09:10:31 +03:00
pairs,
);
return !order.dir || order.dir === 'ASC' ? sortedPairs : reverse(sortedPairs);
};
const determineCurrentPagePairs = (pages: StatsRow[][]): StatsRow[] => {
const page = pages[currentPage - 1];
if (currentPage < pages.length) {
return page;
}
const firstPageLength = pages[0].length;
// Using the "hidden" key, the chart will just replace the label by an empty string
2022-03-26 14:17:42 +03:00
return [...page, ...rangeOf(firstPageLength - page.length, (i): StatsRow => [`hidden_${i}`, 0])];
};
const renderPagination = (pagesCount: number) =>
<SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />;
2022-03-26 14:17:42 +03:00
const determineStats = (statsToSort: Stats, sorting: Record<string, string>, theHighlightedStats?: Stats) => {
const sortedPairs = getSortedPairsForStats(statsToSort, sorting);
const sortedKeys = sortedPairs.map(pickKeyFromPair);
// The highlighted stats have to be ordered based on the regular stats, not on its own values
2022-03-26 14:17:42 +03:00
const sortedHighlightedPairs = theHighlightedStats && toPairs(
{ ...zipObj(sortedKeys, sortedKeys.map(() => 0)), ...theHighlightedStats },
);
if (sortedPairs.length <= itemsPerPage) {
return {
currentPageStats: fromPairs(sortedPairs),
currentPageHighlightedStats: sortedHighlightedPairs && fromPairs(sortedHighlightedPairs),
};
}
const pages = splitEvery(itemsPerPage, sortedPairs);
const highlightedPages = sortedHighlightedPairs && splitEvery(itemsPerPage, sortedHighlightedPairs);
return {
currentPageStats: fromPairs(determineCurrentPagePairs(pages)),
currentPageHighlightedStats: highlightedPages && fromPairs(determineCurrentPagePairs(highlightedPages)),
pagination: renderPagination(pages.length),
max: roundTen(Math.max(...sortedPairs.map(pickValueFromPair))),
2018-10-29 00:54:08 +03:00
};
};
const { currentPageStats, currentPageHighlightedStats, pagination, max } = determineStats(
stats,
2020-08-22 09:10:31 +03:00
sortingItems,
2022-03-26 14:17:42 +03:00
highlightedStats && Object.keys(highlightedStats).length > 0 ? highlightedStats : undefined,
);
const activeCities = Object.keys(currentPageStats);
const computeTitle = () => (
2020-11-14 00:44:26 +03:00
<>
{title}
<div className="float-end">
<OrderingDropdown
isButton={false}
right
items={sortingItems}
order={order}
onChange={(field, dir) => {
setOrder({ field, dir });
setCurrentPage(1);
}}
/>
</div>
{withPagination && Object.keys(stats).length > 50 && (
<div className="float-end">
<PaginationDropdown
toggleClassName="btn-sm p-0 me-3"
2022-03-26 14:17:42 +03:00
ranges={[50, 100, 200, 500]}
value={itemsPerPage}
2022-03-26 14:17:42 +03:00
setValue={(value) => {
setItemsPerPage(value);
setCurrentPage(1);
}}
2018-10-29 00:54:08 +03:00
/>
</div>
)}
{extraHeaderContent && (
<div className="float-end">
{extraHeaderContent(pagination ? activeCities : undefined)}
</div>
)}
2020-11-14 00:44:26 +03:00
</>
);
return (
<ChartCard
title={computeTitle}
footer={pagination}
>
<HorizontalBarChart stats={currentPageStats} highlightedStats={currentPageHighlightedStats} max={max} {...rest} />
</ChartCard>
);
};