diff --git a/src/constants/common.ts b/src/constants/common.ts
index 5c6bdac40..747facdd2 100644
--- a/src/constants/common.ts
+++ b/src/constants/common.ts
@@ -127,8 +127,19 @@ export enum ListPageParams {
export const ChartColor = {
areaColor: '#31EEB3',
- colors: ['#5824FB', '#31EEB3', '#484E4E'],
- moreColors: ['#5824FB', '#66CC99', '#FBB04C', '#525860'],
+ colors: ['#553AF3', '#333333', '#00CC9B'],
+ moreColors: [
+ '#553AF3',
+ '#333333',
+ '#00CC9B',
+ '#FF5656',
+ '#24C0F0',
+ '#BCCC00',
+ '#4661A6',
+ '#EDAF36',
+ '#E63ECB',
+ '#69E63E',
+ ],
totalSupplyColors: ['#5824FB', '#31EEB3', '#484E4E'],
daoColors: ['#5824FB', '#31EEB3', '#484E4E'],
secondaryIssuanceColors: ['#484E4E', '#5824FB', '#31EEB3'],
diff --git a/src/locales/en.json b/src/locales/en.json
index ef3acd0b9..8c0adf0a7 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -312,7 +312,16 @@
"country": "Country/Region",
"node_country_distribution": "Nodes distribution by Country/Region",
"top_50_holders": "Top 50 Holders",
- "rounded": "Rounded"
+ "rounded": "Rounded",
+ "24h": "24h",
+ "day_to_one_week": "1d-1w",
+ "one_week_to_one_month": "1w-1m",
+ "one_month_to_three_months": "1m-3m",
+ "three_months_to_six_months": "3m-6m",
+ "six_months_to_one_year": "6m-1y",
+ "one_year_to_three_years": "1y-3y",
+ "over_three_years": "> 3y",
+ "ckb_hodl_wave": "CKB HODL Wave"
},
"home": {
"height": "Height",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 17a50f8db..302c06195 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -311,7 +311,16 @@
"country": "国家(地区)",
"node_country_distribution": "节点国家(地区)分布图",
"top_50_holders": "前 50 持有地址",
- "rounded": "约等于"
+ "rounded": "约等于",
+ "24h": "24小时",
+ "day_to_one_week": "1天-1周",
+ "one_week_to_one_month": "1周-1月",
+ "one_month_to_three_months": "1月-3月",
+ "three_months_to_six_months": "3月-6月",
+ "six_months_to_one_year": "6月-1年",
+ "one_year_to_three_years": "1年-3年",
+ "over_three_years": "> 3年",
+ "ckb_hodl_wave": "CKB持有波动图"
},
"home": {
"height": "高度",
diff --git a/src/pages/StatisticsChart/activities/CkbHodlWave.tsx b/src/pages/StatisticsChart/activities/CkbHodlWave.tsx
new file mode 100644
index 000000000..9706aa7d2
--- /dev/null
+++ b/src/pages/StatisticsChart/activities/CkbHodlWave.tsx
@@ -0,0 +1,326 @@
+import { useTranslation } from 'react-i18next'
+import dayjs from 'dayjs'
+import { SupportedLng, useCurrentLanguage } from '../../../utils/i18n'
+import {
+ DATA_ZOOM_CONFIG,
+ assertIsArray,
+ assertSerialsItem,
+ assertSerialsDataIsStringArrayOf9,
+ handleAxis,
+} from '../../../utils/chart'
+import { tooltipColor, tooltipWidth, SeriesItem, SmartChartPage } from '../common'
+import { ChartItem, explorerService } from '../../../services/ExplorerService'
+import { ChartColorConfig } from '../../../constants/common'
+
+const widthSpan = (value: string, currentLanguage: SupportedLng) =>
+ tooltipWidth(value, currentLanguage === 'en' ? 125 : 80)
+
+const useTooltip = () => {
+ const { t } = useTranslation()
+ const currentLanguage = useCurrentLanguage()
+ return ({
+ seriesName,
+ data,
+ color,
+ }: SeriesItem & {
+ data: [string, string, string, string, string, string, string, string, string]
+ }): string => {
+ if (seriesName === t('statistic.24h')) {
+ return `
${tooltipColor(color)}${widthSpan(t('statistic.24h'), currentLanguage)} ${handleAxis(
+ data[1],
+ 2,
+ )}%
`
+ }
+ if (seriesName === t('statistic.day_to_one_week')) {
+ return `${tooltipColor(color)}${widthSpan(t('statistic.day_to_one_week'), currentLanguage)} ${handleAxis(
+ data[2],
+ 2,
+ )}%
`
+ }
+ if (seriesName === t('statistic.one_week_to_one_month')) {
+ return `${tooltipColor(color)}${widthSpan(
+ t('statistic.one_week_to_one_month'),
+ currentLanguage,
+ )} ${handleAxis(data[3], 2)}%
`
+ }
+ if (seriesName === t('statistic.one_month_to_three_months')) {
+ return `${tooltipColor(color)}${widthSpan(
+ t('statistic.one_month_to_three_months'),
+ currentLanguage,
+ )} ${handleAxis(data[4], 2)}%
`
+ }
+ if (seriesName === t('statistic.three_months_to_six_months')) {
+ return `${tooltipColor(color)}${widthSpan(
+ t('statistic.three_months_to_six_months'),
+ currentLanguage,
+ )} ${handleAxis(data[5], 2)}%
`
+ }
+ if (seriesName === t('statistic.six_months_to_one_year')) {
+ return `${tooltipColor(color)}${widthSpan(
+ t('statistic.six_months_to_one_year'),
+ currentLanguage,
+ )} ${handleAxis(data[6], 2)}%
`
+ }
+ if (seriesName === t('statistic.one_year_to_three_years')) {
+ return `${tooltipColor(color)}${widthSpan(
+ t('statistic.one_year_to_three_years'),
+ currentLanguage,
+ )} ${handleAxis(data[7], 2)}%
`
+ }
+ if (seriesName === t('statistic.over_three_years')) {
+ return `${tooltipColor(color)}${widthSpan(t('statistic.over_three_years'), currentLanguage)} ${handleAxis(
+ data[8],
+ 2,
+ )}%
`
+ }
+
+ return ''
+ }
+}
+
+const useOption = (
+ statisticCkbHodlWaves: ChartItem.CkbHodlWave[],
+ chartColor: ChartColorConfig,
+ isMobile: boolean,
+ isThumbnail = false,
+): echarts.EChartOption => {
+ const { t } = useTranslation()
+ const currentLanguage = useCurrentLanguage()
+ const gridThumbnail = {
+ left: '4%',
+ right: '10%',
+ top: '8%',
+ bottom: '6%',
+ containLabel: true,
+ }
+ const grid = {
+ left: '3%',
+ right: '3%',
+ top: '8%',
+ bottom: '5%',
+ containLabel: true,
+ }
+ const parseTooltip = useTooltip()
+ return {
+ color: chartColor.moreColors,
+ tooltip: !isThumbnail
+ ? {
+ trigger: 'axis',
+ formatter: dataList => {
+ assertIsArray(dataList)
+ let result = `${tooltipColor('#333333')}${widthSpan(t('statistic.date'), currentLanguage)}
+ ${dataList[0].data[0]}
`
+ dataList.forEach(data => {
+ assertSerialsItem(data)
+ assertSerialsDataIsStringArrayOf9(data)
+ result += parseTooltip(data)
+ })
+ return result
+ },
+ }
+ : undefined,
+ legend: {
+ data: isThumbnail
+ ? []
+ : [
+ {
+ name: t('statistic.24h'),
+ },
+ {
+ name: t('statistic.day_to_one_week'),
+ },
+ {
+ name: t('statistic.one_week_to_one_month'),
+ },
+ {
+ name: t('statistic.one_month_to_three_months'),
+ },
+ {
+ name: t('statistic.three_months_to_six_months'),
+ },
+ {
+ name: t('statistic.six_months_to_one_year'),
+ },
+ {
+ name: t('statistic.one_year_to_three_years'),
+ },
+ {
+ name: t('statistic.over_three_years'),
+ },
+ ],
+ selected: {
+ [t('statistic.24h')]: true,
+ [t('statistic.day_to_one_week')]: true,
+ [t('statistic.one_week_to_one_month')]: true,
+ [t('statistic.one_month_to_three_months')]: true,
+ [t('statistic.three_months_to_six_months')]: true,
+ [t('statistic.six_months_to_one_year')]: true,
+ [t('statistic.one_year_to_three_years')]: true,
+ [t('statistic.over_three_years')]: true,
+ },
+ },
+ grid: isThumbnail ? gridThumbnail : grid,
+ dataZoom: isThumbnail ? [] : DATA_ZOOM_CONFIG,
+ xAxis: [
+ {
+ name: isMobile || isThumbnail ? '' : t('statistic.date'),
+ nameLocation: 'middle',
+ nameGap: 30,
+ type: 'category',
+ boundaryGap: false,
+ },
+ ],
+ yAxis: [
+ {
+ position: 'left',
+ type: 'value',
+ max: 100,
+ axisLine: {
+ lineStyle: {
+ color: chartColor.colors[0],
+ },
+ },
+ axisLabel: {
+ formatter: (value: string) => `${value}%`,
+ },
+ },
+ ],
+ series: [
+ {
+ name: t('statistic.24h'),
+ type: 'line',
+ yAxisIndex: 0,
+ symbol: isThumbnail ? 'none' : 'circle',
+ symbolSize: 3,
+ stack: 'sum',
+ areaStyle: {
+ color: chartColor.colors[0],
+ },
+ lineStyle: {
+ width: 4,
+ },
+ },
+ {
+ name: t('statistic.day_to_one_week'),
+ type: 'line',
+ stack: 'sum',
+ yAxisIndex: 0,
+ symbol: isThumbnail ? 'none' : 'circle',
+ symbolSize: 3,
+ areaStyle: {
+ color: chartColor.colors[1],
+ },
+ },
+ {
+ name: t('statistic.one_week_to_one_month'),
+ type: 'line',
+ yAxisIndex: 0,
+ symbol: isThumbnail ? 'none' : 'circle',
+ stack: 'sum',
+ symbolSize: 3,
+ areaStyle: {
+ color: chartColor.colors[2],
+ },
+ },
+ {
+ name: t('statistic.one_month_to_three_months'),
+ type: 'line',
+ yAxisIndex: 0,
+ symbol: isThumbnail ? 'none' : 'circle',
+ stack: 'sum',
+ symbolSize: 3,
+ areaStyle: {
+ color: chartColor.colors[3],
+ },
+ },
+ {
+ name: t('statistic.three_months_to_six_months'),
+ type: 'line',
+ yAxisIndex: 0,
+ symbol: isThumbnail ? 'none' : 'circle',
+ stack: 'sum',
+ symbolSize: 3,
+ areaStyle: {
+ color: chartColor.colors[4],
+ },
+ },
+ {
+ name: t('statistic.six_months_to_one_year'),
+ type: 'line',
+ yAxisIndex: 0,
+ symbol: isThumbnail ? 'none' : 'circle',
+ stack: 'sum',
+ symbolSize: 3,
+ areaStyle: {
+ color: chartColor.colors[5],
+ },
+ },
+ {
+ name: t('statistic.one_year_to_three_years'),
+ type: 'line',
+ yAxisIndex: 0,
+ symbol: isThumbnail ? 'none' : 'circle',
+ stack: 'sum',
+ symbolSize: 3,
+ areaStyle: {
+ color: chartColor.colors[6],
+ },
+ },
+ {
+ name: t('statistic.over_three_years'),
+ type: 'line',
+ yAxisIndex: 0,
+ symbol: isThumbnail ? 'none' : 'circle',
+ stack: 'sum',
+ symbolSize: 3,
+ areaStyle: {
+ color: chartColor.colors[7],
+ },
+ },
+ ],
+ dataset: {
+ source: statisticCkbHodlWaves.map(data => [
+ dayjs(Number(data.createdAtUnixtimestamp) * 1000).format('MM/DD/YYYY'),
+ ((data.ckbHodlWave.latestDay / data.ckbHodlWave.totalSupply) * 100).toFixed(2),
+ ((data.ckbHodlWave.dayToOneWeek / data.ckbHodlWave.totalSupply) * 100).toFixed(2),
+ ((data.ckbHodlWave.oneWeekToOneMonth / data.ckbHodlWave.totalSupply) * 100).toFixed(2),
+ ((data.ckbHodlWave.oneMonthToThreeMonths / data.ckbHodlWave.totalSupply) * 100).toFixed(2),
+ ((data.ckbHodlWave.threeMonthsToSixMonths / data.ckbHodlWave.totalSupply) * 100).toFixed(2),
+ ((data.ckbHodlWave.sixMonthsToOneYear / data.ckbHodlWave.totalSupply) * 100).toFixed(2),
+ ((data.ckbHodlWave.oneYearToThreeYears / data.ckbHodlWave.totalSupply) * 100).toFixed(2),
+ ((data.ckbHodlWave.overThreeYears / data.ckbHodlWave.totalSupply) * 100).toFixed(2),
+ ]),
+ dimensions: ['timestamp', '24h', '1d-1w', '1w-3m', '1m-3m', '3m-6m', '6m-1y', '1y-3y', '> 3y'],
+ },
+ }
+}
+
+const toCSV = (statisticCkbHodlWaves: ChartItem.CkbHodlWave[]) =>
+ statisticCkbHodlWaves
+ ? statisticCkbHodlWaves.map(data => [
+ data.createdAtUnixtimestamp,
+ data.ckbHodlWave.latestDay,
+ data.ckbHodlWave.dayToOneWeek,
+ data.ckbHodlWave.oneWeekToOneMonth,
+ data.ckbHodlWave.threeMonthsToSixMonths,
+ data.ckbHodlWave.sixMonthsToOneYear,
+ data.ckbHodlWave.oneYearToThreeYears,
+ data.ckbHodlWave.overThreeYears,
+ ])
+ : []
+
+export const CkbHodlWaveChart = ({ isThumbnail = false }: { isThumbnail?: boolean }) => {
+ const [t] = useTranslation()
+ return (
+
+ )
+}
+
+export default CkbHodlWaveChart
diff --git a/src/pages/StatisticsChart/index.tsx b/src/pages/StatisticsChart/index.tsx
index fa82449d6..a3e808240 100644
--- a/src/pages/StatisticsChart/index.tsx
+++ b/src/pages/StatisticsChart/index.tsx
@@ -2,11 +2,13 @@ import { ReactNode } from 'react'
import 'default-passive-events'
import { useTranslation } from 'react-i18next'
import Content from '../../components/Content'
+import { isMainnet } from '../../utils/chain'
import { DifficultyHashRateChart } from './mining/DifficultyHashRate'
import { DifficultyUncleRateEpochChart } from './mining/DifficultyUncleRateEpoch'
import { TransactionCountChart } from './activities/TransactionCount'
import { AddressCountChart } from './activities/AddressCount'
import { CellCountChart } from './activities/CellCount'
+import { CkbHodlWaveChart } from './activities/CkbHodlWave'
import { TotalDaoDepositChart } from './nervosDao/TotalDaoDeposit'
import { ChartsPanel, ChartCardPanel, ChartsTitle, ChartsContent } from './styled'
import { AddressBalanceRankChart } from './activities/AddressBalanceRank'
@@ -191,6 +193,60 @@ const useChartsData = () => {
},
],
},
+ {
+ category: t('statistic.category_activities'),
+ charts: [
+ {
+ title: `${t('statistic.transaction_count')}`,
+ chart: ,
+ path: '/charts/transaction-count',
+ },
+ {
+ title: `${t('statistic.address_count')}`,
+ chart: ,
+ path: '/charts/address-count',
+ description: t('statistic.address_count_description_description'),
+ },
+ {
+ title: t('statistic.cell_count'),
+ chart: ,
+ path: '/charts/cell-count',
+ },
+ ...(isMainnet()
+ ? [
+ {
+ title: t('statistic.ckb_hodl_wave'),
+ chart: ,
+ path: '/charts/ckb-hodl-wave',
+ },
+ ]
+ : []),
+ {
+ title: `${t('statistic.balance_ranking')}`,
+ chart: ,
+ path: '/charts/address-balance-rank',
+ description: t('statistic.balance_ranking_description'),
+ },
+ {
+ title: `${t('statistic.balance_distribution')}`,
+ chart: ,
+ path: '/charts/balance-distribution',
+ description: t('statistic.balance_distribution_description'),
+ },
+ {
+ title: `${t('statistic.tx_fee_history')}`,
+ chart: ,
+ path: '/charts/tx-fee-history',
+ description: t('statistic.tx_fee_description'),
+ },
+ {
+ title: `${t('statistic.contract_resource_distributed')}`,
+ chart: ,
+ path: '/charts/contract-resource-distributed',
+ description: t('statistic.contract_resource_distributed_description'),
+ },
+ ],
+ },
{
category: t('blockchain.nervos_dao'),
charts: [
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index f96973563..db6f58a24 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -53,6 +53,7 @@ const CellCountChart = lazy(() => import('../pages/StatisticsChart/activities/Ce
const ContractResourceDistributedChart = lazy(
() => import('../pages/StatisticsChart/activities/ContractResourceDistributed'),
)
+const CkbHodlWaveChart = lazy(() => import('../pages/StatisticsChart/activities/CkbHodlWave'))
const AddressBalanceRankChart = lazy(() => import('../pages/StatisticsChart/activities/AddressBalanceRank'))
const BalanceDistributionChart = lazy(() => import('../pages/StatisticsChart/activities/BalanceDistribution'))
const TxFeeHistoryChart = lazy(() => import('../pages/StatisticsChart/activities/TxFeeHistory'))
@@ -225,6 +226,10 @@ const routes: RouteProps[] = [
path: '/charts/cell-count',
component: CellCountChart,
},
+ {
+ path: '/charts/ckb-hodl-wave',
+ component: CkbHodlWaveChart,
+ },
{
path: '/charts/address-balance-rank',
component: AddressBalanceRankChart,
diff --git a/src/services/ExplorerService/fetcher.ts b/src/services/ExplorerService/fetcher.ts
index 2ebc1171c..5e03f11b9 100644
--- a/src/services/ExplorerService/fetcher.ts
+++ b/src/services/ExplorerService/fetcher.ts
@@ -599,6 +599,11 @@ export const apiFetcher = {
`/statistics/address_balance_ranking`,
).then(res => res.addressBalanceRanking),
+ fetchStatisticCkbHodlWave: () =>
+ v1GetUnwrappedList(`/daily_statistics/ckb_hodl_wave`).then(items =>
+ items.filter(item => item.ckbHodlWave != null),
+ ),
+
fetchStatisticBalanceDistribution: () =>
v1GetUnwrapped<{ addressBalanceDistribution: string[][] }>(`/distribution_data/address_balance_distribution`).then(
({ addressBalanceDistribution }) => {
diff --git a/src/services/ExplorerService/types.ts b/src/services/ExplorerService/types.ts
index 1a9e2c68b..3a3f5af86 100644
--- a/src/services/ExplorerService/types.ts
+++ b/src/services/ExplorerService/types.ts
@@ -113,6 +113,23 @@ export namespace ChartItem {
balance: string
}
+ export interface CkbHodlWave {
+ ckbHodlWave: CkbHodlWaveDetail
+ createdAtUnixtimestamp: string
+ }
+
+ export interface CkbHodlWaveDetail {
+ overThreeYears: number
+ oneYearToThreeYears: number
+ sixMonthsToOneYear: number
+ threeMonthsToSixMonths: number
+ oneMonthToThreeMonths: number
+ oneWeekToOneMonth: number
+ dayToOneWeek: number
+ latestDay: number
+ totalSupply: number
+ }
+
export interface BalanceDistribution {
balance: string
addresses: string
diff --git a/src/utils/chart.ts b/src/utils/chart.ts
index 5a75c1b0d..1f0f51305 100644
--- a/src/utils/chart.ts
+++ b/src/utils/chart.ts
@@ -202,3 +202,13 @@ export const assertSerialsDataIsStringArrayOf4: (
throw new Error('invalid SeriesItem length of 4')
}
}
+
+export const assertSerialsDataIsStringArrayOf9: (
+ value: EChartOption.Tooltip.Format,
+) => asserts value is { data: [string, string, string, string, string, string, string, string, string] } = (
+ value: EChartOption.Tooltip.Format,
+) => {
+ if (!Array.isArray(value.data) || value.data.length !== 9 || !value.data.every(item => typeof item === 'string')) {
+ throw new Error('invalid SeriesItem length of 9')
+ }
+}