Completed daily txs chart

pull/219/head
artemkolodko 2 years ago
parent 641575154d
commit 41cda2b33e
  1. 16
      src/api/client.ts
  2. 92
      src/pages/ChartsPage/ActiveAddresses.tsx
  3. 65
      src/pages/ChartsPage/AverageFee.tsx
  4. 61
      src/pages/ChartsPage/ChartFilter.tsx
  5. 112
      src/pages/ChartsPage/DailyTransactions.tsx
  6. 8
      src/pages/ChartsPage/index.tsx
  7. 161
      src/pages/ChartsPage/utils.ts
  8. 11
      src/types/api.ts

@ -1,11 +1,11 @@
import { transport } from "./explorer";
import {
Block,
InternalTransaction,
RPCStakingTransactionHarmony,
RPCTransactionHarmony,
RelatedTransaction,
Log, LogDetailed, AddressDetails
Block,
InternalTransaction,
RPCStakingTransactionHarmony,
RPCTransactionHarmony,
RelatedTransaction,
Log, LogDetailed, AddressDetails, MetricsType, MetricsDailyItem
} from "src/types";
import {
IHoldersInfo,
@ -110,6 +110,10 @@ export function getWalletsCountLast14Days(limit = 14) {
return transport("getWalletsCountLast14Days", [limit]) as Promise<any[]>;
}
export function getMetricsByType(type: MetricsType, offset = 0, limit = 14) {
return transport("getMetricsByType", [type, offset, limit]) as Promise<MetricsDailyItem[]>;
}
export function getContractsByField(params: any[]) {
return transport("getContractsByField", params) as Promise<AddressDetails>;
}

@ -1,26 +1,39 @@
import React, {useEffect, useState} from 'react'
import {Box, Heading, Text} from "grommet";
import {Box, Heading} from "grommet";
import {BaseContainer, BasePage} from "../../components/ui";
import {getWalletsCountLast14Days} from "../../api/client";
import {getMetricsByType} from "../../api/client";
import dayjs from "dayjs";
import {palette} from "../../theme";
import {useThemeMode} from "../../hooks/themeSwitcherHook";
import {Line as LineChartJs} from "react-chartjs-2";
import {getChartOptions} from "../../components/metrics/common";
import {MetricsDailyItem, MetricsType} from "../../types";
import { getDetailedChartOptions, getChartData } from './utils'
export const ActiveAddresses = () => {
const themeMode = useThemeMode();
const [result, setResult] = useState<any[]>([]);
const [txs, setTxs] = useState<any[]>([]);
const [wallets, setWallets] = useState<any[]>([]);
const [fee, setFee] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const enrichResponse = (items: MetricsDailyItem[]) => {
return items.reverse().map(item => ({
...item,
timestamp: item.date
}))
}
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true);
const res = await getWalletsCountLast14Days(1000);
const test = Array(1).fill(null).map(_ => res).flat(1)
console.log('test', test)
setResult(test);
const txsResults = await getMetricsByType(MetricsType.transactionsCount, 0, 1000)
const walletsResults = await getMetricsByType(MetricsType.walletsCount, 0, 1000)
const feeResults = await getMetricsByType(MetricsType.averageFee, 0, 1000)
console.log('feeResults', feeResults)
setTxs(enrichResponse(txsResults));
setWallets(enrichResponse(walletsResults));
setFee(enrichResponse(feeResults));
setIsLoading(false);
} catch (e) {
console.error('Error on loading metrics:', e)
@ -29,45 +42,44 @@ export const ActiveAddresses = () => {
loadData()
}, [])
const data = {
// labels: result.map((i) => dayjs(i.date).format("dddd, MMMM DD YYYY")),
labels: result.map((i, index) => dayjs(i.date).format("dddd, MMMM DD YYYY") + "_index_" + index),
datasets: [{
label: "Active wallets",
data: result.map((i) => +i.count),
borderColor: themeMode === 'light' ? palette.DarkGray : palette.MintGreen,
borderWidth: 1,
backgroundColor: 'white',
pointRadius: 0,
pointHoverRadius: 8,
pointBorderWidth: 0,
pointBorderColor: 'transparent',
pointHoverBackgroundColor: themeMode === 'light' ? 'rgba(85, 98, 109, 0.4)' : 'rgba(105, 250, 189, 0.4)',
}]
}
let min = Number.MAX_SAFE_INTEGER;
result.forEach(e=>{
if (min > +e.count) {
min = +e.count;
}
});
const items = result.map((item) => {
return {
...item,
timestamp: item.date
}
})
// let min = Number.MAX_SAFE_INTEGER;
// txs.forEach(e => {
// if (min > +e.value) {
// min = +e.value;
// }
// });
return <BaseContainer pad={{ horizontal: "0" }}>
<Heading size="small" margin={{ bottom: "medium", top: "0" }}>
<Box direction={"row"}>Active addresses</Box>
{/*<Heading size="small" margin={{ bottom: "medium", top: "0" }}>*/}
{/* <Box direction={"row"}>Daily Transactions</Box>*/}
{/*</Heading>*/}
{/*<BasePage pad={"small"} style={{overflow: 'inherit'}}>*/}
{/* <Box style={{ width: "100%" }} direction={"row"} align={'center'}>*/}
{/* {!isLoading && (*/}
{/* // @ts-ignore*/}
{/* <LineChartJs options={getDetailedChartOptions(themeMode, txs)} data={getChartData(txs)} height="50px" />*/}
{/* )}*/}
{/* </Box>*/}
{/*</BasePage>*/}
{/*<Heading size="small" margin={{ bottom: "medium", top: "16px" }}>*/}
{/* <Box direction={"row"}>Daily Active Addresses</Box>*/}
{/*</Heading>*/}
{/*<BasePage pad={"small"} style={{overflow: 'inherit'}}>*/}
{/* <Box style={{ width: "100%" }} direction={"row"} align={'center'}>*/}
{/* {!isLoading && (*/}
{/* // @ts-ignore*/}
{/* <LineChartJs options={getDetailedChartOptions(themeMode, wallets)} data={getChartData(themeMode, wallets)} height="50px" />*/}
{/* )}*/}
{/* </Box>*/}
{/*</BasePage>*/}
<Heading size="small" margin={{ bottom: "medium", top: "16px" }}>
<Box direction={"row"}>Daily Average Fee</Box>
</Heading>
<BasePage pad={"small"} style={{overflow: 'inherit'}}>
<Box style={{ width: "100%" }} direction={"row"} align={'center'}>
{!isLoading && (
// @ts-ignore
<LineChartJs options={getChartOptions(themeMode, items)} data={data} height="50px" />
<LineChartJs options={getDetailedChartOptions(themeMode, fee)} data={getChartData(themeMode, fee, 'Average fee')} height="50px" />
)}
</Box>
</BasePage>

