diff --git a/src/views/components/ledger-chart-distribution/index.js b/src/views/components/ledger-chart-distribution/index.js new file mode 100644 index 00000000..1dec8b25 --- /dev/null +++ b/src/views/components/ledger-chart-distribution/index.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { createSelector } from 'reselect' + +import { nanodb_actions } from '@core/nanodb/actions' + +import LedgerChartDistribution from './ledger-chart-distribution' + +const map_state_to_props = createSelector( + (state) => state.getIn(['nanodb', 'price_history']), + (price_history) => ({ + price_history + }) +) + +const map_dispatch_to_props = { + get_price_history: nanodb_actions.get_price_history +} + +export default connect(map_state_to_props, map_dispatch_to_props)(LedgerChartDistribution) diff --git a/src/views/components/ledger-chart-distribution/ledger-chart-distribution.js b/src/views/components/ledger-chart-distribution/ledger-chart-distribution.js new file mode 100644 index 00000000..2412e2f7 --- /dev/null +++ b/src/views/components/ledger-chart-distribution/ledger-chart-distribution.js @@ -0,0 +1,223 @@ +import React, { useEffect, useMemo } from 'react' +import PropTypes from 'prop-types' +import Button from '@mui/material/Button' +import ReactEChartsCore from 'echarts-for-react/lib/core' +import * as echarts from 'echarts/core' +import { LineChart } from 'echarts/charts' +import { TooltipComponent, GridComponent } from 'echarts/components' +import { CanvasRenderer } from 'echarts/renderers' + +import { download_csv, download_json } from '@core/utils' + +echarts.use([LineChart, TooltipComponent, GridComponent, CanvasRenderer]) + +const color_map = { + // high + _1000000_relative_supply: '#ffd700', // Gold + _100000_relative_supply: '#f9c74f', // Maize Crayola + _10000_relative_supply: '#f4a261', // Sandy Brown + _1000_relative_supply: '#e63946', // Red + + // medium + _100_relative_supply: '#0077b6', // Star Command Blue + _10_relative_supply: '#023e8a', // Royal Blue Dark + _1_relative_supply: '#0096c7', // Pacific Blue + + // low + _01_relative_supply: '#6DB65B', // Apple Green + _001_relative_supply: '#57A55A', // Medium Sea Green + _0001_relative_supply: '#3D8B57', // Jungle Green + _00001_relative_supply: '#2C6E49', // Bottle Green + + // micro + _000001_relative_supply: '#911eb4', // Purple + _000001_below_relative_supply: '#dcbeff' // Lavender Blush +} + +export default function LedgerChartDistribution({ data, isLoading, price_history, get_price_history }) { + useEffect(() => { + if (price_history.length === 0) { + get_price_history() + } + }, []) + + const series_data_usd_price = useMemo(() => { + const data = [] + price_history.forEach((item) => { + data.push([item.timestamp_utc, item.price]) + }) + return data + }, [price_history]) + + const ranges = { + _1000000_relative_supply: '>1M', + _100000_relative_supply: '100K to 1M', + _10000_relative_supply: '10K to 100K', + _1000_relative_supply: '1K to 10K', + _100_relative_supply: '100 to 1K', + _10_relative_supply: '10 to 100', + _1_relative_supply: '1 to 10', + _01_relative_supply: '0.1 to 1', + _001_relative_supply: '0.01 to 0.1', + _0001_relative_supply: '0.001 to 0.01', + _00001_relative_supply: '0.0001 to 0.001', + _000001_relative_supply: '0.00001 to 0.0001', + _000001_below_relative_supply: '<0.00001' + } + + const series_data = Object.entries(ranges).map(([key, value], index) => { + const color = color_map[key] + + return { + name: value, + type: 'line', + stack: 'total', + color, + lineStyle: { + width: 0 + }, + showSymbol: false, + areaStyle: { + color, + opacity: 0.6 + }, + emphasis: { + focus: 'series' + }, + data: data[key].map((item) => [item[0], item[1]]) + } + }) + + const option = { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + label: { + backgroundColor: '#6a7985' + } + } + }, + legend: { + bottom: 0 + }, + xAxis: [ + { + type: 'time', + boundaryGap: false + } + ], + yAxis: [ + { + type: 'value', + min: 0, + max: 100, + axisLabel: { + formatter: '{value} %' + } + }, + { + type: 'log', + position: 'right', + axisLabel: { + formatter: '{value} USD' + } + } + ], + series: [ + ...series_data, + { + name: 'USD Price', + type: 'line', + yAxisIndex: 1, + data: series_data_usd_price, + showSymbol: false, + lineStyle: { + width: 2, + color: '#333333' + } + } + ] + } + + const handle_download_csv = () => { + const download_data = [] + Object.entries(ranges).forEach(([key, value]) => { + data[key].forEach((item) => { + download_data.push({ + range: value, + date: item[0], + relative_supply: item[1] + }) + }) + }) + + download_csv({ + headers: ['Range', 'Date', 'Relative Supply'], + data: download_data, + file_name: 'nano-community-ledger-daily-relative-balance-distribution' + }) + } + + const handle_download_json = () => { + const download_data = { + title: 'Nano Community Ledger Daily Relative Balance Distribution', + data: {} + } + + Object.entries(ranges).forEach(([key, value]) => { + download_data.data[value] = data[key].map((item) => ({ + date: item[0], + relative_supply: item[1] + })) + }) + download_json({ + data: download_data, + file_name: 'nano-community-ledger-daily-relative-balance-distribution' + }) + } + + return ( + <> + +
+
+
+ Description +
+
+ The relative distribution of the supply held by addresses within specific balance ranges. +
+ {!isLoading && ( +
+ + +
+ )} +
+
+ + ) +} + +LedgerChartDistribution.propTypes = { + data: PropTypes.object.isRequired, + isLoading: PropTypes.bool.isRequired, + price_history: PropTypes.array.isRequired, + get_price_history: PropTypes.func.isRequired +} diff --git a/src/views/pages/ledger/index.js b/src/views/pages/ledger/index.js index 579f0ecf..56c9c7f8 100644 --- a/src/views/pages/ledger/index.js +++ b/src/views/pages/ledger/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux' import { createSelector } from 'reselect' +import BigNumber from 'bignumber.js' import { getLedger, ledgerActions } from '@core/ledger' @@ -10,6 +11,22 @@ const mapStateToProps = createSelector(getLedger, (ledger) => { const data = { reused_addresses: [] } + const base_ranges = [ + '_1000000', + '_100000', + '_10000', + '_1000', + '_100', + '_10', + '_1', + '_01', + '_001', + '_0001', + '_00001', + '_000001', + '_000001_below' + ] + const metrics = [ 'active_addresses', @@ -27,19 +44,7 @@ const mapStateToProps = createSelector(getLedger, (ledger) => { // TODO open_volume // TODO receive_volume - '_1000000_count', - '_100000_count', - '_10000_count', - '_1000_count', - '_100_count', - '_10_count', - '_1_count', - '_01_count', - '_001_count', - '_0001_count', - '_00001_count', - '_000001_count', - '_000001_below_count' + ...base_ranges.map((r) => `${r}_count`) ] for (let i = 0; i < metrics.length; i++) { @@ -47,6 +52,10 @@ const mapStateToProps = createSelector(getLedger, (ledger) => { data[metric] = [] } + base_ranges.forEach((r) => { + data[`${r}_relative_supply`] = [] + }) + daily.forEach((d) => { const timestamp = parseInt(d.timestamp, 10) * 1000 for (let i = 0; i < metrics.length; i++) { @@ -55,6 +64,20 @@ const mapStateToProps = createSelector(getLedger, (ledger) => { } data.reused_addresses.push([timestamp, d.active_addresses - d.open_count]) + + // calculate relative supply distribution + const total_supply = base_ranges.reduce((acc, r) => { + return acc + BigInt(d[`${r}_total_balance`]) + }, BigInt(0)) + + base_ranges.forEach((r) => { + const total_balance = BigInt(d[`${r}_total_balance`]) + const relative_supply = BigNumber(total_balance) + .div(total_supply) + .times(100) + .toFixed(5) + data[`${r}_relative_supply`].push([timestamp, relative_supply]) + }) }) return { data, isLoading: ledger.get('isLoading') } diff --git a/src/views/pages/ledger/ledger.js b/src/views/pages/ledger/ledger.js index c0e7faf2..ec1e8d01 100644 --- a/src/views/pages/ledger/ledger.js +++ b/src/views/pages/ledger/ledger.js @@ -8,6 +8,7 @@ import LedgerChartAddresses from '@components/ledger-chart-addresses' import LedgerChartVolume from '@components/ledger-chart-volume' import LedgerChartAmounts from '@components/ledger-chart-amounts' import LedgerChartUSDTransferred from '@components/ledger-chart-usd-transferred' +import LedgerChartDistribution from '@components/ledger-chart-distribution' import Seo from '@components/seo' import Menu from '@components/menu' @@ -73,6 +74,7 @@ export default function LedgerPage({ load, data, isLoading }) { + @@ -89,6 +91,9 @@ export default function LedgerPage({ load, data, isLoading }) { + + +