Commit f25cce10 authored by tom's avatar tom Committed by isstuev

add tooltip

parent 4b497063
import { useToken } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ChainIndicatorChartData } from './types'; import type { ChainIndicatorChartData } from './types';
import ChartArea from 'ui/shared/chart/ChartArea'; import ChartArea from 'ui/shared/chart/ChartArea';
import ChartLine from 'ui/shared/chart/ChartLine'; import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useChartSize from 'ui/shared/chart/useChartSize'; import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController'; import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import { BlueLineGradient } from 'ui/shared/chart/utils/gradients'; import { BlueLineGradient } from 'ui/shared/chart/utils/gradients';
interface Props { interface Props {
data: ChainIndicatorChartData; data: ChainIndicatorChartData;
caption?: string;
} }
const COLOR = '#439AE2';
const ChainIndicatorChart = ({ data }: Props) => { const ChainIndicatorChart = ({ data }: Props) => {
const ref = React.useRef<SVGSVGElement>(null); const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current); const { width, height, innerWidth, innerHeight } = useChartSize(ref.current);
const color = useToken('colors', 'blue.300');
const { xScale, yScale } = useTimeChartController({ const { xScale, yScale } = useTimeChartController({
data: [ { items: data, name: 'spline' } ], data,
width: innerWidth, width: innerWidth,
height: innerHeight, height: innerHeight,
}); });
return ( return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }> <svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer">
<defs> <defs>
<BlueLineGradient.defs/> <BlueLineGradient.defs/>
</defs> </defs>
<ChartArea <ChartArea
data={ data } data={ data[0].items }
color={ color } color={ COLOR }
xScale={ xScale } xScale={ xScale }
yScale={ yScale } yScale={ yScale }
/> />
<ChartLine <ChartLine
data={ data } data={ data[0].items }
xScale={ xScale } xScale={ xScale }
yScale={ yScale } yScale={ yScale }
stroke={ `url(#${ BlueLineGradient.id })` } stroke={ `url(#${ BlueLineGradient.id })` }
animation="left" animation="left"
strokeWidth={ 3 } strokeWidth={ 3 }
/> />
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<ChartTooltip
anchorEl={ overlayRef.current }
width={ innerWidth }
height={ innerHeight }
xScale={ xScale }
yScale={ yScale }
data={ data }
/>
</ChartOverlay>
</svg> </svg>
); );
}; };
......
...@@ -17,7 +17,7 @@ const ChainIndicators = () => { ...@@ -17,7 +17,7 @@ const ChainIndicators = () => {
const listBgColor = useColorModeValue('gray.50', 'black'); const listBgColor = useColorModeValue('gray.50', 'black');
return ( return (
<Flex p={ 8 } borderRadius="lg" boxShadow="lg" bgColor={ bgColor } columnGap={ 12 } w="100%" alignItems="flex-start"> <Flex p={ 8 } borderRadius="lg" boxShadow="lg" bgColor={ bgColor } columnGap={ 12 } w="100%" alignItems="stretch">
<Flex flexGrow={ 1 } flexDir="column"> <Flex flexGrow={ 1 } flexDir="column">
<Flex alignItems="center"> <Flex alignItems="center">
<Text fontWeight={ 500 } fontFamily="Poppins" fontSize="lg">{ indicator?.title }</Text> <Text fontWeight={ 500 } fontFamily="Poppins" fontSize="lg">{ indicator?.title }</Text>
......
import type { ChartTransactionResponse, ChartMarketResponse } from 'types/api/charts'; import type { ChartTransactionResponse, ChartMarketResponse } from 'types/api/charts';
import type { QueryKeys } from 'types/client/queries'; import type { QueryKeys } from 'types/client/queries';
import type { TimeChartItem } from 'ui/shared/chart/types'; import type { TimeChartDataItem } from 'ui/shared/chart/types';
export type ChartsQueryKeys = QueryKeys.chartsTxs | QueryKeys.chartsMarket; export type ChartsQueryKeys = QueryKeys.chartsTxs | QueryKeys.chartsMarket;
...@@ -24,4 +24,4 @@ export type ChartsResponse<Q extends ChartsQueryKeys> = ...@@ -24,4 +24,4 @@ export type ChartsResponse<Q extends ChartsQueryKeys> =
Q extends QueryKeys.chartsMarket ? ChartMarketResponse : Q extends QueryKeys.chartsMarket ? ChartMarketResponse :
never; never;
export type ChainIndicatorChartData = Array<TimeChartItem>; export type ChainIndicatorChartData = Array<TimeChartDataItem>;
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TChainIndicator, ChartsResponse, ChartsQueryKeys, ChainIndicatorChartData } from './types'; import type { TChainIndicator, ChartsResponse, ChartsQueryKeys, ChainIndicatorChartData } from './types';
...@@ -6,23 +8,21 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -6,23 +8,21 @@ import useFetch from 'lib/hooks/useFetch';
type NotUndefined<T> = T extends undefined ? never : T; type NotUndefined<T> = T extends undefined ? never : T;
export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined) { export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<ChainIndicatorChartData> {
const fetch = useFetch(); const fetch = useFetch();
type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>; type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>;
return useQuery<unknown, unknown, ChainIndicatorChartData>( const queryResult = useQuery<unknown, unknown, ResponseType>(
[ indicator?.api.queryName ], [ indicator?.api.queryName ],
() => { () => fetch(indicator?.api.path || ''),
return fetch<ResponseType, unknown>(indicator?.api.path || '')
.then((result) => {
if ('status' in result) {
return Promise.reject(result);
}
return indicator?.api.dataFn(result);
});
},
{ enabled: Boolean(indicator) }, { enabled: Boolean(indicator) },
); );
return React.useMemo(() => {
return {
...queryResult,
data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data) : queryResult.data,
} as UseQueryResult<ChainIndicatorChartData>;
}, [ indicator, queryResult ]);
} }
...@@ -8,6 +8,7 @@ import appConfig from 'configs/app/config'; ...@@ -8,6 +8,7 @@ import appConfig from 'configs/app/config';
import globeIcon from 'icons/globe.svg'; import globeIcon from 'icons/globe.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts'; import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
const COLOR = '#439AE2';
const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = { const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
id: 'daily_txs', id: 'daily_txs',
...@@ -18,9 +19,14 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = { ...@@ -18,9 +19,14 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
api: { api: {
queryName: QueryKeys.chartsTxs, queryName: QueryKeys.chartsTxs,
path: '/node-api/stats/charts/transactions', path: '/node-api/stats/charts/transactions',
dataFn: (response) => response.chart_data dataFn: (response) => ([ {
.map((item) => ({ date: new Date(item.date), value: item.tx_count })) items: response.chart_data
.sort(sortByDateDesc), .map((item) => ({ date: new Date(item.date), value: item.tx_count }))
.sort(sortByDateDesc),
name: 'Tx/day',
color: COLOR,
valueFormatter: (x) => x.toString(),
} ]),
}, },
}; };
...@@ -34,9 +40,14 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { ...@@ -34,9 +40,14 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
api: { api: {
queryName: QueryKeys.chartsMarket, queryName: QueryKeys.chartsMarket,
path: '/node-api/stats/charts/market', path: '/node-api/stats/charts/market',
dataFn: (response) => response.chart_data dataFn: (response) => ([ {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) })) items: response.chart_data
.sort(sortByDateDesc), .map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
.sort(sortByDateDesc),
name: `${ appConfig.network.currency.symbol } price`,
color: COLOR,
valueFormatter: (x) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
} ]),
}, },
}; };
...@@ -50,9 +61,14 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { ...@@ -50,9 +61,14 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
api: { api: {
queryName: QueryKeys.chartsMarket, queryName: QueryKeys.chartsMarket,
path: '/node-api/stats/charts/market', path: '/node-api/stats/charts/market',
dataFn: (response) => response.chart_data dataFn: (response) => ([ {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) })) items: response.chart_data
.sort(sortByDateDesc), .map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
.sort(sortByDateDesc),
name: 'Market cap',
color: COLOR,
valueFormatter: (x) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 0 }),
} ]),
}, },
}; };
......
import { useColorModeValue, useToken } from '@chakra-ui/react';
import * as d3 from 'd3'; import * as d3 from 'd3';
import React from 'react'; import React from 'react';
...@@ -13,6 +14,8 @@ interface Props extends React.SVGProps<SVGPathElement> { ...@@ -13,6 +14,8 @@ interface Props extends React.SVGProps<SVGPathElement> {
const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }: Props) => { const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }: Props) => {
const ref = React.useRef(null); const ref = React.useRef(null);
const gradientStopColor = useToken('colors', useColorModeValue('whiteAlpha.200', 'blackAlpha.100'));
React.useEffect(() => { React.useEffect(() => {
if (disableAnimation) { if (disableAnimation) {
d3.select(ref.current).attr('opacity', 1); d3.select(ref.current).attr('opacity', 1);
...@@ -39,8 +42,8 @@ const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }: ...@@ -39,8 +42,8 @@ const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }:
{ color && ( { color && (
<defs> <defs>
<linearGradient id={ `gradient-${ color }` } x1="0%" x2="0%" y1="0%" y2="100%"> <linearGradient id={ `gradient-${ color }` } x1="0%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stopColor={ color } stopOpacity={ 0.8 }/> <stop offset="2%" stopColor={ color }/>
<stop offset="100%" stopColor={ color } stopOpacity={ 0.02 }/> <stop offset="78%" stopColor={ gradientStopColor }/>
</linearGradient> </linearGradient>
</defs> </defs>
) } ) }
......
...@@ -14,15 +14,22 @@ interface Props { ...@@ -14,15 +14,22 @@ interface Props {
anchorEl: SVGRectElement | null; anchorEl: SVGRectElement | null;
} }
const TEXT_LINE_HEIGHT = 12;
const PADDING = 16;
const LINE_SPACE = 10;
const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, anchorEl, ...props }: Props) => { const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, anchorEl, ...props }: Props) => {
const margin = React.useMemo(() => ({ const margin = React.useMemo(() => ({
top: 0, bottom: 0, left: 0, right: 0, top: 0, bottom: 0, left: 0, right: 0,
..._margin, ..._margin,
}), [ _margin ]); }), [ _margin ]);
const lineColor = useToken('colors', 'red.500'); const lineColor = useToken('colors', 'gray.400');
const textColor = useToken('colors', useColorModeValue('white', 'black')); const titleColor = useToken('colors', 'blue.100');
const bgColor = useToken('colors', useColorModeValue('gray.900', 'gray.400')); const textColor = useToken('colors', 'white');
const markerBgColor = useToken('colors', useColorModeValue('black', 'white'));
const markerBorderColor = useToken('colors', useColorModeValue('white', 'black'));
const bgColor = useToken('colors', 'blackAlpha.900');
const ref = React.useRef(null); const ref = React.useRef(null);
const isPressed = React.useRef(false); const isPressed = React.useRef(false);
...@@ -52,8 +59,8 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an ...@@ -52,8 +59,8 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an
}); });
tooltipContent tooltipContent
.select('.ChartTooltip__contentTitle') .select('.ChartTooltip__contentDate')
.text(d3.timeFormat('%b %d, %Y')(xScale.invert(x))); .text(d3.timeFormat('%e %b %Y')(xScale.invert(x)));
}, },
[ xScale, margin, width ], [ xScale, margin, width ],
); );
...@@ -61,8 +68,8 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an ...@@ -61,8 +68,8 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an
const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => { const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => {
d3.selectAll('.ChartTooltip__value') d3.selectAll('.ChartTooltip__value')
.filter((td, tIndex) => tIndex === i) .filter((td, tIndex) => tIndex === i)
.text(d.value.toLocaleString()); .text(data[i].valueFormatter?.(d.value) || d.value.toLocaleString());
}, []); }, [ data ]);
const drawCircles = React.useCallback((event: MouseEvent) => { const drawCircles = React.useCallback((event: MouseEvent) => {
const [ x ] = d3.pointer(event, anchorEl); const [ x ] = d3.pointer(event, anchorEl);
...@@ -146,34 +153,72 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an ...@@ -146,34 +153,72 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an
return ( return (
<g ref={ ref } opacity={ 0 } { ...props }> <g ref={ ref } opacity={ 0 } { ...props }>
<line className="ChartTooltip__line" stroke={ lineColor }/> <line className="ChartTooltip__line" stroke={ lineColor } strokeDasharray="3"/>
<g className="ChartTooltip__content"> <g className="ChartTooltip__content">
<rect className="ChartTooltip__contentBg" rx={ 8 } ry={ 8 } fill={ bgColor } width={ 125 } height={ data.length * 22 + 34 }/> <rect
<text className="ChartTooltip__contentBg"
className="ChartTooltip__contentTitle" rx={ 12 }
transform="translate(8,20)" ry={ 12 }
fontSize="12px" fill={ bgColor }
fontWeight="bold" width={ 200 }
fill={ textColor } height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE }
pointerEvents="none"
/> />
<g> <g transform={ `translate(${ PADDING },${ PADDING })` }>
{ data.map(({ name, color }, index) => ( <text
<g key={ name } className="ChartTooltip__contentLine" transform={ `translate(12,${ 40 + index * 20 })` }> className="ChartTooltip__contentTitle"
<circle r={ 4 } fill={ color }/> transform="translate(0,0)"
<text fontSize="12px"
transform="translate(10,4)" fontWeight="500"
className="ChartTooltip__value" fill={ titleColor }
fontSize="12px" dominantBaseline="hanging"
fill={ textColor } >
pointerEvents="none" Date
/> </text>
</g> <text
)) } className="ChartTooltip__contentDate"
transform="translate(80,0)"
fontSize="12px"
fontWeight="500"
fill={ textColor }
dominantBaseline="hanging"
/>
</g> </g>
{ data.map(({ name }, index) => (
<g
key={ name }
transform={ `translate(${ PADDING },${ PADDING + (index + 1) * (LINE_SPACE + TEXT_LINE_HEIGHT) })` }
>
<text
className="ChartTooltip__contentTitle"
transform="translate(0,0)"
fontSize="12px"
fontWeight="500"
fill={ titleColor }
dominantBaseline="hanging"
>
{ name }
</text>
<text
transform="translate(80,0)"
className="ChartTooltip__value"
fontSize="12px"
fill={ textColor }
dominantBaseline="hanging"
/>
</g>
)) }
</g> </g>
{ data.map(({ name, color }) => ( { data.map(({ name }) => (
<circle key={ name } className="ChartTooltip__linePoint" r={ 4 } opacity={ 0 } fill={ color } stroke="#FFF" strokeWidth={ 1 }/> <circle
key={ name }
className="ChartTooltip__linePoint"
r={ 8 }
opacity={ 0 }
fill={ markerBgColor }
stroke={ markerBorderColor }
strokeWidth={ 4
}
/>
)) } )) }
</g> </g>
); );
......
...@@ -19,6 +19,7 @@ export interface TimeChartDataItem { ...@@ -19,6 +19,7 @@ export interface TimeChartDataItem {
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
name: string; name: string;
color?: string; color?: string;
valueFormatter?: (value: number) => string;
} }
export type TimeChartData = Array<TimeChartDataItem>; export type TimeChartData = Array<TimeChartDataItem>;
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