@ -0,0 +1,65 @@
import React, {useEffect, useState} from 'react'
import {Box, Heading} from "grommet";
import {BaseContainer, BasePage} from "../../components/ui";
import {getMetricsByType} from "../../api/client";
import dayjs from "dayjs";
import {palette} from "../../theme";
import {useThemeMode} from "../../hooks/themeSwitcherHook";
import {Line as LineChartJs} from "react-chartjs-2";
import {MetricsDailyItem, MetricsType} from "../../types";
import { getDetailedChartOptions, getChartData } from './utils'
export const ActiveAddresses = () => {
const themeMode = useThemeMode();
const [txs, setTxs] = useState<any[]>([]);
const [wallets, setWallets] = useState<any[]>([]);
const [fee, setFee] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const enrichResponse = (items: MetricsDailyItem[]) => {
return items.reverse().map(item => ({
...item,
timestamp: item.date
}))
}
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true);
const txsResults = await getMetricsByType(MetricsType.transactionsCount, 0, 1000)
const walletsResults = await getMetricsByType(MetricsType.walletsCount, 0, 1000)
const feeResults = await getMetricsByType(MetricsType.averageFee, 0, 1000)
console.log('feeResults', feeResults)
setTxs(enrichResponse(txsResults));
setWallets(enrichResponse(walletsResults));
setFee(enrichResponse(feeResults));
setIsLoading(false);
} catch (e) {
console.error('Error on loading metrics:', e)
}
}
loadData()
}, [])
// let min = Number.MAX_SAFE_INTEGER;
// txs.forEach(e => {
// if (min > +e.value) {
// min = +e.value;
// }
// });
return <BaseContainer pad={{ horizontal: "0" }}>
<Heading size="small" margin={{ bottom: "medium", top: "16px" }}>
<Box direction={"row"}>Daily Average Fee</Box>
</Heading>
<BasePage pad={"small"} style={{overflow: 'inherit'}}>
<Box style={{ width: "100%" }} direction={"row"} align={'center'}>
{!isLoading && (
// @ts-ignore
<LineChartJs options={getDetailedChartOptions(themeMode, fee)} data={getChartData(themeMode, fee, 'Average fee')} height="50px" />
)}
</Box>
</BasePage>
</BaseContainer>
}

