diff --git a/src/core/constants.js b/src/core/constants.js index c8b1dfb2..f02400d2 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -12,3 +12,35 @@ export const WS_URL = IS_DEV ? 'ws://localhost:8080' : 'wss://nano.community' // 3 Million Nano (3e36) export const REP_MAX_WEIGHT = BigNumber(3).shiftedBy(36) + +export const base_ranges = [ + '_1000000', + '_100000', + '_10000', + '_1000', + '_100', + '_10', + '_1', + '_01', + '_001', + '_0001', + '_00001', + '_000001', + '_000001_below' +] + +export const base_range_labels = { + _1000000: '>1M', + _100000: '100k to 1M', + _10000: '10k to 100k', + _1000: '1k to 10k', + _100: '100 to 1k', + _10: '10 to 100', + _1: '1 to 10', + _01: '0.1 to 1', + _001: '0.01 to 0.1', + _0001: '0.001 to 0.01', + _00001: '0.0001 to 0.001', + _000001: '0.00001 to 0.0001', + _000001_below: '<0.00001' +} diff --git a/src/views/components/ledger-chart-addresses-with-balance/index.js b/src/views/components/ledger-chart-addresses-with-balance/index.js new file mode 100644 index 00000000..a4d47e62 --- /dev/null +++ b/src/views/components/ledger-chart-addresses-with-balance/index.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux' +import { createSelector } from 'reselect' + +import { nanodb_actions } from '@core/nanodb/actions' + +import LedgerChartAddressesWithBalance from './ledger-chart-addresses-with-balance' + +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 +)(LedgerChartAddressesWithBalance) diff --git a/src/views/components/ledger-chart-addresses-with-balance/ledger-chart-addresses-with-balance.js b/src/views/components/ledger-chart-addresses-with-balance/ledger-chart-addresses-with-balance.js new file mode 100644 index 00000000..60c55040 --- /dev/null +++ b/src/views/components/ledger-chart-addresses-with-balance/ledger-chart-addresses-with-balance.js @@ -0,0 +1,174 @@ +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' +import { base_range_labels } from '@core/constants' + +echarts.use([LineChart, TooltipComponent, GridComponent, CanvasRenderer]) + +export default function LedgerChartAddressesWithBalance({ + data, + isLoading, + price_history, + get_price_history, + range +}) { + useEffect(() => { + if (price_history.length === 0) { + get_price_history() + } + }, []) + + const handle_download_csv = () => { + const download_data = [] + data[`${range}_account_count`].forEach((item) => { + download_data.push({ + range: base_range_labels[range], + date: item[0], + account_count: item[1] + }) + }) + download_csv({ + headers: ['Range', 'Date', 'Account Count'], + data: download_data, + file_name: `nano-community-ledger-daily-addresses-with-balance-${base_range_labels[ + range + ].toLowerCase()}` + }) + } + + const handle_download_json = () => { + const download_data = { + title: `Nano Community Ledger Daily Addresses With Balance - ${base_range_labels[range]}`, + data: data[`${range}_account_count`].map((item) => ({ + date: item[0], + account_count: item[1] + })) + } + download_json({ + data: download_data, + file_name: `nano-community-ledger-daily-addresses-with-balance-${base_range_labels[ + range + ].toLowerCase()}` + }) + } + + const series_data_usd_price = useMemo(() => { + const data = [] + price_history.forEach((item) => { + data.push([item.timestamp_utc, item.price]) + }) + return data + }, [price_history]) + + const series_data = useMemo(() => { + return { + name: base_range_labels[range], + type: 'line', + lineStyle: { + width: 2 + }, + showSymbol: false, + data: data[`${range}_account_count`].map((item) => [item[0], item[1]]) + } + }, [data, range]) + + const option = { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + label: { + backgroundColor: '#6a7985' + } + } + }, + legend: { + bottom: 0 + }, + xAxis: [ + { + type: 'time', + boundaryGap: false + } + ], + yAxis: [ + { + type: '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' + } + } + ] + } + + return ( + <> + +
+
+ Description +
+
+

+ The number of unique addresses holding {base_range_labels[range]}{' '} + Nano. +

