Commit e22db695 authored by isstuev's avatar isstuev

stats fixes after test part 1

parent ab22a7ab
......@@ -248,7 +248,7 @@ export default function useNavItems(): ReturnType {
text: 'Charts & stats',
nextRoute: { pathname: '/stats' as const },
icon: 'stats',
isActive: pathname === '/stats',
isActive: pathname.startsWith('/stats'),
} : null,
apiNavItems.length > 0 && {
text: 'API',
......
import { Button, Flex, IconButton, Text } from '@chakra-ui/react';
import { Button, Flex, IconButton, Link, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -19,7 +19,7 @@ import ChartMenu from 'ui/shared/chart/ChartMenu';
import ChartResolutionSelect from 'ui/shared/chart/ChartResolutionSelect';
import ChartWidgetContent from 'ui/shared/chart/ChartWidgetContent';
import useChartQuery from 'ui/shared/chart/useChartQuery';
import useZoomReset from 'ui/shared/chart/useZoomReset';
import useZoom from 'ui/shared/chart/useZoom';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import IconSvg from 'ui/shared/IconSvg';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -46,7 +46,7 @@ const Chart = () => {
const id = getQueryParamString(router.query.id);
const [ intervalState, setIntervalState ] = React.useState<StatsIntervalIds | undefined>();
const [ resolution, setResolution ] = React.useState<Resolution>(DEFAULT_RESOLUTION);
const { isZoomResetInitial, handleZoom, handleZoomReset } = useZoomReset();
const { zoomRange, handleZoom, handleZoomReset } = useZoom();
const interval = intervalState || getIntervalByResolution(resolution);
......@@ -155,6 +155,10 @@ const Chart = () => {
title={ info?.title || '' }
isLoading={ lineQuery.isPlaceholderData }
chartRef={ ref }
resolution={ resolution }
zoomRange={ zoomRange }
handleZoom={ handleZoom }
handleZoomReset={ handleZoomReset }
/>
) }
</Flex>
......@@ -172,44 +176,31 @@ const Chart = () => {
withTextAd
/>
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center" gap={ 3 } maxW="100%" overflow="hidden">
<Text>Period</Text>
<ChartIntervalSelect interval={ interval } onIntervalChange={ setIntervalState }/>
<Flex alignItems="center" gap={{ base: 3, lg: 6 }} maxW="100%" overflow="hidden">
<Flex alignItems="center" gap={ 3 }>
<Text>Period</Text>
<ChartIntervalSelect interval={ interval } onIntervalChange={ setIntervalState }/>
</Flex>
{ lineQuery.data?.info?.resolutions && lineQuery.data?.info?.resolutions.length > 1 && (
<>
<Text ml={{ base: 0, lg: 3 }}>{ isMobile ? 'Res.' : 'Resolution' }</Text>
<Flex alignItems="center" gap={ 3 }>
<Text>{ isMobile ? 'Res.' : 'Resolution' }</Text>
<ChartResolutionSelect
resolution={ resolution }
onResolutionChange={ setResolution }
resolutions={ lineQuery.data?.info?.resolutions || [] }
/>
</>
</Flex>
) }
{ (!isZoomResetInitial || resolution !== DEFAULT_RESOLUTION) && (
isMobile ? (
<IconButton
aria-label="Reset"
variant="ghost"
size="sm"
icon={ <IconSvg name="repeat" boxSize={ 5 }/> }
onClick={ handleReset }
/>
) : (
<Button
leftIcon={ <IconSvg name="repeat" w={ 4 } h={ 4 }/> }
colorScheme="blue"
gridColumn={ 2 }
justifySelf="end"
alignSelf="top"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleReset }
ml={ 6 }
>
Reset
</Button>
)
{ (Boolean(zoomRange)) && (
<Link
onClick={ handleReset }
display="flex"
alignItems="center"
gap={ 2 }
>
<IconSvg name="repeat" w={ 5 } h={ 5 }/>
{ !isMobile && 'Reset' }
</Link>
) }
</Flex>
{ !isMobile && shareAndMenu }
......@@ -228,9 +219,10 @@ const Chart = () => {
units={ info?.units || undefined }
isEnlarged
isLoading={ lineQuery.isPlaceholderData }
isZoomResetInitial={ isZoomResetInitial }
zoomRange={ zoomRange }
handleZoom={ handleZoom }
emptyText="No data for the selected resolution & interval."
resolution={ resolution }
/>
</Flex>
</>
......
......@@ -12,6 +12,7 @@ import domToImage from 'dom-to-image';
import React from 'react';
import type { TimeChartItem } from './types';
import type { Resolution } from '@blockscout/stats-types';
import type { Route } from 'nextjs-routes';
import { route } from 'nextjs-routes';
......@@ -33,11 +34,27 @@ export type Props = {
isLoading: boolean;
chartRef: React.RefObject<HTMLDivElement>;
href?: Route;
resolution?: Resolution;
zoomRange?: [ Date, Date ];
handleZoom: (range: [ Date, Date ]) => void;
handleZoomReset: () => void;
}
const DOWNLOAD_IMAGE_SCALE = 5;
const ChartMenu = ({ items, title, description, units, isLoading, chartRef, href }: Props) => {
const ChartMenu = ({
items,
title,
description,
units,
isLoading,
chartRef,
href,
resolution,
zoomRange,
handleZoom,
handleZoomReset,
}: Props) => {
const pngBackgroundColor = useColorModeValue('white', 'black');
const [ isFullscreen, setIsFullscreen ] = React.useState(false);
......@@ -172,6 +189,10 @@ const ChartMenu = ({ items, title, description, units, isLoading, chartRef, href
description={ description }
onClose={ clearFullscreenChart }
units={ units }
resolution={ resolution }
zoomRange={ zoomRange }
handleZoom={ handleZoom }
handleZoomReset={ handleZoomReset }
/>
) }
</>
......
import * as d3 from 'd3';
import React from 'react';
import { Resolution } from '@blockscout/stats-types';
import type { TimeChartData } from 'ui/shared/chart/types';
import ChartTooltipBackdrop, { useRenderBackdrop } from './tooltip/ChartTooltipBackdrop';
......@@ -21,9 +22,21 @@ interface Props {
yScale: d3.ScaleLinear<number, number>;
anchorEl: SVGRectElement | null;
noAnimation?: boolean;
resolution?: Resolution;
}
const ChartTooltip = ({ xScale, yScale, width, tooltipWidth = 200, height, data, anchorEl, noAnimation, ...props }: Props) => {
const ChartTooltip = ({
xScale,
yScale,
width,
tooltipWidth = 200,
height,
data,
anchorEl,
noAnimation,
resolution,
...props
}: Props) => {
const ref = React.useRef<SVGGElement>(null);
const trackerId = React.useRef<number>();
const isVisible = React.useRef(false);
......@@ -150,8 +163,8 @@ const ChartTooltip = ({ xScale, yScale, width, tooltipWidth = 200, height, data,
{ data.map(({ name }) => <ChartTooltipPoint key={ name }/>) }
<ChartTooltipContent>
<ChartTooltipBackdrop/>
<ChartTooltipTitle/>
<ChartTooltipRow label="Date" lineNum={ 1 }/>
<ChartTooltipTitle resolution={ resolution }/>
<ChartTooltipRow label={ getDateLabel(resolution) } lineNum={ 1 }/>
{ data.map(({ name }, index) => <ChartTooltipRow key={ name } label={ name } lineNum={ index + 1 }/>) }
</ChartTooltipContent>
</g>
......@@ -159,3 +172,16 @@ const ChartTooltip = ({ xScale, yScale, width, tooltipWidth = 200, height, data,
};
export default React.memo(ChartTooltip);
function getDateLabel(resolution?: Resolution): string {
switch (resolution) {
case Resolution.WEEK:
return 'Dates';
case Resolution.MONTH:
return 'Month';
case Resolution.YEAR:
return 'Year';
default:
return 'Date';
}
}
......@@ -17,7 +17,7 @@ import IconSvg from 'ui/shared/IconSvg';
import ChartMenu from './ChartMenu';
import ChartWidgetContent from './ChartWidgetContent';
import useZoomReset from './useZoomReset';
import useZoom from './useZoom';
export type Props = {
items?: Array<TimeChartItem>;
......@@ -45,7 +45,7 @@ const ChartWidget = ({
href,
}: Props) => {
const ref = useRef<HTMLDivElement>(null);
const { isZoomResetInitial, handleZoom, handleZoomReset } = useZoomReset();
const { zoomRange, handleZoom, handleZoomReset } = useZoom();
const borderColor = useColorModeValue('gray.200', 'gray.600');
......@@ -60,7 +60,7 @@ const ChartWidget = ({
title={ title }
emptyText={ emptyText }
handleZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
zoomRange={ zoomRange }
noAnimation={ noAnimation }
/>
);
......@@ -116,7 +116,7 @@ const ChartWidget = ({
<Flex ml="auto" columnGap={ 2 }>
<Tooltip label="Reset zoom">
<IconButton
hidden={ isZoomResetInitial }
hidden={ !zoomRange }
aria-label="Reset zoom"
colorScheme="blue"
w={ 9 }
......@@ -137,6 +137,9 @@ const ChartWidget = ({
isLoading={ isLoading }
chartRef={ ref }
units={ units }
handleZoom={ handleZoom }
handleZoomReset={ handleZoomReset }
zoomRange={ zoomRange }
/>
) }
</Flex>
......
......@@ -2,6 +2,7 @@ import { Box, Center, Flex, Link, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import type { TimeChartItem } from './types';
import type { Resolution } from '@blockscout/stats-types';
import { apos } from 'lib/html-entities';
......@@ -15,10 +16,11 @@ export type Props = {
isLoading?: boolean;
isError?: boolean;
emptyText?: string;
handleZoom: () => void;
isZoomResetInitial: boolean;
zoomRange?: [ Date, Date ];
handleZoom: (range: [ Date, Date ]) => void;
isEnlarged?: boolean;
noAnimation?: boolean;
resolution?: Resolution;
}
const ChartWidgetContent = ({
......@@ -28,10 +30,11 @@ const ChartWidgetContent = ({
isError,
units,
emptyText,
zoomRange,
handleZoom,
isZoomResetInitial,
isEnlarged,
noAnimation,
resolution,
}: Props) => {
const hasItems = items && items.length > 2;
......@@ -71,12 +74,13 @@ const ChartWidgetContent = ({
<Box flexGrow={ 1 } maxW="100%" position="relative" h="100%">
<ChartWidgetGraph
items={ items }
zoomRange={ zoomRange }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
units={ units }
isEnlarged={ isEnlarged }
noAnimation={ noAnimation }
resolution={ resolution }
/>
<WatermarkIcon w="162px" h="15%"/>
</Box>
......
......@@ -2,9 +2,9 @@ import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
import { Resolution } from '@blockscout/stats-types';
import type { ChartMargin, TimeChartData, TimeChartItem } from 'ui/shared/chart/types';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
......@@ -20,37 +20,42 @@ interface Props {
title: string;
units?: string;
items: Array<TimeChartItem>;
onZoom: () => void;
isZoomResetInitial: boolean;
zoomRange?: [ Date, Date ];
onZoom: (range: [ Date, Date ]) => void;
margin?: ChartMargin;
noAnimation?: boolean;
resolution?: Resolution;
}
// temporarily turn off the data aggregation, we need a better algorithm for that
const MAX_SHOW_ITEMS = 100_000_000_000;
const DEFAULT_CHART_MARGIN = { bottom: 20, left: 10, right: 20, top: 10 };
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin: marginProps, units, noAnimation }: Props) => {
const ChartWidgetGraph = ({
isEnlarged,
items,
onZoom,
title,
margin: marginProps,
units,
noAnimation,
resolution,
zoomRange,
}: Props) => {
const isMobile = useIsMobile();
const color = useToken('colors', 'blue.200');
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
const overlayRef = React.useRef<SVGRectElement>(null);
const [ range, setRange ] = React.useState<[ Date, Date ]>([ items[0].date, items[items.length - 1].date ]);
const range = React.useMemo(() => zoomRange || [ items[0].date, items[items.length - 1].date ], [ zoomRange, items ]);
const rangedItems = React.useMemo(() =>
items.filter((item) => item.date >= range[0] && item.date <= range[1]),
[ items, range ]);
const isGroupedValues = rangedItems.length > MAX_SHOW_ITEMS;
const displayedData = React.useMemo(() => {
if (isGroupedValues) {
return groupChartItemsByWeekNumber(rangedItems);
} else {
return rangedItems;
}
}, [ isGroupedValues, rangedItems ]);
const displayedData = React.useMemo(() =>
items
.filter((item) => item.date >= range[0] && item.date <= range[1])
.map((item) => ({
...item,
dateLabel: getDateLabel(item.date, item.date_to, resolution),
})),
[ items, range, resolution ]);
const chartData: TimeChartData = React.useMemo(() => ([ { items: displayedData, name: 'Value', color, units } ]), [ color, displayedData, units ]);
......@@ -80,17 +85,6 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
axesConfig,
});
const handleRangeSelect = React.useCallback((nextRange: [ Date, Date ]) => {
setRange([ nextRange[0], nextRange[1] ]);
onZoom();
}, [ onZoom ]);
React.useEffect(() => {
if (isZoomResetInitial) {
setRange([ items[0].date, items[items.length - 1].date ]);
}
}, [ isZoomResetInitial, items ]);
return (
<svg width="100%" height="100%" ref={ ref } cursor="pointer" id={ chartId } opacity={ rect ? 1 : 0 }>
......@@ -143,12 +137,13 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
<ChartTooltip
anchorEl={ overlayRef.current }
width={ innerWidth }
tooltipWidth={ isGroupedValues ? 280 : 200 }
tooltipWidth={ (resolution === Resolution.WEEK) ? 280 : 200 }
height={ innerHeight }
xScale={ axes.x.scale }
yScale={ axes.y.scale }
data={ chartData }
noAnimation={ noAnimation }
resolution={ resolution }
/>
<ChartSelectionX
......@@ -156,7 +151,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
height={ innerHeight }
scale={ axes.x.scale }
data={ chartData }
onSelect={ handleRangeSelect }
onSelect={ onZoom }
/>
</ChartOverlay>
</g>
......@@ -166,13 +161,15 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
export default React.memo(ChartWidgetGraph);
function groupChartItemsByWeekNumber(items: Array<TimeChartItem>): Array<TimeChartItem> {
return d3.rollups(items,
(group) => ({
date: group[0].date,
value: d3.sum(group, (d) => d.value),
dateLabel: `${ d3.timeFormat('%e %b %Y')(group[0].date) }${ d3.timeFormat('%e %b %Y')(group[group.length - 1].date) }`,
}),
(t) => `${ dayjs(t.date).week() } / ${ dayjs(t.date).year() }`,
).map(([ , v ]) => v);
function getDateLabel(date: Date, dateTo?: Date, resolution?: Resolution): string {
switch (resolution) {
case Resolution.WEEK:
return d3.timeFormat('%e %b %Y')(date) + (dateTo ? ` – ${ d3.timeFormat('%e %b %Y')(dateTo) }` : '');
case Resolution.MONTH:
return d3.timeFormat('%b %Y')(date);
case Resolution.YEAR:
return d3.timeFormat('%Y')(date);
default:
return d3.timeFormat('%e %b %Y')(date);
}
}
import { Box, Button, Grid, Heading, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay, Text } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import React from 'react';
import type { TimeChartItem } from './types';
import type { Resolution } from '@blockscout/stats-types';
import IconSvg from 'ui/shared/IconSvg';
......@@ -14,6 +15,10 @@ type Props = {
items: Array<TimeChartItem>;
onClose: () => void;
units?: string;
resolution?: Resolution;
zoomRange?: [ Date, Date ];
handleZoom: (range: [ Date, Date ]) => void;
handleZoomReset: () => void;
}
const FullscreenChartModal = ({
......@@ -23,17 +28,11 @@ const FullscreenChartModal = ({
items,
units,
onClose,
resolution,
zoomRange,
handleZoom,
handleZoomReset,
}: Props) => {
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
const handleZoom = useCallback(() => {
setIsZoomResetInitial(false);
}, []);
const handleZoomReset = useCallback(() => {
setIsZoomResetInitial(true);
}, []);
return (
<Modal
isOpen={ isOpen }
......@@ -69,7 +68,7 @@ const FullscreenChartModal = ({
</Text>
) }
{ !isZoomResetInitial && (
{ Boolean(zoomRange) && (
<Button
leftIcon={ <IconSvg name="repeat" w={ 4 } h={ 4 }/> }
colorScheme="blue"
......@@ -98,8 +97,9 @@ const FullscreenChartModal = ({
items={ items }
units={ units }
handleZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
zoomRange={ zoomRange }
title={ title }
resolution={ resolution }
/>
</ModalBody>
</ModalContent>
......
......@@ -2,10 +2,15 @@ import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
import { Resolution } from '@blockscout/stats-types';
import { STATS_RESOLUTIONS } from 'ui/stats/constants';
import ChartTooltipRow from './ChartTooltipRow';
const ChartTooltipTitle = () => {
const ChartTooltipTitle = ({ resolution = Resolution.DAY }: { resolution?: Resolution }) => {
const titleColor = useToken('colors', 'yellow.300');
const resolutionTitle = STATS_RESOLUTIONS.find(r => r.id === resolution)?.title || 'day';
return (
<ChartTooltipRow lineNum={ 0 }>
......@@ -16,7 +21,7 @@ const ChartTooltipTitle = () => {
opacity={ 0 }
dominantBaseline="hanging"
>
Incomplete day
{ `Incomplete ${ resolutionTitle.toLowerCase() }` }
</text>
</ChartTooltipRow>
);
......
......@@ -6,6 +6,7 @@ export interface TimeChartItemRaw {
export interface TimeChartItem {
date: Date;
date_to?: Date;
dateLabel?: string;
value: number;
isApproximate?: boolean;
......
......@@ -50,7 +50,7 @@ export default function useChartQuery(id: string, resolution: Resolution, interv
}, [ info, lineQuery.data?.info, lineQuery.isPlaceholderData ]);
const items = React.useMemo(() => lineQuery.data?.chart?.map((item) => {
return { date: new Date(item.date), value: Number(item.value), isApproximate: item.is_approximate };
return { date: new Date(item.date), date_to: new Date(item.date_to), value: Number(item.value), isApproximate: item.is_approximate };
}), [ lineQuery ]);
return {
......
import React from 'react';
export default function useZoomReset() {
export default function useZoom() {
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
const [ zoomRange, setZoomRange ] = React.useState<[ Date, Date ] | undefined>();
const handleZoom = React.useCallback(() => {
const handleZoom = React.useCallback((range: [ Date, Date ]) => {
setZoomRange(range);
setIsZoomResetInitial(false);
}, []);
const handleZoomReset = React.useCallback(() => {
setZoomRange(undefined);
setIsZoomResetInitial(true);
}, []);
return {
isZoomResetInitial,
zoomRange,
handleZoom,
handleZoomReset,
};
......
......@@ -47,6 +47,8 @@ const TagGroupSelect = <T extends string>({ items, value, isMulti, onChange, tag
cursor="pointer"
onClick={ onItemClick }
size={ tagSize }
display="inline-flex"
justifyContent="center"
>
{ item.title }
</Tag>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment