Merge pull request #219 from ArtemKolodko/metrics_refactoring

Add metrics page
pull/222/head
Artem 2 years ago committed by GitHub
commit c8531ac3ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/Routes.tsx
  2. 24
      src/api/client.ts
  3. 14
      src/components/appHeader/ToolsButton.tsx
  4. 50
      src/pages/ChartsPage/ActiveAddresses.tsx
  5. 49
      src/pages/ChartsPage/AverageBlockSize.tsx
  6. 49
      src/pages/ChartsPage/AverageFee.tsx
  7. 62
      src/pages/ChartsPage/ChartFilter.tsx
  8. 186
      src/pages/ChartsPage/DailyChartPage.tsx
  9. 49
      src/pages/ChartsPage/DailyTransactions.tsx
  10. 104
      src/pages/ChartsPage/index.tsx
  11. BIN
      src/pages/ChartsPage/thumbnails/daily_addresses.png
  12. BIN
      src/pages/ChartsPage/thumbnails/daily_blocksize.png
  13. BIN
      src/pages/ChartsPage/thumbnails/daily_fee.png
  14. BIN
      src/pages/ChartsPage/thumbnails/daily_txs.png
  15. 212
      src/pages/ChartsPage/utils.ts
  16. 12
      src/types/api.ts

@ -15,6 +15,7 @@ import { ExportData } from "./pages/ExportData";
import { InventoryDetailsPage } from "./pages/InventoryDetailsPage/InventoryDetailsPage";
import { ApprovalPage } from "./pages/ApprovalPage";
import { CheckHRC } from "./pages/tools/CheckHRC";
import { ChartsPage } from "./pages/ChartsPage";
export function Routes() {
return (
@ -98,6 +99,10 @@ export function Routes() {
<ExportData />
</Route>
<Route path="/charts">
<Route path={'/'}><ChartsPage /></Route>
</Route>
<Route path="*">
<Redirect to="/" />
</Route>

@ -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,
@ -101,13 +101,17 @@ export function getRelatedTransactions(params: any[]) {
>;
}
export function getTransactionCountLast14Days() {
return transport("getTransactionCountLast14Days", []) as Promise<any[]>;
export function getTransactionCountLast14Days(limit = 14) {
return transport("getTransactionCountLast14Days", [limit]) as Promise<any[]>;
}
export function getWalletsCountLast14Days() {
return transport("getWalletsCountLast14Days", []) as Promise<any[]>;
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[]) {

@ -41,7 +41,7 @@ export function ToolsButton() {
history.push("/tools/approvals");
}}
>
Token Approvals
Token Approvals
</Anchor>
<Anchor
style={{ textDecoration: "underline" }}
@ -52,6 +52,18 @@ export function ToolsButton() {
>
Check HRC
</Anchor>
<Anchor
style={{ textDecoration: "underline" }}
onClick={(e) => {
setIsOpen(false);
history.push("/charts");
}}
>
<Box direction={'row'} align={'center'} gap={'4px'}>
Charts & Stats
<Text size={'xsmall'} color={'successText'}>(new)</Text>
</Box>
</Anchor>
</Box>
}
style={{

@ -0,0 +1,50 @@
import React, {useEffect, useState} from 'react'
import {getMetricsByType} from "../../api/client";
import {MetricsDailyItem, MetricsType} from "../../types";
import {
enrichResponse,
} from './utils'
import {DailyChartPage} from "./DailyChartPage";
export const ActiveAddresses = () => {
const [items, setItems] = useState<MetricsDailyItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [loadingError, setLoadingError] = useState('')
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true)
const data = await getMetricsByType(MetricsType.walletsCount, 0, 2000)
setItems(enrichResponse(data))
} catch (e) {
console.error('Error on loading metrics:', e)
setLoadingError('Loading error')
} finally {
setIsLoading(false);
}
}
loadData()
}, [])
const dailyPageProps = {
title: 'Harmony Daily Active Addresses',
description: 'The Active Address chart shows the daily number of unique addresses that were active on the network as a sender or receiver',
unitLabel: 'addresses',
items,
isLoading,
loadingError,
chart: {
yAxisLabel: 'Active Harmony Addresses',
tooltipLabel: 'Active Harmony Addresses'
},
renderMaxValue: (value: string, date: string) => {
return `Highest number of ${value} addresses on ${date}`
},
renderMinValue: (value: string, date: string) => {
return `Lowest number of ${value} addresses on ${date}`
}
}
return <DailyChartPage {...dailyPageProps} />
}

@ -0,0 +1,49 @@
import React, {useEffect, useState} from 'react'
import {getMetricsByType} from "../../api/client";
import {MetricsDailyItem, MetricsType} from "../../types";
import {
enrichResponse,
} from './utils'
import {DailyChartPage} from "./DailyChartPage";
export const AverageBlockSize = () => {
const [items, setItems] = useState<MetricsDailyItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [loadingError, setLoadingError] = useState('')
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true)
const data = await getMetricsByType(MetricsType.blockSize, 0, 2000)
setItems(enrichResponse(data))
} catch (e) {
console.error('Error on loading metrics:', e)
setLoadingError('Loading error')
} finally {
setIsLoading(false);
}
}
loadData()
}, [])
const dailyPageProps = {
title: 'Harmony Average Block Size',
unitLabel: 'blocksize',
items,
isLoading,
loadingError,
chart: {
yAxisLabel: 'Block Size in Bytes',
tooltipLabel: 'Block Size (Bytes)'
},
renderMaxValue: (value: string, date: string) => {
return `Largest size of ${value} bytes on ${date}`
},
renderMinValue: (value: string, date: string) => {
return `Smallest size of ${value} bytes on ${date}`
}
}
return <DailyChartPage {...dailyPageProps} />
}

