commit
c8531ac3ba
@ -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> |
||||
); |
||||
} |
After Width: | Height: | Size: 247 KiB |
After Width: | Height: | Size: 273 KiB |
After Width: | Height: | Size: 208 KiB |
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) |
||||
} |
Loading…
Reference in new issue