From 3570967bb5f4bb554abfc477ffc1b3a0a3ab3a90 Mon Sep 17 00:00:00 2001 From: tyleroooo Date: Fri, 28 Jun 2024 11:30:02 -0400 Subject: [PATCH] feat: compact, consistent asset amount rendering in orderbook + trades (#744) --- src/hooks/Orderbook/useDrawOrderbook.ts | 72 +++++++++++++----------- src/lib/consistentAssetSize.ts | 74 +++++++++++++++++++++++++ src/views/tables/LiveTrades.tsx | 20 +++++-- 3 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 src/lib/consistentAssetSize.ts diff --git a/src/hooks/Orderbook/useDrawOrderbook.ts b/src/hooks/Orderbook/useDrawOrderbook.ts index ba8855065..4e27e15f5 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -1,6 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import BigNumber from 'bignumber.js'; import { shallowEqual } from 'react-redux'; import type { PerpetualMarketOrderbookLevel } from '@/constants/abacus'; @@ -15,10 +14,13 @@ import { import { useAppThemeAndColorModeContext } from '@/hooks/useAppThemeAndColorMode'; +import { OutputType, formatNumberOutput } from '@/components/Output'; + import { useAppSelector } from '@/state/appTypes'; +import { getSelectedLocale } from '@/state/localizationSelectors'; import { getCurrentMarketConfig, getCurrentMarketOrderbookMap } from '@/state/perpetualsSelectors'; -import { MustBigNumber } from '@/lib/numbers'; +import { getConsistentAssetSizeString } from '@/lib/consistentAssetSize'; import { getHistogramXValues, getRektFromIdx, @@ -26,6 +28,7 @@ import { getYForElements, } from '@/lib/orderbookHelpers'; import { generateFadedColorVariant } from '@/lib/styles'; +import { orEmptyObj } from '@/lib/typeUtils'; import { useLocaleSeparators } from '../useLocaleSeparators'; @@ -58,11 +61,13 @@ export const useDrawOrderbook = ({ const canvasRef = useRef(null); const canvas = canvasRef.current; const currentOrderbookMap = useAppSelector(getCurrentMarketOrderbookMap, shallowEqual); - const { decimal: LOCALE_DECIMAL_SEPARATOR, group: LOCALE_GROUP_SEPARATOR } = - useLocaleSeparators(); + const { decimal: decimalSeparator, group: groupSeparator } = useLocaleSeparators(); + const selectedLocale = useAppSelector(getSelectedLocale); - const { stepSizeDecimals = TOKEN_DECIMALS, tickSizeDecimals = SMALL_USD_DECIMALS } = - useAppSelector(getCurrentMarketConfig, shallowEqual) ?? {}; + const marketConfig = orEmptyObj(useAppSelector(getCurrentMarketConfig)); + const stepSizeDecimals = marketConfig.stepSizeDecimals ?? TOKEN_DECIMALS; + const tickSizeDecimals = marketConfig.tickSizeDecimals ?? SMALL_USD_DECIMALS; + const stepSize = marketConfig.stepSize ?? 10 ** (-1 * TOKEN_DECIMALS); const prevData = useRef(data); const theme = useAppThemeAndColorModeContext(); @@ -217,43 +222,41 @@ export const useDrawOrderbook = ({ } } - const format = { - decimalSeparator: LOCALE_DECIMAL_SEPARATOR, - ...{ - groupSeparator: LOCALE_GROUP_SEPARATOR, - groupSize: 3, - secondaryGroupSize: 0, - fractionGroupSeparator: ' ', - fractionGroupSize: 0, - }, - }; - // Price text if (price != null) { ctx.fillStyle = textColor; ctx.fillText( - MustBigNumber(price).toFormat( - tickSizeDecimals ?? SMALL_USD_DECIMALS, - BigNumber.ROUND_HALF_UP, - { - ...format, - } - ), + formatNumberOutput(price, OutputType.Number, { + decimalSeparator, + groupSeparator, + fractionDigits: tickSizeDecimals, + }), getXByColumn({ canvasWidth, colIdx: 0 }) - ORDERBOOK_ROW_PADDING_RIGHT, y ); } - const decimalPlaces = displayUnit === 'asset' ? stepSizeDecimals ?? TOKEN_DECIMALS : 0; + const getSizeInFiatString = (sizeToRender: number) => + formatNumberOutput(sizeToRender, OutputType.Number, { + decimalSeparator, + groupSeparator, + fractionDigits: 0, + }); // Size text const displaySize = displayUnit === 'asset' ? size : sizeCost; if (displaySize != null) { ctx.fillStyle = updatedTextColor ?? textColor; ctx.fillText( - MustBigNumber(displaySize).toFormat(decimalPlaces, BigNumber.ROUND_HALF_UP, { - ...format, - }), + displayUnit === 'asset' + ? getConsistentAssetSizeString(displaySize, { + decimalSeparator, + groupSeparator, + selectedLocale, + stepSize, + stepSizeDecimals, + }) + : getSizeInFiatString(displaySize), getXByColumn({ canvasWidth, colIdx: 1 }) - ORDERBOOK_ROW_PADDING_RIGHT, y ); @@ -264,9 +267,15 @@ export const useDrawOrderbook = ({ if (displayDepth != null) { ctx.fillStyle = textColor; ctx.fillText( - MustBigNumber(displayDepth).toFormat(decimalPlaces, BigNumber.ROUND_HALF_UP, { - ...format, - }), + displayUnit === 'asset' + ? getConsistentAssetSizeString(displayDepth, { + decimalSeparator, + groupSeparator, + selectedLocale, + stepSize, + stepSizeDecimals, + }) + : getSizeInFiatString(displayDepth), getXByColumn({ canvasWidth, colIdx: 2 }) - ORDERBOOK_ROW_PADDING_RIGHT, y ); @@ -381,6 +390,7 @@ export const useDrawOrderbook = ({ theme, currentOrderbookMap, displayUnit, + canvas, ]); return { canvasRef }; diff --git a/src/lib/consistentAssetSize.ts b/src/lib/consistentAssetSize.ts new file mode 100644 index 000000000..145c59eb0 --- /dev/null +++ b/src/lib/consistentAssetSize.ts @@ -0,0 +1,74 @@ +import { mapValues, range, zipObject } from 'lodash'; + +import { SUPPORTED_LOCALE_STRING_LABELS, SupportedLocales } from '@/constants/localization'; + +import { formatNumberOutput, OutputType } from '@/components/Output'; + +// for each locale, an array of the correct compact number suffix to use for 10^{index} +// e.g. for "en" we have ['', '', '', 'k', 'k', 'k', 'm', 'm', 'm', 'b', 'b', 'b', 't', 't', 't'] +const supportedLocaleToCompactSuffixByPowerOfTen = mapValues( + SUPPORTED_LOCALE_STRING_LABELS, + (name, lang) => + range(15) + .map((a) => + Intl.NumberFormat(lang, { + style: 'decimal', + notation: 'compact', + maximumSignificantDigits: 6, + }).format(Math.abs(10 ** a)) + ) + // first capture group grabs all the numbers with normal separator, then we grab any groups of whitespace+numbers + // this is so we know which languages keep whitespace before the suffix + .map((b) => b.replace(/(^[\d,.]+){1}(\s\d+)*/, '')) + .map((b) => b.toLowerCase()) +); + +const zipObjectFn = (arr: T[], valueGenerator: (val: T) => K) => + zipObject( + arr, + arr.map((val) => valueGenerator(val)) + ); + +// for each locale, look up a given suffix (from map above) and get the correct power of ten to divide numbers by when using this suffix +// e.g. for "en" if you look up "k" you get 3 (1000), if you look up "m" you get 6 (1,000,000) +const supportedLocaleToSuffixPowers = mapValues( + supportedLocaleToCompactSuffixByPowerOfTen, + (values) => zipObjectFn([...new Set(values)], (f) => values.indexOf(f)) +); + +export const getConsistentAssetSizeString = ( + sizeToRender: number, + { + decimalSeparator, + groupSeparator, + selectedLocale, + stepSize, + stepSizeDecimals, + }: { + selectedLocale: SupportedLocales; + stepSizeDecimals: number; + stepSize: number; + decimalSeparator: string | undefined; + groupSeparator: string | undefined; + } +) => { + const { displayDivisor, displaySuffix } = (() => { + if (stepSizeDecimals !== 0 || stepSize == null || stepSize < 10) { + return { displayDivisor: 1, displaySuffix: '' }; + } + const unitToUse = + supportedLocaleToCompactSuffixByPowerOfTen[selectedLocale][Math.floor(Math.log10(stepSize))]; + if (unitToUse == null) { + return { displayDivisor: 1, displaySuffix: '' }; + } + return { + displayDivisor: 10 ** supportedLocaleToSuffixPowers[selectedLocale][unitToUse], + displaySuffix: unitToUse, + }; + })(); + return `${formatNumberOutput(sizeToRender / displayDivisor, OutputType.Number, { + decimalSeparator, + groupSeparator, + fractionDigits: stepSizeDecimals, + })}${displaySuffix}`; +}; diff --git a/src/views/tables/LiveTrades.tsx b/src/views/tables/LiveTrades.tsx index ad2489ef4..ab4d7bc0e 100644 --- a/src/views/tables/LiveTrades.tsx +++ b/src/views/tables/LiveTrades.tsx @@ -6,9 +6,11 @@ import styled, { css, keyframes } from 'styled-components'; import { MarketTrade } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; +import { TOKEN_DECIMALS } from '@/constants/numbers'; import { EMPTY_ARR } from '@/constants/objects'; import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useLocaleSeparators } from '@/hooks/useLocaleSeparators'; import { useStringGetter } from '@/hooks/useStringGetter'; import breakpoints from '@/styles/breakpoints'; @@ -18,8 +20,10 @@ import { Output, OutputType } from '@/components/Output'; import { useAppSelector } from '@/state/appTypes'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; +import { getSelectedLocale } from '@/state/localizationSelectors'; import { getCurrentMarketConfig, getCurrentMarketLiveTrades } from '@/state/perpetualsSelectors'; +import { getConsistentAssetSizeString } from '@/lib/consistentAssetSize'; import { getSimpleStyledOutputType } from '@/lib/genericFunctionalComponentUtils'; import { isTruthy } from '@/lib/isTruthy'; import { getSelectedOrderSide } from '@/lib/tradeData'; @@ -52,7 +56,9 @@ export const LiveTrades = ({ className, histogramSide = 'left' }: StyleProps) => useAppSelector(getCurrentMarketLiveTrades, shallowEqual) ?? EMPTY_ARR; const { id = '' } = currentMarketAssetData ?? {}; - const { stepSizeDecimals, tickSizeDecimals } = currentMarketConfig ?? {}; + const { stepSizeDecimals, tickSizeDecimals, stepSize } = currentMarketConfig ?? {}; + const { decimal: decimalSeparator, group: groupSeparator } = useLocaleSeparators(); + const selectedLocale = useAppSelector(getSelectedLocale); const rows = currentMarketLiveTrades.map( ({ createdAtMilliseconds, price, size, side }: MarketTrade, idx) => ({ @@ -95,11 +101,15 @@ export const LiveTrades = ({ className, histogramSide = 'left' }: StyleProps) => tag: id, renderCell: (row: RowData) => ( <$SizeOutput - type={OutputType.Asset} - value={row.size} - fractionDigits={stepSizeDecimals} + type={OutputType.Text} + value={getConsistentAssetSizeString(row.size, { + decimalSeparator, + groupSeparator, + selectedLocale, + stepSize: stepSize ?? 10 ** (-1 * TOKEN_DECIMALS), + stepSizeDecimals: stepSizeDecimals ?? TOKEN_DECIMALS, + })} histogramSide={histogramSide} - useGrouping={false} /> ), },