parent
641575154d
commit
41cda2b33e
@ -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> |
||||
} |
@ -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 |
||||
} |
||||
} |
Loading…
Reference in new issue