Commit 4ada09dd authored by Yuri Mikhin's avatar Yuri Mikhin Committed by Yuri Mikhin

Add grouping of values for charts at the long ranges.

parent 9292dd55
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration'; import duration from 'dayjs/plugin/duration';
import localizedFormat from 'dayjs/plugin/localizedFormat'; import localizedFormat from 'dayjs/plugin/localizedFormat';
import minMax from 'dayjs/plugin/minMax';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale'; import updateLocale from 'dayjs/plugin/updateLocale';
import weekOfYear from 'dayjs/plugin/weekOfYear';
const relativeTimeConfig = { const relativeTimeConfig = {
thresholds: [ thresholds: [
...@@ -26,6 +28,8 @@ dayjs.extend(relativeTime, relativeTimeConfig); ...@@ -26,6 +28,8 @@ dayjs.extend(relativeTime, relativeTimeConfig);
dayjs.extend(updateLocale); dayjs.extend(updateLocale);
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
dayjs.extend(duration); dayjs.extend(duration);
dayjs.extend(weekOfYear);
dayjs.extend(minMax);
dayjs.updateLocale('en', { dayjs.updateLocale('en', {
formats: { formats: {
......
...@@ -30,7 +30,6 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => { ...@@ -30,7 +30,6 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
return ( return (
<ChartWidget <ChartWidget
chartHeight="200px"
title="Balances" title="Balances"
items={ items } items={ items }
isLoading={ isLoading } isLoading={ isLoading }
......
...@@ -5,6 +5,7 @@ import type { TimeChartData } from 'ui/shared/chart/types'; ...@@ -5,6 +5,7 @@ import type { TimeChartData } from 'ui/shared/chart/types';
import ethTokenTransferData from 'data/charts_eth_token_transfer.json'; import ethTokenTransferData from 'data/charts_eth_token_transfer.json';
import ethTxsData from 'data/charts_eth_txs.json'; import ethTxsData from 'data/charts_eth_txs.json';
import dayjs from 'lib/date/dayjs';
import ChartArea from 'ui/shared/chart/ChartArea'; import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis'; import ChartAxis from 'ui/shared/chart/ChartAxis';
import ChartGridLine from 'ui/shared/chart/ChartGridLine'; import ChartGridLine from 'ui/shared/chart/ChartGridLine';
...@@ -22,24 +23,30 @@ const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 }; ...@@ -22,24 +23,30 @@ const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const CHART_OFFSET = { const CHART_OFFSET = {
y: 26, // legend height y: 26, // legend height
}; };
const RANGE_DEFAULT_START_DATE = dayjs.min(dayjs(ethTokenTransferData[0].date), dayjs(ethTxsData[0].date)).toDate();
const RANGE_DEFAULT_LAST_DATE = dayjs.max(dayjs(ethTokenTransferData.at(-1)?.date), dayjs(ethTxsData.at(-1)?.date)).toDate();
const EthereumChart = () => { const EthereumChart = () => {
const ref = React.useRef<SVGSVGElement>(null); const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null); const overlayRef = React.useRef<SVGRectElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN, CHART_OFFSET); const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN, CHART_OFFSET);
const [ range, setRange ] = React.useState<[number, number]>([ 0, Infinity ]); const [ range, setRange ] = React.useState<[ Date, Date ]>([ RANGE_DEFAULT_START_DATE, RANGE_DEFAULT_LAST_DATE ]);
const data: TimeChartData = [ const data: TimeChartData = [
{ {
name: 'Daily txs', name: 'Daily txs',
color: useToken('colors', 'blue.500'), color: useToken('colors', 'blue.500'),
items: ethTxsData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })), items: ethTxsData
.map((d) => ({ ...d, date: new Date(d.date) }))
.filter((d) => d.date >= range[0] && d.date <= range[1]),
}, },
{ {
name: 'ERC-20 tr.', name: 'ERC-20 tr.',
color: useToken('colors', 'green.500'), color: useToken('colors', 'green.500'),
items: ethTokenTransferData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })), items: ethTokenTransferData
.map((d) => ({ ...d, date: new Date(d.date) }))
.filter((d) => d.date >= range[0] && d.date <= range[1]),
}, },
]; ];
...@@ -52,12 +59,12 @@ const EthereumChart = () => { ...@@ -52,12 +59,12 @@ const EthereumChart = () => {
height: innerHeight, height: innerHeight,
}); });
const handleRangeSelect = React.useCallback((nextRange: [number, number]) => { const handleRangeSelect = React.useCallback((nextRange: [ Date, Date ]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]); setRange([ nextRange[0], nextRange[1] ]);
}, [ range ]); }, [ ]);
const handleZoomReset = React.useCallback(() => { const handleZoomReset = React.useCallback(() => {
setRange([ 0, Infinity ]); setRange([ RANGE_DEFAULT_START_DATE, RANGE_DEFAULT_LAST_DATE ]);
}, [ ]); }, [ ]);
// uncomment if we need brush the chart // uncomment if we need brush the chart
...@@ -156,7 +163,7 @@ const EthereumChart = () => { ...@@ -156,7 +163,7 @@ const EthereumChart = () => {
</ChartOverlay> </ChartOverlay>
</g> </g>
</svg> </svg>
{ (range[0] !== 0 || range[1] !== Infinity) && ( { (range[0] !== RANGE_DEFAULT_START_DATE || range[1] !== RANGE_DEFAULT_LAST_DATE) && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
......
...@@ -4,14 +4,16 @@ import React from 'react'; ...@@ -4,14 +4,16 @@ import React from 'react';
import type { TimeChartData, TimeChartItem } from 'ui/shared/chart/types'; import type { TimeChartData, TimeChartItem } from 'ui/shared/chart/types';
const SELECTION_THRESHOLD = 1; import dayjs from 'lib/date/dayjs';
const SELECTION_THRESHOLD = 2;
interface Props { interface Props {
height: number; height: number;
anchorEl?: SVGRectElement | null; anchorEl?: SVGRectElement | null;
scale: d3.ScaleTime<number, number>; scale: d3.ScaleTime<number, number>;
data: TimeChartData; data: TimeChartData;
onSelect: (range: [number, number]) => void; onSelect: (range: [Date, Date]) => void;
} }
const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => { const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => {
...@@ -51,13 +53,13 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -51,13 +53,13 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
}, []); }, []);
const handleSelect = React.useCallback((x0: number, x1: number) => { const handleSelect = React.useCallback((x0: number, x1: number) => {
const index0 = getIndexByX(x0); const startDate = scale.invert(x0);
const index1 = getIndexByX(x1); const endDate = scale.invert(x1);
if (Math.abs(index0 - index1) > SELECTION_THRESHOLD) { if (Math.abs(dayjs(startDate).diff(endDate, 'day')) > SELECTION_THRESHOLD) {
onSelect([ Math.min(index0, index1), Math.max(index0, index1) ]); onSelect([ dayjs.min(dayjs(startDate), dayjs(endDate)).toDate(), dayjs.max(dayjs(startDate), dayjs(endDate)).toDate() ]);
} }
}, [ getIndexByX, onSelect ]); }, [ onSelect, scale ]);
const cleanUp = React.useCallback(() => { const cleanUp = React.useCallback(() => {
isActive.current = false; isActive.current = false;
......
...@@ -11,6 +11,7 @@ import { trackPointer } from 'ui/shared/chart/utils/pointerTracker'; ...@@ -11,6 +11,7 @@ import { trackPointer } from 'ui/shared/chart/utils/pointerTracker';
interface Props { interface Props {
chartId?: string; chartId?: string;
width?: number; width?: number;
tooltipWidth?: number;
height?: number; height?: number;
data: TimeChartData; data: TimeChartData;
xScale: d3.ScaleTime<number, number>; xScale: d3.ScaleTime<number, number>;
...@@ -23,7 +24,7 @@ const PADDING = 16; ...@@ -23,7 +24,7 @@ const PADDING = 16;
const LINE_SPACE = 10; const LINE_SPACE = 10;
const POINT_SIZE = 16; const POINT_SIZE = 16;
const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...props }: Props) => { const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, data, anchorEl, ...props }: Props) => {
const lineColor = useToken('colors', 'gray.400'); const lineColor = useToken('colors', 'gray.400');
const titleColor = useToken('colors', 'blue.100'); const titleColor = useToken('colors', 'blue.100');
const textColor = useToken('colors', 'white'); const textColor = useToken('colors', 'white');
...@@ -66,11 +67,14 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...@@ -66,11 +67,14 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
return `translate(${ translateX }, ${ translateY })`; return `translate(${ translateX }, ${ translateY })`;
}); });
const date = xScale.invert(x);
const dateLabel = data[0].items.find((item) => item.date.getTime() === date.getTime())?.dateLabel;
tooltipContent tooltipContent
.select('.ChartTooltip__contentDate') .select('.ChartTooltip__contentDate')
.text(d3.timeFormat('%e %b %Y')(xScale.invert(x))); .text(dateLabel || d3.timeFormat('%e %b %Y')(xScale.invert(x)));
}, },
[ xScale, width, height ], [ xScale, data, width, height ],
); );
const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => { const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => {
...@@ -226,7 +230,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...@@ -226,7 +230,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
rx={ 12 } rx={ 12 }
ry={ 12 } ry={ 12 }
fill={ bgColor } fill={ bgColor }
width={ 200 } width={ tooltipWidth || 200 }
height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE } height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE }
/> />
<g transform={ `translate(${ PADDING },${ PADDING })` }> <g transform={ `translate(${ PADDING },${ PADDING })` }>
......
...@@ -99,6 +99,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -99,6 +99,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
return ( return (
<> <>
<Box <Box
height={ chartHeight }
ref={ ref } ref={ ref }
padding={{ base: 3, lg: 4 }} padding={{ base: 3, lg: 4 }}
borderRadius="md" borderRadius="md"
...@@ -198,6 +199,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -198,6 +199,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
<Box h={ chartHeight || 'auto' }> <Box h={ chartHeight || 'auto' }>
<ChartWidgetGraph <ChartWidgetGraph
margin={{ bottom: 20 }}
items={ items } items={ items }
onZoom={ handleZoom } onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial } isZoomResetInitial={ isZoomResetInitial }
......
import { useToken } from '@chakra-ui/react'; import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import type { TimeChartItem } from 'ui/shared/chart/types'; import type { ChartMargin, TimeChartItem } from 'ui/shared/chart/types';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import ChartArea from 'ui/shared/chart/ChartArea'; import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis'; import ChartAxis from 'ui/shared/chart/ChartAxis';
...@@ -20,21 +22,35 @@ interface Props { ...@@ -20,21 +22,35 @@ interface Props {
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
onZoom: () => void; onZoom: () => void;
isZoomResetInitial: boolean; isZoomResetInitial: boolean;
margin: ChartMargin;
} }
const CHART_MARGIN = { bottom: 20, left: 40, right: 20, top: 10 }; const MAX_SHOW_ITEMS = 100;
const DEFAULT_CHART_MARGIN = { bottom: 20, left: 40, right: 20, top: 10 };
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title }: Props) => { const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const ref = React.useRef<SVGSVGElement>(null);
const color = useToken('colors', 'blue.200'); const color = useToken('colors', 'blue.200');
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null); const overlayRef = React.useRef<SVGRectElement>(null);
const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]); const chartMargin = { ...DEFAULT_CHART_MARGIN, ...margin };
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN); const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, chartMargin);
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`; const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
const [ range, setRange ] = React.useState<[ Date, Date ]>([ items[0].date, items[items.length - 1].date ]);
const displayedData = useMemo(() => items.slice(range[0], range[1]), [ items, range ]);
const chartData = [ { items: items, name: 'Value', color } ]; const rangedItems = useMemo(() =>
items.filter((item) => item.date >= range[0] && item.date <= range[1]),
[ items, range ]);
const isGroupedValues = rangedItems.length > MAX_SHOW_ITEMS;
const displayedData = useMemo(() => {
if (isGroupedValues) {
return groupChartItemsByWeekNumber(rangedItems);
} else {
return rangedItems;
}
}, [ isGroupedValues, rangedItems ]);
const chartData = [ { items: displayedData, name: 'Value', color } ];
const { yTickFormat, xScale, yScale } = useTimeChartController({ const { yTickFormat, xScale, yScale } = useTimeChartController({
data: [ { items: displayedData, name: title, color } ], data: [ { items: displayedData, name: title, color } ],
...@@ -42,21 +58,21 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -42,21 +58,21 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
height: innerHeight, height: innerHeight,
}); });
const handleRangeSelect = React.useCallback((nextRange: [ number, number ]) => { const handleRangeSelect = React.useCallback((nextRange: [ Date, Date ]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]); setRange([ nextRange[0], nextRange[1] ]);
onZoom(); onZoom();
}, [ onZoom, range ]); }, [ onZoom ]);
useEffect(() => { useEffect(() => {
if (isZoomResetInitial) { if (isZoomResetInitial) {
setRange([ 0, Infinity ]); setRange([ items[0].date, items[items.length - 1].date ]);
} }
}, [ isZoomResetInitial ]); }, [ isZoomResetInitial, items ]);
return ( return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer" id={ chartId } opacity={ width ? 1 : 0 }> <svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer" id={ chartId } opacity={ width ? 1 : 0 }>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` }> <g transform={ `translate(${ chartMargin?.left || 0 },${ chartMargin?.top || 0 })` }>
<ChartGridLine <ChartGridLine
type="horizontal" type="horizontal"
scale={ yScale } scale={ yScale }
...@@ -104,6 +120,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -104,6 +120,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
chartId={ chartId } chartId={ chartId }
anchorEl={ overlayRef.current } anchorEl={ overlayRef.current }
width={ innerWidth } width={ innerWidth }
tooltipWidth={ isGroupedValues ? 280 : 200 }
height={ innerHeight } height={ innerHeight }
xScale={ xScale } xScale={ xScale }
yScale={ yScale } yScale={ yScale }
...@@ -124,3 +141,14 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -124,3 +141,14 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
}; };
export default React.memo(ChartWidgetGraph); 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(),
).map(([ , v ]) => v);
}
...@@ -91,6 +91,7 @@ const FullscreenChartModal = ({ ...@@ -91,6 +91,7 @@ const FullscreenChartModal = ({
h="100%" h="100%"
> >
<ChartWidgetGraph <ChartWidgetGraph
margin={{ bottom: 60 }}
isEnlarged isEnlarged
items={ items } items={ items }
onZoom={ handleZoom } onZoom={ handleZoom }
......
export interface TimeChartItem { export interface TimeChartItem {
date: Date; date: Date;
dateLabel?: string;
value: number; value: number;
} }
......
...@@ -48,7 +48,7 @@ export const statsChartsScheme: Array<StatsSection> = [ ...@@ -48,7 +48,7 @@ export const statsChartsScheme: Array<StatsSection> = [
title: 'Blocks', title: 'Blocks',
charts: [ charts: [
{ {
apiId: 'newBlocksPerDay', apiId: 'newBlocks',
title: 'New blocks', title: 'New blocks',
description: 'New blocks number', description: 'New blocks number',
}, },
...@@ -97,7 +97,7 @@ export const statsChartsScheme: Array<StatsSection> = [ ...@@ -97,7 +97,7 @@ export const statsChartsScheme: Array<StatsSection> = [
{ {
apiId: 'averageGasPrice', apiId: 'averageGasPrice',
title: 'Average gas price', title: 'Average gas price',
description: 'Average gas price for the period', description: 'Average gas price for the period (Gwei)',
}, },
], ],
}, },
......
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