@ -0,0 +1,49 @@
import React, {useEffect, useState} from 'react'
import {getMetricsByType} from "../../api/client";
import {MetricsDailyItem, MetricsType} from "../../types";
import {
enrichResponse,
} from './utils'
import {DailyChartPage} from "./DailyChartPage";
export const AverageFee = () => {
const [items, setItems] = useState<MetricsDailyItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [loadingError, setLoadingError] = useState('')
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true)
const data = await getMetricsByType(MetricsType.averageFee, 0, 2000)
setItems(enrichResponse(data))
} catch (e) {
console.error('Error on loading metrics:', e)
setLoadingError('Loading error')
} finally {
setIsLoading(false);
}
}
loadData()
}, [])
const dailyPageProps = {
title: 'Harmony Daily Average Fee',
unitLabel: 'fee',
items,
isLoading,
loadingError,
chart: {
yAxisLabel: 'Average Transaction Fee (ONE)',
tooltipLabel: 'Average tx fee (ONE)'
},
renderMaxValue: (value: string, date: string) => {
return `Highest average transaction fee of ${value} ONE on ${date}`
},
renderMinValue: (value: string, date: string) => {
return `Lowest average transaction fee of ${value} ONE on ${date}`
}
}
return <DailyChartPage {...dailyPageProps} />
}

@ -0,0 +1,62 @@
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={'4px'}
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={'4px'}
direction={'row'}
gap={'8px'}
round={'8px'}
background={'backgroundBack'}
border={{ size: '1px' }}
>
{ChartOptions.map(option => <Option
key={option}
value={option}
isActive={activeOption === option}
onSelect={(option) => !disabled ? onSelect(option) : undefined}
/>)}
</Box>
}