+
+ {!isLoading && ( +
+ + +
+ )} +
+ + ) +} + +LedgerChartAddressesWithBalance.propTypes = { + data: PropTypes.object.isRequired, + isLoading: PropTypes.bool.isRequired, + price_history: PropTypes.array.isRequired, + get_price_history: PropTypes.func.isRequired, + range: PropTypes.string +} diff --git a/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js b/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js index b0aa3e06..ddd99076 100644 --- a/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js +++ b/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js @@ -13,6 +13,7 @@ import { CanvasRenderer } from 'echarts/renderers' import Button from '@mui/material/Button' import { download_csv, download_json } from '@core/utils' +import { base_ranges, base_range_labels } from '@core/constants' echarts.use([ TooltipComponent, @@ -26,49 +27,33 @@ echarts.use([ const color_map = { // high - _1000000_count: '#ffd700', // Gold - _100000_count: '#f9c74f', // Maize Crayola - _10000_count: '#f4a261', // Sandy Brown - _1000_count: '#e63946', // Red + _1000000: '#ffd700', // Gold + _100000: '#f9c74f', // Maize Crayola + _10000: '#f4a261', // Sandy Brown + _1000: '#e63946', // Red // medium - _100_count: '#0077b6', // Star Command Blue - _10_count: '#023e8a', // Royal Blue Dark - _1_count: '#0096c7', // Pacific Blue + _100: '#0077b6', // Star Command Blue + _10: '#023e8a', // Royal Blue Dark + _1: '#0096c7', // Pacific Blue // low - _01_count: '#6DB65B', // Apple Green - _001_count: '#57A55A', // Medium Sea Green - _0001_count: '#3D8B57', // Jungle Green - _00001_count: '#2C6E49', // Bottle Green + _01: '#6DB65B', // Apple Green + _001: '#57A55A', // Medium Sea Green + _0001: '#3D8B57', // Jungle Green + _00001: '#2C6E49', // Bottle Green // micro - _000001_count: '#911eb4', // Purple - _000001_below_count: '#dcbeff' // Lavender Blush + _000001: '#911eb4', // Purple + _000001_below: '#dcbeff' // Lavender Blush } export default function LedgerChartAmounts({ data, isLoading }) { - const ranges = { - _1000000_count: '>1M', - _100000_count: '100k to 1M', - _10000_count: '10k to 100k', - _1000_count: '1k to 10k', - _100_count: '100 to 1k', - _10_count: '10 to 100', - _1_count: '1 to 10', - _01_count: '0.1 to 1', - _001_count: '0.01 to 0.1', - _0001_count: '0.001 to 0.01', - _00001_count: '0.0001 to 0.001', - _000001_count: '0.00001 to 0.0001', - _000001_below_count: '<0.00001' - } - - const series_data = Object.entries(ranges).map(([key, value], index) => { + const series_data = base_ranges.map((key) => { const color = color_map[key] return { - name: value, + name: base_range_labels[key], type: 'line', stack: 'total', color, @@ -82,7 +67,7 @@ export default function LedgerChartAmounts({ data, isLoading }) { emphasis: { focus: 'series' }, - data: data[key].map((item) => [item[0], item[1]]) + data: data[`${key}_count`].map((item) => [item[0], item[1]]) } }) @@ -122,10 +107,10 @@ export default function LedgerChartAmounts({ data, isLoading }) { const handle_download_csv = () => { const download_data = [] - Object.entries(ranges).forEach(([key, value]) => { - data[key].forEach((item) => { + base_ranges.forEach((key) => { + data[`${key}_count`].forEach((item) => { download_data.push({ - range: value, + range: base_range_labels[key], date: item[0], count: item[1] }) @@ -145,11 +130,13 @@ export default function LedgerChartAmounts({ data, isLoading }) { data: {} } - Object.entries(ranges).forEach(([key, value]) => { - download_data.data[value] = data[key].map((item) => ({ - date: item[0], - count: item[1] - })) + base_ranges.forEach((key) => { + download_data.data[base_range_labels[key]] = data[`${key}_count`].map( + (item) => ({ + date: item[0], + count: item[1] + }) + ) }) download_json({ diff --git a/src/views/components/ledger-chart-distribution/ledger-chart-distribution.js b/src/views/components/ledger-chart-distribution/ledger-chart-distribution.js index 38999b87..fe123a16 100644 --- a/src/views/components/ledger-chart-distribution/ledger-chart-distribution.js +++ b/src/views/components/ledger-chart-distribution/ledger-chart-distribution.js @@ -7,31 +7,32 @@ import { LineChart } from 'echarts/charts' import { TooltipComponent, GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' +import { base_ranges, base_range_labels } from '@core/constants' 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 + _1000000: '#ffd700', // Gold + _100000: '#f9c74f', // Maize Crayola + _10000: '#f4a261', // Sandy Brown + _1000: '#e63946', // Red // medium - _100_relative_supply: '#0077b6', // Star Command Blue - _10_relative_supply: '#023e8a', // Royal Blue Dark - _1_relative_supply: '#0096c7', // Pacific Blue + _100: '#0077b6', // Star Command Blue + _10: '#023e8a', // Royal Blue Dark + _1: '#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 + _01: '#6DB65B', // Apple Green + _001: '#57A55A', // Medium Sea Green + _0001: '#3D8B57', // Jungle Green + _00001: '#2C6E49', // Bottle Green // micro - _000001_relative_supply: '#911eb4', // Purple - _000001_below_relative_supply: '#dcbeff' // Lavender Blush + _000001: '#911eb4', // Purple + _000001_below: '#dcbeff' // Lavender Blush } export default function LedgerChartDistribution({ @@ -54,27 +55,11 @@ export default function LedgerChartDistribution({ 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 series_data = base_ranges.map((key) => { const color = color_map[key] return { - name: value, + name: base_range_labels[key], type: 'line', stack: 'total', color, @@ -89,7 +74,7 @@ export default function LedgerChartDistribution({ emphasis: { focus: 'series' }, - data: data[key].map((item) => [item[0], item[1]]) + data: data[`${key}_relative_supply`].map((item) => [item[0], item[1]]) } }) @@ -147,10 +132,10 @@ export default function LedgerChartDistribution({ const handle_download_csv = () => { const download_data = [] - Object.entries(ranges).forEach(([key, value]) => { - data[key].forEach((item) => { + base_ranges.forEach((key) => { + data[`${key}_relative_supply`].forEach((item) => { download_data.push({ - range: value, + range: base_range_labels[key], date: item[0], relative_supply: item[1] }) @@ -170,8 +155,10 @@ export default function LedgerChartDistribution({ data: {} } - Object.entries(ranges).forEach(([key, value]) => { - download_data.data[value] = data[key].map((item) => ({ + base_ranges.forEach((key) => { + download_data.data[base_range_labels[key]] = data[ + `${key}_relative_supply` + ].map((item) => ({ date: item[0], relative_supply: item[1] })) diff --git a/src/views/pages/ledger/index.js b/src/views/pages/ledger/index.js index 56c9c7f8..ed448a6c 100644 --- a/src/views/pages/ledger/index.js +++ b/src/views/pages/ledger/index.js @@ -3,6 +3,7 @@ import { createSelector } from 'reselect' import BigNumber from 'bignumber.js' import { getLedger, ledgerActions } from '@core/ledger' +import { base_ranges } from '@core/constants' import LedgerPage from './ledger' @@ -11,21 +12,6 @@ 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', @@ -44,7 +30,8 @@ const mapStateToProps = createSelector(getLedger, (ledger) => { // TODO open_volume // TODO receive_volume - ...base_ranges.map((r) => `${r}_count`) + ...base_ranges.map((r) => `${r}_count`), + ...base_ranges.map((r) => `${r}_account_count`) ] for (let i = 0; i < metrics.length; i++) { diff --git a/src/views/pages/ledger/ledger.js b/src/views/pages/ledger/ledger.js index bca4cd43..c677e278 100644 --- a/src/views/pages/ledger/ledger.js +++ b/src/views/pages/ledger/ledger.js @@ -16,8 +16,10 @@ 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 LedgerChartAddressesWithBalance from '@components/ledger-chart-addresses-with-balance' import Seo from '@components/seo' import Menu from '@components/menu' +import { base_ranges, base_range_labels } from '@core/constants' import './ledger.styl' @@ -36,6 +38,48 @@ export default function LedgerPage({ load, data, isLoading }) { const [menu_open, set_menu_open] = useState(false) const filter_input_ref = useRef(null) + const categories = { + Addresses: { + 'Address Counts': ( + + ) + }, + Blocks: { + 'Block Counts': + }, + Transactions: { + 'Value Transferred': ( + + ), + 'Transfer Volume': ( + + ), + 'Transfer Amounts': ( + + ) + }, + Distribution: { + 'Address Supply Distribution': ( + + ), + 'Address Balances (Nano)': { + ...base_ranges.reduce( + (acc, range) => ({ + ...acc, + [`Addresses with Balance ${base_range_labels[range]}`]: ( + + ) + }), + {} + ) + } + } + } + useEffect(() => { load() }, []) @@ -78,15 +122,21 @@ export default function LedgerPage({ load, data, isLoading }) { useEffect(() => { if (filter_text.length > 0) { - // Uncollapse all categories when filtering - set_open_categories( - Object.keys(filtered_categories).reduce( - (acc, key) => ({ ...acc, [key]: true }), - {} - ) - ) + // Uncollapse all categories and their children when filtering + const all_open_categories = Object.keys(categories).reduce((acc, key) => { + const category = categories[key] + if (typeof category === 'object') { + const sub_keys = Object.keys(category) + sub_keys.forEach((sub_key) => { + acc[sub_key] = true + }) + } + acc[key] = true + return acc + }, {}) + set_open_categories(all_open_categories) } - }, [filtered_categories]) + }, [filtered_categories, categories, filter_text]) useEffect(() => { if (menu_open) { @@ -135,57 +185,48 @@ export default function LedgerPage({ load, data, isLoading }) { set_open_categories((prev) => ({ ...prev, [category]: !prev[category] })) } - const categories = { - Addresses: { - 'Address Counts': ( - - ) - }, - Blocks: { - 'Block Counts': - }, - Transactions: { - 'Value Transferred': ( - - ), - 'Transfer Volume': ( - - ), - 'Transfer Amounts': ( - - ) - }, - Distribution: { - 'Address Supply Distribution': ( - - ) - } + const count_children = (children) => { + return Object.values(children).reduce((acc, child) => { + if (React.isValidElement(child)) { + return acc + 1 + } else { + return acc + count_children(child) + } + }, 0) } - const render_category = (category, charts, depth) => { - const is_open = open_categories[category] || false + const render_category = (parent_category, children, depth, path = []) => { + const is_open = open_categories[parent_category] || false + const total_children_count = count_children(children) return ( <> toggle_category(category)} + onClick={() => toggle_category(parent_category)} className={`ledger__category ledger__category-depth-${depth}`}> - - + + - {Object.entries(charts).map(([key, value]) => { + {Object.entries(children).map(([key, value]) => { + console.log({ + path, + parent_category, + key + }) + const current_path = [...path, parent_category] const is_selected = - selected_chart[0] === category && selected_chart[1] === key + selected_chart.join(' > ') === + [...current_path, key].join(' > ') const item_class = is_selected ? 'ledger__chart--selected' : '' if (React.isValidElement(value)) { return ( set_selected_chart([category, key])} + onClick={() => set_selected_chart([...current_path, key])} className={`ledger__chart ledger__category-depth-${ depth + 1 } ${item_class}`}> @@ -193,7 +234,7 @@ export default function LedgerPage({ load, data, isLoading }) { ) } else { - return render_category(key, value, depth + 1) + return render_category(key, value, depth + 1, current_path) } })} @@ -255,8 +296,8 @@ export default function LedgerPage({ load, data, isLoading }) { {Object.entries( filter_text.length > 0 ? filtered_categories : categories - ).map(([category, charts]) => - render_category(category, charts, 0) + ).map(([parent_category, children]) => + render_category(parent_category, children, 0) )} diff --git a/src/views/pages/ledger/ledger.styl b/src/views/pages/ledger/ledger.styl index d87a296c..46527cd2 100644 --- a/src/views/pages/ledger/ledger.styl +++ b/src/views/pages/ledger/ledger.styl @@ -53,10 +53,20 @@ background hsl(210, 100%, 60%) .ledger__category-depth-2 - padding-left 49px + padding-left 72px + &:before + content: "" + display block + position: absolute + z-index 1 + left 44px + height 100% + width 1px + opacity 1 + background #E5EAF2 .ledger__category-depth-3 - padding-left 62px + padding-left 90px .ledger__footer margin 32px auto