From cf6e6fadc5ed490fec431cd0538d27b493c28472 Mon Sep 17 00:00:00 2001 From: Nikaru Date: Sun, 26 May 2024 14:18:17 +0000 Subject: [PATCH 1/2] fix: add search by transaction hash --- constant/index.ts | 1 + pages/search.tsx | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/constant/index.ts b/constant/index.ts index 57b8ff7..a0910d2 100644 --- a/constant/index.ts +++ b/constant/index.ts @@ -3,6 +3,7 @@ import { StatisticDaysFilter } from 'types'; export const MATCH_TYPE = { ADDRESS: 'ADDRESS', REQUESTID: 'REQUESTID', + TXHASH: 'TXHASH', }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/pages/search.tsx b/pages/search.tsx index 30b3e96..31e73d3 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -56,6 +56,18 @@ export const getServerSideProps: GetServerSideProps = async (context) => { }, }; } + + if (result?.length && result[0].matchType === MATCH_TYPE.TXHASH) { + const { requestId } = result[0]; + if (requestId) + return { + redirect: { + permanent: false, + destination: `/swap/${requestId}`, + }, + }; + } + return { props: {}, }; From 5518cbc868768b43a7f8d56bf0f72dd5cd70448c Mon Sep 17 00:00:00 2001 From: Nikaru <141495369+nikaaru@users.noreply.github.com> Date: Wed, 5 Jun 2024 12:25:57 +0330 Subject: [PATCH 2/2] fix: update bar chart library --- components/common/Select/index.tsx | 2 +- .../statistics/ChartBarBox/BarChart.tsx | 456 +++++++++++++----- .../ChartBarBox/ChartBarBox.helper.ts | 144 ++++-- .../ChartBarBox/ChartBarBox.type.ts | 37 +- components/statistics/ChartBarBox/index.tsx | 91 ++-- pages/statistics.tsx | 3 +- 6 files changed, 512 insertions(+), 221 deletions(-) diff --git a/components/common/Select/index.tsx b/components/common/Select/index.tsx index 0ee9437..9d29dc9 100644 --- a/components/common/Select/index.tsx +++ b/components/common/Select/index.tsx @@ -52,7 +52,7 @@ export function Select(props: SelectProps) { {open && (
diff --git a/components/statistics/ChartBarBox/BarChart.tsx b/components/statistics/ChartBarBox/BarChart.tsx index f59b348..16c18a9 100644 --- a/components/statistics/ChartBarBox/BarChart.tsx +++ b/components/statistics/ChartBarBox/BarChart.tsx @@ -1,152 +1,350 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - AnimatedAxis, - AnimatedGrid, - XYChart, - BarSeries, - Tooltip, - BarStack, -} from '@visx/xychart'; -import isMobile from 'is-mobile'; - -import { BarChartProps } from './ChartBarBox.type'; + BarChartProps, + BarStackDataType, + TooltipDataType, +} from './ChartBarBox.type'; import { getDayOfMonth } from 'utils/common'; import { + DEFAULT_MARGIN, + DesktopBottomAxisData, barChartColors, - transactionTheme, - volumeTheme, + getTotalValueDates, + mobileBottomAxisData, } from './ChartBarBox.helper'; import { AmountConverter, compactNumberFormat } from 'utils/amountConverter'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; -import React from 'react'; +import React, { Fragment, useEffect, useRef } from 'react'; + +import { BarStack } from '@visx/shape'; + +import { Group } from '@visx/group'; +import { Grid } from '@visx/grid'; +import { AxisBottom, AxisLeft } from '@visx/axis'; +import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale'; +import { useTooltip, useTooltipInPortal } from '@visx/tooltip'; +import { localPoint } from '@visx/event'; dayjs.extend(utc); const BarChart = (props: BarChartProps) => { - const IsMobile = isMobile({ tablet: true }); - const { series, type, days } = props; - const barPadding = days === 7 ? 0.6 : 0.4; - const defaultColor = - type === 'transaction' ? barChartColors[0] : barChartColors[1]; + const { + data, + type, + days, + width, + height, + margin = DEFAULT_MARGIN, + colorBlockchainMap, + buckets, + } = props; + + let tooltipTimeout: number; + const tooltipRef = useRef(null); + + // bounds + const xMax = width - margin.left - 20; + const yMax = height - margin.top - 30; + + const isMobile = width <= 640; + // accessors + const getDate = (d: BarStackDataType) => d.date; + + // handle bottom axis data + const allDate = data.map(getDate); + const bottomAxisData = isMobile + ? mobileBottomAxisData + : DesktopBottomAxisData; + + const { intervalBottomAxis, numBottomAxis, startBottomAxis } = + bottomAxisData[days]; + + // Function to generate tick values at intervals of 5, starting from the 5th element + const generateTickValues = (dates: string[]) => { + const tickValues = []; + for (let i = startBottomAxis; i < dates.length; i += intervalBottomAxis) { + tickValues.push(dates[i]); + } + return tickValues; + }; + + const bottomAxisValue = generateTickValues(allDate); + + const { + tooltipOpen, + tooltipLeft, + tooltipTop, + tooltipData, + hideTooltip, + showTooltip, + } = useTooltip(); + + const totalValueDates = getTotalValueDates(data, buckets); + + // scales + const dateScale = scaleBand({ + domain: data.map(getDate), + paddingInner: days === 7 ? 0.3 : 0.5, + paddingOuter: days === 7 ? 0.3 : 0, + }); + + const totalValue = Math.max(...totalValueDates); + + const valueScale = scaleLinear({ + domain: [0, totalValue + totalValue / 5], + nice: true, + }); + + const colorScale = scaleOrdinal({ + domain: buckets, + range: barChartColors, + }); + + const getTotalValue = (dataColumn: BarStackDataType) => { + let result = 0; + Object.keys(dataColumn).forEach((key) => { + if (key !== 'date') { + const value = dataColumn[key]; + if (!isNaN(Number(value))) { + result += Number(value); + } + } + }); + return result; + }; + + const { containerRef, TooltipInPortal } = useTooltipInPortal({ + // TooltipInPortal is rendered in a separate child of + scroll: true, + }); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function handleClickOutside(event: any) { + if ( + isMobile && + tooltipRef?.current && + !tooltipRef.current.contains(event.target) + ) { + hideTooltip(); + } + } + document.addEventListener('click', handleClickOutside); + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [tooltipRef]); + + dateScale.rangeRound([0, xMax]); + valueScale.range([yMax, 0]); + + if (width < 10) return null; return ( - - getDayOfMonth(d)} - /> - compactNumberFormat(d)} - numTicks={3} - /> - - - - {series.map((stackItem) => { - const { name, data, color } = stackItem; - return ( - d.date} - yAccessor={(d) => d.value} - barPadding={0} - colorAccessor={() => color} - /> - ); - })} - - - { - const { tooltipData } = tooltipContext; - const { datumByKey, nearestDatum } = tooltipData; - const { datum } = nearestDatum; - - let totalValue = 0; - Object.keys(datumByKey).forEach((blockchainItem: any) => { - const { datum } = datumByKey[blockchainItem]; - totalValue += datum?.value || 0; - }); - - return ( -
-
+
+ + + + + + + {(barStacks) => { + // barStacks returns an array of series objects broken down by key. + // in this case we've got blockchain name + return barStacks.map((barStack) => { + // each barStack contains an array of bars, which contain the data + // for only that series for a given data point. the number of bars in a + // given stack corresponds to the number of data points in our data array + + return ( + <> + {barStack.bars.map((bar, index) => { + // we can then assume that the data in each stack at a given index + // is related to the data in all other stacks at that index. + const shouldBeHighlighted = + tooltipData?.hoveredIndex === index; + + // we can then decide the opacity for our stacks based on whether the + // tooltip is open, and whether the stack being hovered matches the + // index passed to our tooltipData + const shouldHavePartialOpacity = + !shouldBeHighlighted && tooltipOpen; + + const barColor = + colorBlockchainMap.get(barStack.key) || bar.color; + + return ( + { + if (!isMobile) return; + + if (tooltipTimeout) clearTimeout(tooltipTimeout); + const eventSvgCoords = localPoint(event); + const left = bar.x + bar.width / 2; + setTimeout(() => { + showTooltip({ + tooltipData: { bar, hoveredIndex: index }, + tooltipTop: eventSvgCoords?.y, + tooltipLeft: left, + }); + }, 100); + }} + onMouseLeave={() => { + if (isMobile) return; + tooltipTimeout = window.setTimeout(() => { + hideTooltip(); + }, 300); + }} + onMouseMove={(event) => { + if (isMobile) return; + + if (tooltipTimeout) clearTimeout(tooltipTimeout); + // TooltipInPortal expects coordinates to be relative to containerRef + // localPoint returns coordinates relative to the nearest SVG, which + // is what containerRef is set to in this example. + const eventSvgCoords = localPoint(event); + const left = bar.x + bar.width / 2 + 40; + + // make sure to pass the index of the hovered bar + showTooltip({ + tooltipData: { bar, hoveredIndex: index }, + tooltipTop: eventSvgCoords?.y, + tooltipLeft: left, + }); + }} + /> + ); + })} + + ); + }); + }} + + + + getDayOfMonth(d)} + tickLabelProps={() => ({ + fontSize: isMobile ? 10 : 12, + fill: '#727272', + textAnchor: 'middle', + })} + /> + + compactNumberFormat(Number(d))} + tickLabelProps={() => ({ + fontSize: isMobile ? 10 : 12, + fill: '#727272', + textAnchor: 'middle', + })} + /> + + + {tooltipOpen && tooltipData && ( + +
+ {tooltipData.bar.bar.data.date && ( +
+
+ {dayjs + .utc(tooltipData.bar.bar.data.date) + .local() + .format('YYYY/MM/DD') + .toString()} +
- {datum?.date - ? dayjs - .utc(datum.date) - .local() - .format('YYYY/MM/DD') - .toString() - : ''} + {AmountConverter( + Number(getTotalValue(tooltipData.bar.bar.data).toFixed(2)), + )}
-
{AmountConverter(totalValue)}
+ )} + {Array.from(colorBlockchainMap).map((mapItem) => { + const [blockchainItem, blockchainColor] = mapItem; + const value = tooltipData.bar.bar.data[blockchainItem]; + return ( + +
- {Object.keys(datumByKey).map((datumByKeyItem) => { - const { key, datum } = datumByKey[datumByKeyItem]; - const stackItem = series.find( - (seriesItem) => seriesItem.name === key, - ); - const stackColor = stackItem ? stackItem.color : defaultColor; - return ( - -
- -
-
- - {key} -
- {datum && ( -
- {type === 'volume' && '$'} - {AmountConverter(datum.value.toFixed(2))} -
- )} +
+
+ + + {blockchainItem}
- - ); - })} -
- ); - }} - /> - +
+ {type === 'volume' && '$'} + {!isNaN(Number(value)) + ? AmountConverter(Number(Number(value).toFixed(2))) + : 0} +
+
+
+ ); + })} +
+
+ )} +
); }; diff --git a/components/statistics/ChartBarBox/ChartBarBox.helper.ts b/components/statistics/ChartBarBox/ChartBarBox.helper.ts index 5684a1d..5f9c930 100644 --- a/components/statistics/ChartBarBox/ChartBarBox.helper.ts +++ b/components/statistics/ChartBarBox/ChartBarBox.helper.ts @@ -1,9 +1,15 @@ import { buildChartTheme } from '@visx/xychart'; -import { BarStack, ChartData, ChartType } from './ChartBarBox.type'; +import { + BarStackDataType, + ChartType, + ColorBlockchainMapType, +} from './ChartBarBox.type'; import { DailySummaryType } from 'types'; export const BAR_CHART_BLOCKCHAIN_NUMBER = 10; +export const DEFAULT_MARGIN = { top: 40, right: 0, bottom: 0, left: 20 }; + export const transactionTheme = buildChartTheme({ backgroundColor: 'transparent', colors: ['#469BF5', '#F17606'], @@ -28,7 +34,6 @@ export const barChartColors: string[] = [ '#9DF546', '#F01DA8', '#FF8B66', - '#8566FF', '#44F1E6', '#29DA7A', '#F17606', @@ -36,29 +41,47 @@ export const barChartColors: string[] = [ '#F4C932', ]; +export const mobileBottomAxisData = { + 7: { numBottomAxis: 3, startBottomAxis: 1, intervalBottomAxis: 2 }, + 30: { numBottomAxis: 3, startBottomAxis: 3, intervalBottomAxis: 10 }, + 90: { numBottomAxis: 3, startBottomAxis: 10, intervalBottomAxis: 30 }, +}; + +export const DesktopBottomAxisData = { + 7: { numBottomAxis: 7, startBottomAxis: 0, intervalBottomAxis: 1 }, + 30: { numBottomAxis: 6, startBottomAxis: 4, intervalBottomAxis: 5 }, + 90: { numBottomAxis: 8, startBottomAxis: 5, intervalBottomAxis: 10 }, +}; + export const getBarChartData = (chartOption: { dailyData: DailySummaryType[]; isStackBar: boolean; type: ChartType; }) => { const { dailyData, isStackBar, type } = chartOption; - const dataSeries: BarStack[] = []; - const defaultColor = type === 'transaction' ? '#469BF5' : '#8B62FF'; + const chartData: BarStackDataType[] = []; + const colorBlockchainMap: ColorBlockchainMapType = new Map(); + const buckets: string[] = []; if (!isStackBar) { - const chartData: ChartData[] = dailyData.map((dailyItem) => { - return { - date: dailyItem.date, - value: type === 'transaction' ? dailyItem.count : dailyItem.volume, - }; - }); - dataSeries.push({ - data: chartData, - name: type === 'transaction' ? 'Transactions' : 'Volume', - color: defaultColor, + dailyData.forEach((dailyItem) => { + const dataItem: BarStackDataType = { date: dailyItem.date }; + dataItem[type === 'transaction' ? 'Transactions' : 'Volume'] = + type === 'transaction' + ? dailyItem.count.toString() + : dailyItem.volume.toString(); + + chartData.push(dataItem); }); - return dataSeries; + colorBlockchainMap.set( + type === 'transaction' ? 'Transactions' : 'Volume', + type === 'transaction' ? '#469BF5' : '#8B62FF', + ); + + buckets.push(type === 'transaction' ? 'Transactions' : 'Volume'); + + return { chartData, colorBlockchainMap, buckets }; } // map sum of value base on type(transaction or volume) for each blockchain @@ -75,12 +98,23 @@ export const getBarChartData = (chartOption: { (a, b) => b[1] - a[1], ); - // get n top blockchains for stack bars + // get top blockchains for stack bars const topBlockchain = sortedBlockchain .map((sortedItem) => sortedItem[0]) .slice(0, BAR_CHART_BLOCKCHAIN_NUMBER); - // map chart data for each date + // create map structure for assign color for each blockchain + topBlockchain.forEach((blockchainItem, index) => { + colorBlockchainMap.set( + blockchainItem, + barChartColors[index % barChartColors.length], + ); + buckets.push(blockchainItem); + }); + colorBlockchainMap.set('Others', barChartColors[barChartColors.length - 1]); + buckets.push('Others'); + + // create map structure for assign chart data for each date const dateMap = new Map(); dailyData.forEach((dailyItem) => { if (!dateMap.has(dailyItem.date)) dateMap.set(dailyItem.date, []); @@ -89,49 +123,55 @@ export const getBarChartData = (chartOption: { dateItem?.push(dailyItem); }); - // prepare top blockchain data for stack bar - topBlockchain.forEach((topBlockchainItem, index) => { - const chartData: ChartData[] = []; - dateMap.forEach((dateDailyList, keyDate) => { - const blockchainData = dateDailyList.find( - (listItem) => listItem.bucket === topBlockchainItem, - ); - - // value base on type (transaction or volume) - let blockchainValue = 0; - if (blockchainData) - blockchainValue = - type === 'transaction' ? blockchainData.count : blockchainData.volume; - chartData.push({ date: keyDate, value: blockchainValue }); - }); - - dataSeries.push({ - data: chartData, - name: topBlockchainItem, - color: barChartColors[index % BAR_CHART_BLOCKCHAIN_NUMBER], + // create data result for bar stack chart + dateMap.forEach((dateDailyList, keyDate) => { + const dataItem: BarStackDataType = { date: keyDate }; + dateDailyList + .filter((dailyItem) => topBlockchain.includes(dailyItem.bucket)) + .forEach((topDailyItem) => { + const bucketValue = + type === 'transaction' ? topDailyItem.count : topDailyItem.volume; + dataItem[topDailyItem.bucket] = bucketValue + ? bucketValue.toString() + : '0'; + }); + + topBlockchain.forEach((topItem) => { + if (!(topItem in dataItem)) dataItem[topItem] = '0'; }); - }); - // prepare others blockchain data for stack bar - const othersChartData: ChartData[] = []; - dateMap.forEach((dateDailyList, keyDate) => { - const othersBlockchain = dateDailyList.filter( - (listDataItem) => !topBlockchain.includes(listDataItem.bucket), + const otherBlockchains = dateDailyList.filter( + (dailyItem) => !topBlockchain.includes(dailyItem.bucket), ); - // sum value (base on type) of blockchains other than tops - const othersValue = othersBlockchain + const othersValue = otherBlockchains .map((dailyItem) => type === 'transaction' ? dailyItem.count : dailyItem.volume, ) .reduce((accumulator, currentValue) => accumulator + currentValue, 0); - othersChartData.push({ date: keyDate, value: othersValue }); - }); - dataSeries.push({ - data: othersChartData, - name: 'Others', - color: barChartColors[barChartColors.length - 1], + + dataItem['Others'] = othersValue.toString(); + + chartData.push(dataItem); }); - return dataSeries; + return { chartData, colorBlockchainMap, buckets }; +}; + +export const getTotalValueDates = ( + data: BarStackDataType[], + buckets: string[], +) => { + const totalValueDates = data.reduce((accumulator, currentData) => { + const totalValuePerDate = buckets.reduce((dailyTotal, currentBucket) => { + dailyTotal += !isNaN(Number(currentData[currentBucket])) + ? Number(currentData[currentBucket]) + : 0; + return dailyTotal; + }, 0); + accumulator.push(totalValuePerDate); + return accumulator; + }, [] as number[]); + + return totalValueDates; }; diff --git a/components/statistics/ChartBarBox/ChartBarBox.type.ts b/components/statistics/ChartBarBox/ChartBarBox.type.ts index 8ed9f43..119a0cb 100644 --- a/components/statistics/ChartBarBox/ChartBarBox.type.ts +++ b/components/statistics/ChartBarBox/ChartBarBox.type.ts @@ -1,5 +1,6 @@ import { DailySummaryType, StatisticDaysFilter } from 'types'; import { BlockchainMeta } from 'types/meta'; +import { SeriesPoint } from '@visx/shape/lib/types'; export type ChartType = 'transaction' | 'volume'; @@ -32,27 +33,41 @@ export interface BlockchainFilterProps { onSelect: (selected: string) => void; } -export interface ChartData { - date: string; - value: number; -} - export interface FilterBarChart { source: string; destination: string; breakDownBy: string; } -export interface BarStack { - data: ChartData[]; - name: string; - color: string; -} +export type BarStackDataType = { + [key: string]: string; +}; + +export type ColorBlockchainMapType = Map; export interface BarChartProps { - series: BarStack[]; + data: BarStackDataType[]; type: ChartType; days: StatisticDaysFilter; + width: number; + height: number; + colorBlockchainMap: ColorBlockchainMapType; + buckets: string[]; + margin?: { top: number; right: number; bottom: number; left: number }; } export type BreakDownType = 'None' | 'Source chain' | 'Destination chain'; + +export type TooltipDataType = { + bar: { + bar: SeriesPoint; + key: string; + index: number; + height: number; + width: number; + x: number; + y: number; + color: string; + }; + hoveredIndex: number; +}; diff --git a/components/statistics/ChartBarBox/index.tsx b/components/statistics/ChartBarBox/index.tsx index c15700e..124caec 100644 --- a/components/statistics/ChartBarBox/index.tsx +++ b/components/statistics/ChartBarBox/index.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from 'react'; -import { BarStack, FilterBarChart, PropsType } from './ChartBarBox.type'; +import React, { useEffect, useMemo, useState } from 'react'; +import { FilterBarChart, PropsType } from './ChartBarBox.type'; import { numberWithCommas } from 'utils/amountConverter'; import { SelectBlockchain } from 'components/common/SelectBlockchain'; import { BreakDownList, DailySummaryOption, DailySummaryType } from 'types'; @@ -10,6 +10,7 @@ import { OptionType } from 'components/common/Select/Select.types'; import { getBarChartData } from './ChartBarBox.helper'; import { ActiveFilterIcon, FilterIcon, LoadingIcon } from 'components/icons'; import ModalFilter from './ModalFilter'; +import ParentSize from '@visx/responsive/lib/components/ParentSize'; const BarChart = dynamic(() => import('./BarChart'), { ssr: false, @@ -37,11 +38,13 @@ function ChartBarBox(props: PropsType) { const { source, destination, breakDownBy } = filter; - const totalValue = dailyData - .map((dailyItem) => - type === 'transaction' ? dailyItem.count : dailyItem.volume, - ) - .reduce((accumulator, currentValue) => accumulator + currentValue, 0); + const totalValue = + dailyData && + dailyData + .map((dailyItem) => + type === 'transaction' ? dailyItem.count : dailyItem.volume, + ) + .reduce((accumulator, currentValue) => accumulator + currentValue, 0); const breakDownOptions: OptionType[] = Object.keys(BreakDownList).map( (breakItem) => { @@ -68,6 +71,7 @@ function ChartBarBox(props: PropsType) { } useEffect(() => { + setLoading(true); fetchDailySummaryData(); }, [days]); @@ -87,11 +91,16 @@ function ChartBarBox(props: PropsType) { (!!destination && breakDownBy === BreakDownList['Destination chain']) ); - const dataSeries: BarStack[] = getBarChartData({ - dailyData, - isStackBar, - type, - }); + const { chartData, colorBlockchainMap, buckets } = useMemo( + () => + getBarChartData({ + dailyData, + isStackBar, + type, + }), + + [type, dailyData, isStackBar], + ); return (
-
- {isStackBar && !loading && ( +
+ {isStackBar && ( <> -
- +
+ {!loading && ( + + {({ width, height }) => ( + + )} + + )}
-
- {dataSeries.map((dataItem, index) => { - const { name, color: stackColor } = dataItem; - +
+ {Array.from(colorBlockchainMap).map((mapItem, index) => { + const [blockchainItem, blockchainColor] = mapItem; return ( - +
- {name} + {blockchainItem}
- {index !== dataSeries.length - 1 && ( + + {index !== colorBlockchainMap.size - 1 && (
)}
@@ -218,9 +241,23 @@ function ChartBarBox(props: PropsType) { )} - {!isStackBar && !loading && ( -
- + {!isStackBar && ( +
+ {!loading && ( + + {({ width, height }) => ( + + )} + + )}
)}
diff --git a/pages/statistics.tsx b/pages/statistics.tsx index f4fecbb..db27936 100644 --- a/pages/statistics.tsx +++ b/pages/statistics.tsx @@ -62,7 +62,7 @@ function Statistics(props: PropsType) { topSourceByVolume, } = topListSummary || {}; - if (blockchains) { + if (blockchains && !status) { blockchains.forEach((blockchainItem) => { blockchainDataMap.set(blockchainItem.name, blockchainItem); }); @@ -192,6 +192,7 @@ export const getServerSideProps: GetServerSideProps = async () => { days: DEFAULT_STATISTIC_DAYS, breakDownBy: BreakDownList.None, }); + const topListSummary = await getTopListSummary(DEFAULT_STATISTIC_DAYS); return { props: {