diff --git a/src/plugins/d3/__stories__/scatter/PerformanceIssue.stories.tsx b/src/plugins/d3/__stories__/scatter/PerformanceIssue.stories.tsx index 80d6f8ba..4513d121 100644 --- a/src/plugins/d3/__stories__/scatter/PerformanceIssue.stories.tsx +++ b/src/plugins/d3/__stories__/scatter/PerformanceIssue.stories.tsx @@ -3,14 +3,38 @@ import {Meta, Story} from '@storybook/react'; import {Button} from '@gravity-ui/uikit'; import {settings} from '../../../../libs'; import {ChartKit} from '../../../../components/ChartKit'; -import type {ChartKitRef} from '../../../../types'; +import type {ChartKitRef, ChartKitWidgetData} from '../../../../types'; import {D3Plugin} from '../..'; -import data from '../scatter-performance.json'; +import {randomNormal} from 'd3'; const Template: Story = () => { const [shown, setShown] = React.useState(false); const chartkitRef = React.useRef(); + const widgetData: ChartKitWidgetData = React.useMemo(() => { + const categories = Array.from({length: 5000}).map((_, i) => String(i)); + const randomFn = randomNormal(0, 10); + + return { + xAxis: { + type: 'category', + categories: categories, + }, + series: { + data: [ + { + type: 'scatter', + name: 'Series 1', + data: categories.map((_, i) => ({ + x: i, + y: randomFn(), + })), + }, + ], + }, + }; + }, []); + if (!shown) { settings.set({plugins: [D3Plugin]}); return ; @@ -19,7 +43,7 @@ const Template: Story = () => { return (
{/* @ts-ignore */} - +
); }; diff --git a/src/plugins/d3/renderer/components/AxisX.tsx b/src/plugins/d3/renderer/components/AxisX.tsx index 550fee8b..9e6a5564 100644 --- a/src/plugins/d3/renderer/components/AxisX.tsx +++ b/src/plugins/d3/renderer/components/AxisX.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {axisBottom, select} from 'd3'; +import {axisBottom, ScaleLinear, select} from 'd3'; import type {AxisScale, AxisDomain} from 'd3'; import {block} from '../../../../utils/cn'; @@ -30,6 +30,10 @@ export const AxisX = ({axis, width, height, scale, chartWidth}: Props) => { const svgElement = select(ref.current); svgElement.selectAll('*').remove(); const tickSize = axis.grid.enabled ? height * -1 : 0; + const tickStep = + axis.type === 'category' + ? undefined + : (scale as ScaleLinear).ticks()[0]; let xAxisGenerator = axisBottom(scale as AxisScale) .tickSize(tickSize) .tickPadding(axis.labels.padding) @@ -43,6 +47,7 @@ export const AxisX = ({axis, width, height, scale, chartWidth}: Props) => { value, dateFormat: axis.labels['dateFormat'], numberFormat: axis.labels['numberFormat'], + step: tickStep, }); }); diff --git a/src/plugins/d3/renderer/components/AxisY.tsx b/src/plugins/d3/renderer/components/AxisY.tsx index c6e0d197..1467e405 100644 --- a/src/plugins/d3/renderer/components/AxisY.tsx +++ b/src/plugins/d3/renderer/components/AxisY.tsx @@ -1,14 +1,20 @@ import React from 'react'; -import {axisLeft, select} from 'd3'; -import type {AxisScale, AxisDomain, Selection} from 'd3'; +import {axisLeft, ScaleLinear, select} from 'd3'; +import type {AxisScale, AxisDomain} from 'd3'; import {block} from '../../../../utils/cn'; import type {ChartScale, PreparedAxis} from '../hooks'; -import {formatAxisTickLabel, parseTransformStyle} from '../utils'; +import { + formatAxisTickLabel, + parseTransformStyle, + setEllipsisForOverflowText, + setEllipsisForOverflowTexts, +} from '../utils'; const b = block('d3-axis'); const EMPTY_SPACE_BETWEEN_LABELS = 10; +const MAX_WIDTH = 80; type Props = { axises: PreparedAxis[]; @@ -17,27 +23,6 @@ type Props = { scale: ChartScale; }; -// Note: this method do not prepared for rotated labels -const removeOverlappingYTicks = (axis: Selection) => { - const a = axis.selectAll('g.tick').nodes(); - - if (a.length <= 1) { - return; - } - - for (let i = 0, x = 0; i < a.length; i++) { - const node = a[i] as Element; - const r = node.getBoundingClientRect(); - - if (r.bottom > x && i !== 0) { - node?.parentNode?.removeChild(node); - } else { - x = r.top - EMPTY_SPACE_BETWEEN_LABELS; - } - } -}; - -// FIXME: add overflow ellipsis for the labels that out of boundaries export const AxisY = ({axises, width, height, scale}: Props) => { const ref = React.useRef(null); @@ -50,6 +35,10 @@ export const AxisY = ({axises, width, height, scale}: Props) => { const svgElement = select(ref.current); svgElement.selectAll('*').remove(); const tickSize = axis.grid.enabled ? width * -1 : 0; + const tickStep = + axis.type === 'category' + ? undefined + : (scale as ScaleLinear).ticks()[0]; let yAxisGenerator = axisLeft(scale as AxisScale) .tickSize(tickSize) .tickPadding(axis.labels.padding) @@ -63,6 +52,7 @@ export const AxisY = ({axises, width, height, scale}: Props) => { value, dateFormat: axis.labels['dateFormat'], numberFormat: axis.labels['numberFormat'], + step: tickStep, }); }); @@ -78,10 +68,12 @@ export const AxisY = ({axises, width, height, scale}: Props) => { .style('stroke', axis.lineColor || ''); if (axis.labels.enabled) { - svgElement - .selectAll('.tick text') + const tickTexts = svgElement + .selectAll('.tick text') .style('font-size', axis.labels.style.fontSize) .style('transform', 'translateY(-1px)'); + + tickTexts.call(setEllipsisForOverflowTexts, MAX_WIDTH); } const transformStyle = svgElement.select('.tick').attr('transform'); @@ -92,6 +84,23 @@ export const AxisY = ({axises, width, height, scale}: Props) => { svgElement.select('.tick line').style('stroke', 'none'); } + // remove overlapping ticks + // Note: this method do not prepared for rotated labels + let elementY = 0; + svgElement + .selectAll('.tick') + .filter(function (_d, index) { + const node = this as unknown as Element; + const r = node.getBoundingClientRect(); + + if (r.bottom > elementY && index !== 0) { + return true; + } + elementY = r.top - EMPTY_SPACE_BETWEEN_LABELS; + return false; + }) + .remove(); + if (axis.title.text) { const textY = axis.title.height + axis.labels.padding; @@ -103,10 +112,9 @@ export const AxisY = ({axises, width, height, scale}: Props) => { .attr('dx', -height / 2) .attr('font-size', axis.title.style.fontSize) .attr('transform', 'rotate(-90)') - .text(axis.title.text); + .text(axis.title.text) + .call(setEllipsisForOverflowText, height); } - - removeOverlappingYTicks(svgElement); }, [axises, width, height, scale]); return ; diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index 30705d56..13260133 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -36,7 +36,9 @@ export const Chart = (props: Props) => { const {top, left, width, height, data} = props; const svgRef = React.createRef(); const {chartHovered, handleMouseEnter, handleMouseLeave} = useChartEvents(); - const {chart, title, tooltip, xAxis, yAxis} = useChartOptions(data); + const {chart, title, tooltip, xAxis, yAxis} = useChartOptions({ + data, + }); const {legendItems, legendConfig, preparedSeries, preparedLegend, handleLegendItemClick} = useSeries({ chartWidth: width, diff --git a/src/plugins/d3/renderer/hooks/useAxisScales/index.ts b/src/plugins/d3/renderer/hooks/useAxisScales/index.ts index a85f455e..fd91b731 100644 --- a/src/plugins/d3/renderer/hooks/useAxisScales/index.ts +++ b/src/plugins/d3/renderer/hooks/useAxisScales/index.ts @@ -3,7 +3,7 @@ import {scaleBand, scaleLinear, scaleUtc, extent} from 'd3'; import type {ScaleBand, ScaleLinear, ScaleTime} from 'd3'; import get from 'lodash/get'; -import type {ChartOptions} from '../useChartOptions/types'; +import type {ChartOptions, PreparedAxis} from '../useChartOptions/types'; import { getOnlyVisibleSeries, getDataCategoryValue, @@ -56,22 +56,70 @@ const filterCategoriesByVisibleSeries = (args: { return categories.filter((c) => visibleCategories.has(c)); }; +export function createYScale(axis: PreparedAxis, series: PreparedSeries[], boundsHeight: number) { + const yType = get(axis, 'type', 'linear'); + const yMin = get(axis, 'min'); + const yCategories = get(axis, 'categories'); + const yTimestamps = get(axis, 'timestamps'); + + switch (yType) { + case 'linear': { + const domain = getDomainDataYBySeries(series); + const range = [boundsHeight, boundsHeight * axis.maxPadding]; + + if (isNumericalArrayData(domain)) { + const [domainYMin, yMax] = extent(domain) as [number, number]; + const yMinValue = typeof yMin === 'number' ? yMin : domainYMin; + return scaleLinear().domain([yMinValue, yMax]).range(range).nice(); + } + + break; + } + case 'category': { + if (yCategories) { + const filteredCategories = filterCategoriesByVisibleSeries({ + axisDirection: 'y', + categories: yCategories, + series: series, + }); + return scaleBand().domain(filteredCategories).range([boundsHeight, 0]); + } + + break; + } + case 'datetime': { + const range = [boundsHeight, boundsHeight * axis.maxPadding]; + + if (yTimestamps) { + const [yMin, yMax] = extent(yTimestamps) as [number, number]; + return scaleUtc().domain([yMin, yMax]).range(range).nice(); + } else { + const domain = getDomainDataYBySeries(series); + + if (isNumericalArrayData(domain)) { + const [yMin, yMax] = extent(domain) as [number, number]; + return scaleUtc().domain([yMin, yMax]).range(range).nice(); + } + } + + break; + } + } + + throw new Error('Failed to create yScale'); +} + const createScales = (args: Args) => { const {boundsWidth, boundsHeight, series, xAxis, yAxis} = args; const xMin = get(xAxis, 'min'); const xType = get(xAxis, 'type', 'linear'); const xCategories = get(xAxis, 'categories'); const xTimestamps = get(xAxis, 'timestamps'); - const yType = get(yAxis[0], 'type', 'linear'); - const yMin = get(yAxis[0], 'min'); - const yCategories = get(yAxis[0], 'categories'); - const yTimestamps = get(xAxis, 'timestamps'); let visibleSeries = getOnlyVisibleSeries(series); // Reassign to all series in case of all series unselected, // otherwise we will get an empty space without grid visibleSeries = visibleSeries.length === 0 ? series : visibleSeries; let xScale: ChartScale | undefined; - let yScale: ChartScale | undefined; const xAxisMinPadding = boundsWidth * xAxis.maxPadding; const xRange = [0, boundsWidth - xAxisMinPadding]; @@ -125,55 +173,7 @@ const createScales = (args: Args) => { throw new Error('Failed to create xScale'); } - switch (yType) { - case 'linear': { - const domain = getDomainDataYBySeries(visibleSeries); - const range = [boundsHeight, boundsHeight * yAxis[0].maxPadding]; - - if (isNumericalArrayData(domain)) { - const [domainYMin, yMax] = extent(domain) as [number, number]; - const yMinValue = typeof yMin === 'number' ? yMin : domainYMin; - yScale = scaleLinear().domain([yMinValue, yMax]).range(range).nice(); - } - - break; - } - case 'category': { - if (yCategories) { - const filteredCategories = filterCategoriesByVisibleSeries({ - axisDirection: 'y', - categories: yCategories, - series: visibleSeries, - }); - yScale = scaleBand().domain(filteredCategories).range([boundsHeight, 0]); - } - - break; - } - case 'datetime': { - const range = [boundsHeight, boundsHeight * yAxis[0].maxPadding]; - - if (yTimestamps) { - const [yMin, yMax] = extent(yTimestamps) as [number, number]; - yScale = scaleUtc().domain([yMin, yMax]).range(range).nice(); - } else { - const domain = getDomainDataYBySeries(visibleSeries); - - if (isNumericalArrayData(domain)) { - const [yMin, yMax] = extent(domain) as [number, number]; - yScale = scaleUtc().domain([yMin, yMax]).range(range).nice(); - } - } - - break; - } - } - - if (!yScale) { - throw new Error('Failed to create yScale'); - } - - return {xScale, yScale}; + return {xScale, yScale: createYScale(yAxis[0], visibleSeries, boundsHeight)}; }; /** diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/index.ts b/src/plugins/d3/renderer/hooks/useChartOptions/index.ts index 1a5d3b7b..3c0c098d 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/index.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/index.ts @@ -9,10 +9,14 @@ import {getPreparedXAxis} from './x-axis'; import {getPreparedYAxis} from './y-axis'; import type {ChartOptions} from './types'; -type Args = ChartKitWidgetData; +type Args = { + data: ChartKitWidgetData; +}; export const useChartOptions = (args: Args): ChartOptions => { - const {chart, series, title, tooltip, xAxis, yAxis} = args; + const { + data: {chart, series, title, tooltip, xAxis, yAxis}, + } = args; const options: ChartOptions = React.useMemo(() => { const preparedTitle = getPreparedTitle({title}); const preparedTooltip = getPreparedTooltip({tooltip}); diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts index ba311e73..f827273e 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts @@ -1,5 +1,4 @@ -import {select, max} from 'd3'; -import type {AxisDomain} from 'd3'; +import {select, ScaleLinear} from 'd3'; import get from 'lodash/get'; import type { @@ -13,53 +12,46 @@ import { DEFAULT_AXIS_LABEL_PADDING, DEFAULT_AXIS_TITLE_FONT_SIZE, } from '../../constants'; -import {getDomainDataYBySeries, getHorisontalSvgTextHeight, formatAxisTickLabel} from '../../utils'; +import {getHorisontalSvgTextHeight, formatAxisTickLabel} from '../../utils'; import type {PreparedAxis} from './types'; +import {createYScale} from '../useAxisScales'; +import {PreparedSeries} from '../useSeries/types'; const getAxisLabelMaxWidth = (args: {axis: PreparedAxis; series: ChartKitWidgetSeries[]}) => { const {axis, series} = args; - let maxDomainValue: AxisDomain; - let width = 0; - switch (axis.type) { - case 'category': { - const yCategories = get(axis, 'categories', [] as string[]); - maxDomainValue = [...yCategories].sort((c1, c2) => c2.length - c1.length)[0]; - break; - } - case 'datetime': { - const yTimestamps = get(axis, 'timestamps'); - const domain = yTimestamps || (getDomainDataYBySeries(series) as number[]); - maxDomainValue = max(domain) as number; - break; - } - case 'linear': { - const domain = getDomainDataYBySeries(series) as number[]; - maxDomainValue = max(domain) as number; - } + if (!axis.labels.enabled) { + return 0; } - let formattedValue = ''; + const scale = createYScale(axis, series as PreparedSeries[], 1) as ScaleLinear; + const ticks = axis.type === 'category' ? axis.categories || [] : scale.ticks(); + const tickStep = axis.type === 'category' ? undefined : Number(ticks[0]); - if (axis.labels.enabled) { - formattedValue = formatAxisTickLabel({ - axisType: axis.type, - value: maxDomainValue, - dateFormat: axis.labels['dateFormat'], - numberFormat: axis.labels['numberFormat'], + // ToDo: it is necessary to filter data, since we do not draw overlapping ticks + + const svg = select(document.body).append('svg'); + const text = svg.append('g').append('text').style('font-size', axis.labels.style.fontSize); + text.selectAll('tspan') + .data(ticks as (string | number)[]) + .enter() + .append('tspan') + .attr('x', 0) + .attr('dy', 0) + .text((d) => { + return formatAxisTickLabel({ + axisType: axis.type, + value: d, + dateFormat: axis.labels['dateFormat'], + numberFormat: axis.labels['numberFormat'], + step: tickStep, + }); }); - } - select(document.body) - .append('text') - .style('font-size', axis.labels.style.fontSize) - .text(formattedValue) - .each(function () { - width = this.getBoundingClientRect().width; - }) - .remove(); + const maxWidth = (text.node() as SVGTextElement).getBoundingClientRect()?.width || 0; + svg.remove(); - return width; + return maxWidth; }; const applyLabelsMaxWidth = (args: { @@ -67,8 +59,8 @@ const applyLabelsMaxWidth = (args: { preparedYAxis: PreparedAxis; }) => { const {series, preparedYAxis} = args; - const maxWidth = getAxisLabelMaxWidth({axis: preparedYAxis, series}); - preparedYAxis.labels.maxWidth = maxWidth; + + preparedYAxis.labels.maxWidth = getAxisLabelMaxWidth({axis: preparedYAxis, series}); }; export const getPreparedYAxis = ({ diff --git a/src/plugins/d3/renderer/utils/index.ts b/src/plugins/d3/renderer/utils/index.ts index b3a3d7ae..d89d5df6 100644 --- a/src/plugins/d3/renderer/utils/index.ts +++ b/src/plugins/d3/renderer/utils/index.ts @@ -12,8 +12,8 @@ import type { BarXSeries, } from '../../../../types/widget-data'; import {formatNumber} from '../../../shared'; -import type {FormatNumberOptions} from '../../../shared'; import {DEFAULT_AXIS_LABEL_FONT_SIZE} from '../constants'; +import {getNumberUnitRate} from '../../../shared/format-number/format-number'; export * from './math'; export * from './text'; @@ -135,22 +135,14 @@ export const parseTransformStyle = (style: string | null): {x?: number; y?: numb return {x, y}; }; -const defaultFormatNumberOptions: FormatNumberOptions = { - precision: 0, -}; - export const formatAxisTickLabel = (args: { axisType: ChartKitWidgetAxisType; value: AxisDomain; dateFormat?: ChartKitWidgetAxisLabels['dateFormat']; numberFormat?: ChartKitWidgetAxisLabels['numberFormat']; + step?: number; }) => { - const { - axisType, - value, - dateFormat = 'DD.MM.YY', - numberFormat = defaultFormatNumberOptions, - } = args; + const {axisType, value, dateFormat = 'DD.MM.YY', numberFormat, step} = args; switch (axisType) { case 'category': { @@ -161,7 +153,8 @@ export const formatAxisTickLabel = (args: { } case 'linear': default: { - return formatNumber(value as number | string, numberFormat); + const unitRate = step ? getNumberUnitRate(step) : undefined; + return formatNumber(value as number | string, {unitRate, ...numberFormat}); } } }; diff --git a/src/plugins/d3/renderer/utils/text.ts b/src/plugins/d3/renderer/utils/text.ts index d7a95761..8a24ffa1 100644 --- a/src/plugins/d3/renderer/utils/text.ts +++ b/src/plugins/d3/renderer/utils/text.ts @@ -1,7 +1,8 @@ -import {Selection} from 'd3-selection'; +import type {Selection} from 'd3'; +import {select} from 'd3'; export function setEllipsisForOverflowText( - selection: Selection, + selection: Selection, maxWidth: number, ) { let text = selection.text(); @@ -15,3 +16,12 @@ export function setEllipsisForOverflowText( textLength = tSpan.node()?.getComputedTextLength() || 0; } } + +export function setEllipsisForOverflowTexts( + selection: Selection, + maxWidth: number, +) { + selection.each(function () { + setEllipsisForOverflowText(select(this), maxWidth); + }); +} diff --git a/src/plugins/highcharts/__stories__/scatter/PerformanceIssue.stories.tsx b/src/plugins/highcharts/__stories__/scatter/PerformanceIssue.stories.tsx new file mode 100644 index 00000000..220a67a3 --- /dev/null +++ b/src/plugins/highcharts/__stories__/scatter/PerformanceIssue.stories.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {Meta, Story} from '@storybook/react'; +import {Button} from '@gravity-ui/uikit'; +import {settings} from '../../../../libs'; +import {ChartKit} from '../../../../components/ChartKit'; +import type {ChartKitRef} from '../../../../types'; +import {HighchartsPlugin, HighchartsWidgetData} from '../..'; +import {randomNormal} from 'd3'; + +const Template: Story = () => { + const [shown, setShown] = React.useState(false); + const chartkitRef = React.useRef(); + + const widgetData = React.useMemo(() => { + const categories = Array.from({length: 5000}).map((_, i) => String(i)); + const randomFn = randomNormal(0, 10); + + return { + data: { + graphs: [ + { + type: 'scatter', + name: 'Series 1', + data: categories.map((_, i) => ({ + x: i, + y: randomFn(), + })), + }, + ], + categories: categories, + }, + libraryConfig: { + chart: { + type: 'scatter', + }, + }, + } as unknown as HighchartsWidgetData; + }, []); + + if (!shown) { + settings.set({plugins: [HighchartsPlugin]}); + return ; + } + + return ( +
+ +
+ ); +}; + +export const PerformanceIssue = Template.bind({}); + +const meta: Meta = { + title: 'Plugins/Highcharts/Scatter', +}; + +export default meta; diff --git a/src/plugins/highcharts/renderer/components/HighchartsComponent.tsx b/src/plugins/highcharts/renderer/components/HighchartsComponent.tsx index dc9f826e..5a84415c 100644 --- a/src/plugins/highcharts/renderer/components/HighchartsComponent.tsx +++ b/src/plugins/highcharts/renderer/components/HighchartsComponent.tsx @@ -53,7 +53,7 @@ export class HighchartsComponent extends React.PureComponent { const configOptions = Object.assign( { nonBodyScroll, - drillDownData: nextProps.data.config.drillDown, + drillDownData: nextProps.data.config?.drillDown, splitTooltip: nextProps.splitTooltip, highcharts: libraryConfig, entryId, diff --git a/src/plugins/shared/format-number/format-number.ts b/src/plugins/shared/format-number/format-number.ts index 0b5298a5..a38a2921 100644 --- a/src/plugins/shared/format-number/format-number.ts +++ b/src/plugins/shared/format-number/format-number.ts @@ -5,6 +5,19 @@ import type {FormatOptions, FormatNumberOptions} from './types'; const i18n = makeInstance('chartkit-units', {ru, en}); +function getUnitRate(value: number, exponent: number, unitsI18nKeys: string[]) { + let resultUnitRate = 1; + while ( + Math.abs(value / Math.pow(exponent, resultUnitRate)) >= 1 && + resultUnitRate < 10 && + i18n(unitsI18nKeys[resultUnitRate]) + ) { + resultUnitRate++; + } + + return resultUnitRate - 1; +} + const unitFormatter = ({ exponent, unitsI18nKeys, @@ -22,20 +35,8 @@ const unitFormatter = ({ i18nInstance.setLang(lang); } - let resultUnitRate; - if (typeof unitRate === 'number') { - resultUnitRate = unitRate; - } else { - resultUnitRate = 1; - while ( - Math.abs(value / Math.pow(exponent, resultUnitRate)) >= 1 && - resultUnitRate < 10 && - i18n(unitsI18nKeys[resultUnitRate]) - ) { - resultUnitRate++; - } - resultUnitRate--; - } + const resultUnitRate = + typeof unitRate === 'number' ? unitRate : getUnitRate(value, exponent, unitsI18nKeys); let result: number | string = value / Math.pow(exponent, resultUnitRate); if (typeof precision === 'number') { @@ -71,18 +72,23 @@ export const formatDuration = unitFormatter({ unitsI18nKeys: ['value_short-milliseconds', 'value_short-seconds', 'value_short-minutes'], }); +const BASE_NUMBER_FORMAT_UNIT_KEYS = [ + 'value_short-empty', + 'value_short-k', + 'value_short-m', + 'value_short-b', + 'value_short-t', +]; + const baseFormatNumber = unitFormatter({ exponent: 1000, unitDelimiterI18nKey: 'value_number-delimiter', - unitsI18nKeys: [ - 'value_short-empty', - 'value_short-k', - 'value_short-m', - 'value_short-b', - 'value_short-t', - ], + unitsI18nKeys: BASE_NUMBER_FORMAT_UNIT_KEYS, }); +export const getNumberUnitRate = (value: number) => + getUnitRate(value, 1000, BASE_NUMBER_FORMAT_UNIT_KEYS); + const NUMBER_UNIT_RATE_BY_UNIT = { default: 0, auto: undefined,