@ -0,0 +1,61 @@
import React, {useState} from 'react'
import {Box} from "grommet";
export enum ChartOption {
month = 'month',
month3 = 'month3',
year = 'year',
ytd = 'ytd',
all = 'all'
}
const OptionAlias = {
[ChartOption.month]: '1M',
[ChartOption.month3]: '3M',
[ChartOption.year]: ['1Y'],
[ChartOption.ytd]: ['YTD'],
[ChartOption.all]: ['ALL'],
}
export const ChartOptions = Object.values(ChartOption)
const Option = (props: { value: ChartOption, isActive: boolean, onSelect: (value: ChartOption) => void }) => {
const {value, isActive, onSelect} = props
return <Box
round={'8px'}
pad={'8px'}
width={'48px'}
background={isActive ? 'background' : 'unset'}
align={'center'}
onClick={() => onSelect(value)}
style={{ cursor: 'pointer', fontWeight: isActive ? 'bold': 'normal' }}
>
{OptionAlias[value]}
</Box>
}
export interface ChartFilterProps {
activeOption: ChartOption
onSelect: (option: ChartOption) => void
disabled?: boolean
}
export const ChartFilter = (props: ChartFilterProps) => {
const { activeOption, disabled, onSelect } = props
return <Box
pad={'8px'}
direction={'row'}
gap={'8px'}
round={'8px'}
background={'backgroundMark'}
>
{ChartOptions.map(option => <Option
key={option}
value={option}
isActive={activeOption === option}
onSelect={(option) => !disabled ? onSelect(option) : undefined}
/>)}
</Box>
}