@ -0,0 +1,186 @@
import React, {useEffect, useState} from 'react'
import {Box, Heading, Spinner, Text} from "grommet";
import {BaseContainer, BasePage} from "../../components/ui";
import {useThemeMode} from "../../hooks/themeSwitcherHook";
import {Line as LineChartJs} from "react-chartjs-2";
import {MetricsDailyItem} from "../../types";
import {
getDetailedChartOptions,
getChartData,
getLimitByFilterOption,
downloadMetricsCSV
} from './utils'
import {ChartFilter, ChartOption} from "./ChartFilter";
import styled from "styled-components";
import dayjs from "dayjs";
import {Alert, Info} from "grommet-icons";
import {Link} from "react-router-dom";
import {useMediaQuery} from "react-responsive";
const ChartModalContainer = styled(Box)`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
const TextLink = styled(Text)`
cursor: pointer;
text-decoration: underline;
`
const LoadingErrorModal = () => {
return <ChartModalContainer
justify={'center'}
gap={'16px'}
pad={'16px'}
align={'center'}
background={'warningBackground'}
round={'8px'}
border={{ size: '1px' }}
style={{ zIndex: 1 }}
>
<Alert size={'medium'} />
<Text>Error on loading data</Text>
<Text size={'small'}>Please try again later</Text>
</ChartModalContainer>
}
const formatValue = (value: string) => Intl.NumberFormat('en-US').format(+value)
const formatDate = (date: string) => dayjs(date).format('dddd, MMMM D, YYYY')
export interface DailyChartPageProps {
title: string
description?: string
unitLabel: string
items: MetricsDailyItem[]
isLoading: boolean
loadingError?: string
chart: {
yAxisLabel: string,
tooltipLabel: string
}
renderMaxValue: (value: string, date: string) => string
renderMinValue: (value: string, date: string) => string
}
export const DailyChartPage = (props: DailyChartPageProps) => {
const themeMode = useThemeMode();
const { isLoading, loadingError } = props
const [items, setItems] = useState<MetricsDailyItem[]>([]);
const [cache, setCache] = useState<MetricsDailyItem[]>([])
const [filterOption, setFilterOption] = useState(ChartOption.year)
const [minValue, setMinValue] = useState<MetricsDailyItem>({value: '0', date: ''})
const [maxValue, setMaxValue] = useState<MetricsDailyItem>({value: '0', date: ''})
const applyFilter = (cachedData: MetricsDailyItem[]) => {
const limit = getLimitByFilterOption(filterOption)
const data = cachedData.slice(-limit)
const sortedData = [...data].sort((a, b) => +a.value - +b.value)
setItems(cachedData.slice(-limit))
setMinValue(sortedData[0])
setMaxValue(sortedData[sortedData.length - 1])
}
useEffect(() => {
if(cache.length > 0) {
applyFilter(cache)
}
}, [filterOption])
useEffect(() => {
setCache(props.items)
if(props.items.length > 0) {
applyFilter(props.items)
}
}, [props.items])
const isMobile = useMediaQuery({ query: '(max-width: 868px)' })
const chartOptions = getDetailedChartOptions(themeMode, items, { isMobile, yAxisLabel: props.chart.yAxisLabel })
const chartData = getChartData(themeMode, items, props.chart.tooltipLabel)
return <BaseContainer pad={{ horizontal: "0" }}>
<Heading size="20px" margin={{ bottom: "medium", top: "0" }} style={{ maxWidth: 'unset' }}>
<Box direction={'row'} justify={'between'} align={'center'}>
<Box direction={"row"} gap={'8px'} align={'center'}>
<Box>
<Link to={'/charts'}><TextLink color={'brand'}>Charts</TextLink></Link>
</Box>
<Box>
<Text style={{ opacity: 0.4 }}>/</Text>
</Box>
<Box>
{props.title}
</Box>
</Box>
{!isMobile && props.description &&
<Box align={'end'}>
<Text size={'xsmall'} weight={'normal'}>{props.description}</Text>
</Box>
}
</Box>
</Heading>
<BasePage pad={"small"}>
<Box direction={'row'} justify={'between'} flex={'grow'} wrap={true}>
<Box direction={'row'} gap={'8px'} justify={'center'} align={'center'} style={{ flexGrow: 2 }}>
<Info size={'small'} />
<Text size={'small'}>
{props.renderMaxValue(formatValue(maxValue.value), formatDate(maxValue.date))}
</Text>
</Box>
<Box direction={'row'} gap={'8px'} justify={'center'} align={'center'} style={{ flexGrow: 2 }}>
<Info size={'small'} />
<Text size={'small'}>
{props.renderMinValue(formatValue(minValue.value), formatDate(minValue.date))}
</Text>
</Box>
</Box>
</BasePage>
<BasePage pad={"small"} style={{overflow: 'inherit', marginTop: '16px'}}>
<Box align={'end'}>
<ChartFilter
disabled={isLoading || !!loadingError}
activeOption={filterOption}
onSelect={(o) => setFilterOption(o)}
/>
</Box>
<Box
width={'100%'}
height="260px"
direction={"row"}
align={'center'}
justify={'center'}
margin={{ top: '8px' }}
style={{ position: 'relative', pointerEvents: isLoading || loadingError ? 'none': 'unset' }}
>
{isLoading && <ChartModalContainer justify={'center'} gap={'16px'} align={'center'}>
<Spinner size={'medium'} />
<Text>Loading Data</Text>
</ChartModalContainer>}
{!isLoading && loadingError && <LoadingErrorModal />}
<Box
height={'inherit'}
width={'inherit'}
style={{ filter: isLoading || loadingError ? 'blur(4px)': 'unset' }}
>
{
// @ts-ignore
<LineChartJs options={chartOptions} data={chartData} />
}
</Box>
</Box>
<Box margin={{ top: '32px' }} align={'end'}>
<Box direction={'row'} gap={'4px'}>
<Text>Download</Text>
<TextLink
color={'brand'}
onClick={() => downloadMetricsCSV(`${props.unitLabel}_metrics.csv`, { items: [...cache].reverse() })}>
CSV Data
</TextLink>
</Box>
</Box>
</BasePage>
</BaseContainer>
}

@ -0,0 +1,49 @@
import React, {useEffect, useState} from 'react'
import {getMetricsByType} from "../../api/client";
import {MetricsDailyItem, MetricsType} from "../../types";
import {
enrichResponse,
} from './utils'
import {DailyChartPage} from "./DailyChartPage";
export const DailyTransactions = () => {
const [items, setItems] = useState<MetricsDailyItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [loadingError, setLoadingError] = useState('')
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true)
const data = await getMetricsByType(MetricsType.transactionsCount, 0, 2000)
setItems(enrichResponse(data))
} catch (e) {
console.error('Error on loading metrics:', e)
setLoadingError('Loading error')
} finally {
setIsLoading(false);
}
}
loadData()
}, [])
const dailyPageProps = {
title: 'Harmony Daily Transactions',
unitLabel: 'transactions',
items,
isLoading,
loadingError,
chart: {
yAxisLabel: 'Transactions per day',
tooltipLabel: 'Daily transactions'
},
renderMaxValue: (value: string, date: string) => {
return `Highest number of ${value} transactions on ${date}`
},
renderMinValue: (value: string, date: string) => {
return `Lowest number of ${value} transactions on ${date}`
}
}
return <DailyChartPage {...dailyPageProps} />
}

@ -0,0 +1,104 @@
import React from "react";
import {Box, Heading, Text} from "grommet";
import { BasePage, BaseContainer } from "src/components/ui";
import {useHistory, useLocation} from "react-router-dom";
import {ActiveAddresses} from "./ActiveAddresses";
import {DailyTransactions} from "./DailyTransactions";
import {AverageFee} from "./AverageFee";
import {AverageBlockSize} from "./AverageBlockSize";
import styled from "styled-components";
import {useThemeMode} from "../../hooks/themeSwitcherHook";
enum ChartType {
tx = 'tx',
addresses = 'addresses',
fee = 'fee',
blockSize = 'blocksize'
}
const PreviewContainer = styled(Box)`
flex: 0 0 calc(25% - 16px);
margin-left: 8px;
margin-right: 8px;
@media (max-width: 1024px) {
flex: 0 0 calc(50% - 16px);
margin-bottom: 0.75rem;
}
@media (max-width: 768px) {
flex: 0 0 calc(100% - 16px);
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
`
const PreviewCard = (props: { type: ChartType, title: string }) => {
const themeMode = useThemeMode();
const {type, title} = props
const history = useHistory();
const onClick = () => history.push(`charts/${type}`)
const imgProps = {
alt: type,
style: { opacity: themeMode === 'dark' ? '0.4' : 1 }
}
return <PreviewContainer
border={{ size: '1px' }}
round={'8px'}
overflow={'hidden'}
onClick={onClick}
style={{ filter: 'hue-rotate(360deg)' }}
>
<Box pad={'8px'} background={'backgroundDropdownItem'}>
<Text size={'small'} color={'brand'}>{title}</Text>
</Box>
<Box style={{ filter: 'grayscale(0.8)' }} pad={'8px'} border={{ side: 'top' }}>
{type === ChartType.tx && <img src={require("./thumbnails/daily_txs.png").default} {...imgProps} />}
{type === ChartType.addresses && <img src={require("./thumbnails/daily_addresses.png").default} {...imgProps} />}
{type === ChartType.fee && <img src={require("./thumbnails/daily_fee.png").default} {...imgProps} />}
{type === ChartType.blockSize && <img src={require("./thumbnails/daily_blocksize.png").default} {...imgProps} />}
</Box>
</PreviewContainer>
}
export function ChartsPage() {
const location = useLocation();
const [, ,route] = location.pathname.split('/')
if(route === ChartType.tx) {
return <DailyTransactions />
} else if(route === ChartType.addresses) {
return <ActiveAddresses />
} else if(route === ChartType.fee) {
return <AverageFee />
} else if(route === ChartType.blockSize) {
return <AverageBlockSize />
}
return (
<BaseContainer pad={{ horizontal: "0" }}>
<Heading size="small" margin={{ bottom: "medium", top: "0" }}>
<Box direction={"row"}>Harmony One Charts</Box>
</Heading>
<BasePage pad={'0'} style={{overflow: 'inherit'}}>
<Box border={{ side: 'bottom' }} pad={"small"}>
<Text weight={'bold'}>Blockchain Data</Text>
</Box>
<Box
wrap
direction={'row'}
pad={"small"}
justify={'center'}
align={'center'}
>
<PreviewCard type={ChartType.tx} title={'Daily Transactions Chart'} />
<PreviewCard type={ChartType.addresses} title={'Daily Active Addresses'} />
<PreviewCard type={ChartType.fee} title={'Average Transaction Fee'} />
<PreviewCard type={ChartType.blockSize} title={'Average Block Size'} />
</Box>
</BasePage>
</BaseContainer>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

@ -0,0 +1,212 @@
import {palette} from "../../theme";
import dayjs from "dayjs";
import dayOfYear from "dayjs/plugin/dayOfYear";
import {MetricsDailyItem} from "../../types";
import {ChartOption} 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 {
isMobile?: boolean
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,
maintainAspectRatio: false, // To properly adjust height on page resize
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: 2000,
maxRotation: 0,
minRotation: 0,
align: 'end',
callback: function(value: string, index: any, ticks: any) {
const item = points[index]
const nextItem = points[index + 1]
if(nextItem) {
// Show January and July
if (dayjs(item.timestamp).month() !== dayjs(nextItem.timestamp).month() &&
([6, 0].includes(dayjs(nextItem.timestamp).month()))) {
return dayjs(nextItem.timestamp).format("MMM 'YY")
}
// too many labels for mobile screen
if(!config.isMobile) {
// show each month
if(ticks.length <= 365) {
if (dayjs(item.timestamp).month() !== dayjs(nextItem.timestamp).month()) {
return dayjs(nextItem.timestamp).format("MMM 'YY")
}
}
}
if(ticks.length <= 30) {
// show each day
if (dayjs(item.timestamp).day() !== dayjs(nextItem.timestamp).day() &&
[1].includes(dayjs(item.timestamp).day())) {
return dayjs(nextItem.timestamp).format("D MMM")
}
}
}
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 2000
default:
return 1000
}
}
const downloadBlob = (content: any, filename: string) => {
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
// Create a link to download it
const pom = document.createElement('a');
pom.href = url;
pom.setAttribute('download', filename);
pom.click();
}
export const downloadMetricsCSV = (filename: string, params: { items: MetricsDailyItem[] }) => {
const { items } = params
const mappedTxs = items.map(item => ({ date: item.date, value: item.value }))
const header = mappedTxs.filter((_, index) => index === 0)
.map(item => Object.keys(item))
const body = mappedTxs
.map((item) => Object.values(item))
const csv = [...header, ...body]
.map(row =>
row
.map(String) // convert every value to String
.map((v: any) => v.replaceAll('"', '""')) // escape double colons
// .map((v: any) => `"${v}"`) // quote it
.join(', ') // comma-separated
).join('\r\n'); // rows starting on new lines
downloadBlob(csv, filename)
}

@ -45,3 +45,15 @@ 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',
blockSize = 'block_size'
}

Loading…
Cancel
Save