diff --git a/packages/app-staking/src/Query/Validator.tsx b/packages/app-staking/src/Query/Validator.tsx index 085e614a0312..a8147d45637c 100644 --- a/packages/app-staking/src/Query/Validator.tsx +++ b/packages/app-staking/src/Query/Validator.tsx @@ -3,11 +3,12 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { I18nProps } from '@polkadot/react-components/types'; -import { BlockNumber, Exposure, SessionIndex } from '@polkadot/types/interfaces'; +import { Balance, BlockNumber, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces'; import BN from 'bn.js'; import React, { useContext, useEffect, useState } from 'react'; import { Chart, Columar, Column } from '@polkadot/react-components'; +import { toShortAddress } from '@polkadot/react-components/util'; import { ApiContext, withCalls } from '@polkadot/react-api'; import { getHistoric } from '@polkadot/react-api/util'; import { formatBalance, formatNumber } from '@polkadot/util'; @@ -24,9 +25,21 @@ interface Props extends I18nProps { type LineData = (BN | number)[][]; +interface SplitEntry { + colors: string[]; + label: string; + value: number; +} + +type SplitData = SplitEntry[]; + // assuming 4 hrs sessions, we grab results for 10 days const SESSIONS = 10 * (24 / 4); +const COLORS_MINE = ['#ff8c00']; +const COLORS_OTHER = ['#acacac']; +const COLORS_BLOCKS = [undefined, '#acacac']; + function getIndexRange (currentIndex: SessionIndex): BN[] { const range: BN[] = []; let thisIndex: BN = currentIndex; @@ -40,12 +53,46 @@ function getIndexRange (currentIndex: SessionIndex): BN[] { return range.reverse(); } +function extractStake (values: [BN, Hash, Exposure][], divisor: BN): [string[], LineData] { + return [ + values.map(([bn]): string => formatNumber(bn)), + [ + values.map(([,, { total }]): BN => + total.unwrap().div(divisor)) + // exposures.map(({ own }): BN => + // own.unwrap().div(divisor)), + // exposures.map(({ others }): BN => + // others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor)) + ] + ]; +} + +function extractSplit (values: [BN, Hash, Exposure][], validatorId: string): SplitData | null { + const last = values[values.length - 1][2]; + const total = last.total.unwrap(); + + if (total.eqn(0)) { + return null; + } + + return [{ accountId: validatorId, isOwn: true, value: last.own.unwrap() }] + .concat(last.others.map(({ who, value }): { accountId: string; isOwn: boolean; value: Balance } => ({ + accountId: who.toString(), isOwn: false, value: value.unwrap() + }))) + .sort((a, b): number => b.value.cmp(a.value)) + .map(({ accountId, isOwn, value }): SplitEntry => ({ + colors: isOwn ? COLORS_MINE : COLORS_OTHER, + label: toShortAddress(accountId), + value: value.muln(10000).div(total).toNumber() / 100 + })); +} + function Validator ({ blockCounts, className, currentIndex, startNumber, t, validatorId }: Props): React.ReactElement { const { api } = useContext(ApiContext); const [blocksLabels, setBlocksLabels] = useState([]); - const [blocksChart, setBlocksChart] = useState(); - const [stakeLabels, setStakeLabels] = useState([]); - const [stakeChart, setStateChart] = useState(); + const [blocksChart, setBlocksChart] = useState(null); + const [splitChart, setSplitInfo] = useState(null); + const [{ stakeChart, stakeLabels }, setStakeInfo] = useState<{ stakeChart: LineData | null; stakeLabels: string[]}>({ stakeChart: null, stakeLabels: [] }); useEffect((): void => { api.isReady.then(async (): Promise => { @@ -55,16 +102,11 @@ function Validator ({ blockCounts, className, currentIndex, startNumber, t, vali max: SESSIONS, startNumber }); + const [stakeLabels, stakeChart] = extractStake(values, divisor); + const splitChart = extractSplit(values, validatorId); - setStakeLabels(values.map(([bn]): string => formatNumber(bn))); - setStateChart([ - values.map(([,, { total }]): BN => - total.unwrap().div(divisor)) - // exposures.map(({ own }): BN => - // own.unwrap().div(divisor)), - // exposures.map(({ others }): BN => - // others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor)) - ]); + setStakeInfo({ stakeChart, stakeLabels }); + setSplitInfo(splitChart); }); }, []); @@ -94,29 +136,43 @@ function Validator ({ blockCounts, className, currentIndex, startNumber, t, vali return ( - + {blocksChart && ( - +
+

{t('blocks per session')}

+ +
)}
- - {stakeChart && ( - + + {(stakeChart || splitChart) && ( + <> + {stakeChart && ( +
+

{t('elected stake')}

+ +
+ )} + {splitChart && ( +
+

{t('staker percentages')}

+ +
+ )} + )}
diff --git a/packages/app-staking/src/index.tsx b/packages/app-staking/src/index.tsx index 9bb8b16acb3d..83c8a3480090 100644 --- a/packages/app-staking/src/index.tsx +++ b/packages/app-staking/src/index.tsx @@ -110,6 +110,14 @@ export default withMulti( .staking--queryInput { margin-bottom: 1.5rem; } + + .staking--Chart h1 { + margin-bottom: 0.5rem; + } + + .staking--Chart+.staking--Chart { + margin-top: 1.5rem; + } `, translate, withCalls( diff --git a/packages/react-components/src/Chart/HorizBar.tsx b/packages/react-components/src/Chart/HorizBar.tsx index 813c8dc2cba2..98f00febfac8 100644 --- a/packages/react-components/src/Chart/HorizBar.tsx +++ b/packages/react-components/src/Chart/HorizBar.tsx @@ -8,7 +8,7 @@ import BN from 'bn.js'; import React, { useEffect, useState } from 'react'; import ChartJs from 'chart.js'; import { HorizontalBar } from 'react-chartjs-2'; -import { bnToBn } from '@polkadot/util'; +import { bnToBn, isNumber } from '@polkadot/util'; interface Value { colors: string[]; @@ -18,7 +18,9 @@ interface Value { interface Props extends BareProps { aspectRatio?: number; + max?: number; values: Value[]; + withColors?: boolean; } interface State { @@ -39,13 +41,13 @@ interface Config { const alphaColor = (hexColor: string): string => ChartJs.helpers.color(hexColor).alpha(0.65).rgbString(); -function calculateOptions (aspectRatio: number, values: Value[], jsonValues: string): State { +function calculateOptions (aspectRatio: number, values: Value[], jsonValues: string, max: number): State { const chartData = values.reduce((data, { colors: [normalColor = '#00f', hoverColor], label, value }): Config => { const dataset = data.datasets[0]; dataset.backgroundColor.push(alphaColor(normalColor)); dataset.hoverBackgroundColor.push(alphaColor(hoverColor || normalColor)); - dataset.data.push(bnToBn(value).toNumber()); + dataset.data.push(isNumber(value) ? value : bnToBn(value).toNumber()); data.labels.push(label); return data; @@ -71,7 +73,7 @@ function calculateOptions (aspectRatio: number, values: Value[], jsonValues: str xAxes: [{ ticks: { beginAtZero: true, - max: 100 + max } }] } @@ -80,14 +82,14 @@ function calculateOptions (aspectRatio: number, values: Value[], jsonValues: str }; } -export default function ChartHorizBar ({ aspectRatio = 4, className, style, values }: Props): React.ReactElement | null { +export default function ChartHorizBar ({ aspectRatio = 4, className, max = 100, style, values }: Props): React.ReactElement | null { const [{ chartData, chartOptions, jsonValues }, setState] = useState({}); useEffect((): void => { const newJsonValues = JSON.stringify(values); if (newJsonValues !== jsonValues) { - setState(calculateOptions(aspectRatio, values, newJsonValues)); + setState(calculateOptions(aspectRatio, values, newJsonValues, max)); } }, [values]);