@ -0,0 +1,112 @@
import React, {useEffect, useState} from 'react'
import {Box, Heading, Spinner, Text} from "grommet";
import {BaseContainer, BasePage} from "../../components/ui";
import {getMetricsByType} from "../../api/client";
import {useThemeMode} from "../../hooks/themeSwitcherHook";
import {Line as LineChartJs} from "react-chartjs-2";
import {MetricsDailyItem, MetricsType} from "../../types";
import {getDetailedChartOptions, getChartData, enrichResponse, getLimitByFilterOption} from './utils'
import {ChartFilter, ChartOption} from "./ChartFilter";
import styled from "styled-components";
import dayjs from "dayjs";
import {Info} from "grommet-icons";
const SpinnerContainer = styled(Box)`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const DailyTransactions = () => {
const themeMode = useThemeMode();
const [txs, setTxs] = useState<any[]>([]);
const [cache, setCache] = useState<MetricsDailyItem[]>([])
const [isLoading, setIsLoading] = useState<boolean>(false);
const [filterOption, setFilterOption] = useState(ChartOption.year)
const [minValue, setMinValue] = useState<MetricsDailyItem>()
const [maxValue, setMaxValue] = useState<MetricsDailyItem>()
const applyFilter = (cachedData: MetricsDailyItem[]) => {
const limit = getLimitByFilterOption(filterOption)
const txsData = cachedData.slice(-limit)
const sortedTxs = [...txsData].sort((a, b) => +a.value - +b.value)
setTxs(cachedData.slice(-limit))
setMinValue(sortedTxs[0])
setMaxValue(sortedTxs[sortedTxs.length - 1])
}
useEffect(() => {
if(cache.length > 0) {
applyFilter(cache)
}
}, [filterOption])
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true)
const data = await getMetricsByType(MetricsType.transactionsCount, 0, 1000)
const cachedData = enrichResponse(data)
setCache(cachedData)
applyFilter(cachedData)
} catch (e) {
console.error('Error on loading metrics:', e)
} finally {
setIsLoading(false);
}
}
loadData()
}, [])
const chartOptions = getDetailedChartOptions(themeMode, txs, { yAxisLabel: 'Transactions per day' })
const chartData = getChartData(themeMode, txs, 'Daily transactions')
// @ts-ignore
return <BaseContainer pad={{ horizontal: "0" }}>
<Heading size="small" margin={{ bottom: "medium", top: "0" }}>
<Box direction={"row"}>Harmony Daily Transactions Chart</Box>
</Heading>
<BasePage pad={"small"}>
<Box direction={'row'} justify={'between'}>
<Box direction={'row'} gap={'4px'} align={'center'}>
<Info size={'small'} />
<Text>Highest number of {Intl.NumberFormat('en-US').format(maxValue ? +maxValue.value : 0)} transactions
on {dayjs(maxValue?.date).format('dddd, MMMM D, YYYY')}</Text>
</Box>
<Box direction={'row'} gap={'4px'} align={'center'}>
<Info size={'small'} />
<Text>Lowest number of {Intl.NumberFormat('en-US').format(minValue ? +minValue.value : 0)} transactions
on {dayjs(minValue?.date).format('dddd, MMMM D, YYYY')}</Text>
</Box>
</Box>
</BasePage>
<BasePage pad={"small"} style={{overflow: 'inherit', marginTop: '16px'}}>
<Box align={'end'}>
<ChartFilter
disabled={isLoading}
activeOption={filterOption}
onSelect={(o) => setFilterOption(o)}
/>
</Box>
<Box
width={'100%'}
direction={"row"}
align={'center'}
justify={'center'}
>
{isLoading && <SpinnerContainer justify={'center'} gap={'16px'} align={'center'}>
<Spinner size={'medium'} />
<Text>Loading Data</Text>
</SpinnerContainer>}
<Box style={{ filter: isLoading ? 'blur(2px)': 'unset' }} height="inherit" width={'inherit'} >
{
// @ts-ignore
<LineChartJs options={chartOptions} data={chartData} height="60px" />
}
</Box>
</Box>
</BasePage>
</BaseContainer>
}

@ -3,6 +3,7 @@ import { Box, Heading, Text } from "grommet";
import { BasePage, BaseContainer } from "src/components/ui";
import {Route, Switch, useHistory, useLocation, useParams, useRouteMatch} from "react-router-dom";
import {ActiveAddresses} from "./ActiveAddresses";
import {DailyTransactions} from "./DailyTransactions";
export function ChartsPage() {
// @ts-ignore
@ -14,7 +15,9 @@ export function ChartsPage() {
const navigate = (path: string) => history.push(path)
if(route === 'addresses') {
if(route === 'tx') {
return <DailyTransactions />
} else if(route === 'addresses') {
return <ActiveAddresses />
}
@ -27,6 +30,9 @@ export function ChartsPage() {
<Box style={{ width: "200px" }} direction={"row"} align={'center'}>
<Text onClick={() => navigate('/charts/addresses')}>daily active addresses</Text>
</Box>
<Box style={{ width: "200px" }} direction={"row"} align={'center'}>
<Text onClick={() => navigate('/charts/tx')}>txs</Text>
</Box>
</BasePage>
</BaseContainer>
);

@ -0,0 +1,161 @@
import {palette} from "../../theme";
import dayjs from "dayjs";
import dayOfYear from "dayjs/plugin/dayOfYear";
import {MetricsDailyItem} from "../../types";
import {ChartOption, ChartOptions} from "./ChartFilter";
dayjs.extend(dayOfYear)
export const getChartData = (themeMode: string, items: MetricsDailyItem[], label: string) => {
return {
labels: items.map((i) => dayjs(i.date).format("dddd, MMMM DD YYYY")),
datasets: [{
label,
data: items.map((i) => +i.value),
borderColor: themeMode === 'light' ? palette.Purple : palette.MintGreen,
borderWidth: 2,
backgroundColor: 'white',
pointRadius: 0,
pointHoverRadius: 8,
pointBorderWidth: 0,
pointBorderColor: 'transparent',
pointHoverBackgroundColor: themeMode === 'light' ? 'rgba(85, 98, 109, 0.4)' : 'rgba(105, 250, 189, 0.4)',
}]
}
}
export interface OptionsConfig {
yAxisLabel?: string
}
export const getDetailedChartOptions = (themeMode: 'light' | 'dark', points: any, config: OptionsConfig = {}) => {
const { yAxisLabel } = config
const [minPoint] = [...points]
.sort((a: { value: string; }, b: { value: string; }) => +a.value - +b.value)
let minY = minPoint ? minPoint.value : 0
const minPointLog = Math.floor(Math.log10(minY))
minY = minY - (minY % Math.pow(10, minPointLog))
const ticksColor = themeMode === 'light' ? palette.DarkGray : palette.WhiteGrey
const tooltipColor = themeMode === 'light' ? '#3f4850' : palette.WhiteGrey
const tooltipBorderColor = themeMode === 'light' ? '#3f4850' : palette.DarkBlue
const tooltipBackground = themeMode === 'light' ? 'rgba(244, 247, 249, 0.85)' : 'rgba(27, 41, 94, 0.95)'
return {
responsive: true,
animation: false,
animations: {
colors: false,
x: false,
},
tooltips: {
mode: 'index',
intersect: false
},
hover: {
intersect: false
},
plugins: {
legend: {
display: false,
},
title: {
display: false,
},
// https://www.chartjs.org/docs/latest/configuration/tooltip.html
tooltip: {
intersect: false,
displayColors: false, // Removes colored square icon
caretPadding: 8,
caretSize: 8,
cornerRadius: 4,
titleSpacing: 4,
titleFont: { weight: 400, size: 10 },
bodyFont: { weight: 'bold' },
backgroundColor: tooltipBackground,
borderColor: tooltipBorderColor,
borderWidth: 1,
titleColor: tooltipColor,
bodyColor: tooltipColor
}
},
scales: {
x: {
grid: {
display: false,
drawBorder: true,
},
ticks: {
color: ticksColor,
maxTicksLimit: 1000,
maxRotation: 0,
minRotation: 0,
align: 'start',
callback: function(value: string, index: any, ticks: any) {
const item = points[index]
const nextItem = points[index + 1]
// Month change
if(nextItem) {
if (dayjs(item.timestamp).month() !== dayjs(nextItem.timestamp).month() &&
([0, 6].includes(dayjs(item.timestamp).month()))) {
return dayjs(item.timestamp).format("MMM 'YY")
}
}
return '';
}
},
},
y: {
min: minY,
title: {
display: !!yAxisLabel,
text: yAxisLabel
},
grid: {
display: true,
drawBorder: true,
},
ticks: {
color: ticksColor,
callback: function(value: string, index: any, ticks: any) {
if (index === 0 || index === ticks.length - 1 || index === Math.round(ticks.length / 2 - 1)) {
return Intl.NumberFormat('en-US', {
notation: "compact",
maximumFractionDigits: 4
}).format(+value);
}
return '';
}
}
}
},
tension: 0.4, // Curve line
borderWidth: 1,
};
}
export const enrichResponse = (items: MetricsDailyItem[]) => {
return items.reverse().map(item => ({
...item,
timestamp: item.date
}))
}
export const getLimitByFilterOption = (option: ChartOption) => {
switch(option) {
case ChartOption.month: return 30
case ChartOption.month3: return 30 * 3
case ChartOption.year: {
return 365
}
case ChartOption.ytd: {
const date1 = dayjs()
const date2 = dayjs().startOf('year')
return date1.diff(date2, 'day')
}
case ChartOption.all: return 1000
default:
return 1000
}
}

@ -45,3 +45,14 @@ export interface IGetTxsHistoryParams {
txType?: RequestTxType;
order?: RequestOrder
}
export interface MetricsDailyItem {
date: string
value: string
}
export enum MetricsType {
transactionsCount = 'transactions_count',
walletsCount = 'wallets_count',
averageFee = 'average_fee',
}

Loading…
Cancel
Save