Commit 8d6429de authored by tom goriunov's avatar tom goriunov Committed by GitHub

Fix RSK stats - transaction fees precision (#1602)

* Fix RSK stats - transaction fees precision

Fixes #1473

* update screenshot
parent b2432b38
......@@ -38,7 +38,7 @@ NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
......
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.98 5.36c-.387 0-.645.258-.645.645a4.485 4.485 0 0 1-4.512 4.512h-.451c-.13 0-.194 0-.322-.064-.13 0-.194-.065-.323-.065-.064 0-.129-.064-.193-.064-.451-.129-.903-.387-1.29-.645-.064-.064-.128-.064-.128-.129-.065-.064-.13-.129-.194-.129-.129-.064-.258-.193-.322-.257l-.129-.13 1.16-.193a.669.669 0 0 0 .516-.773.669.669 0 0 0-.773-.516l-2.128.387-.515.129a.435.435 0 0 0-.387.258 1.195 1.195 0 0 0-.13.45l.517 2.708c.064.323.322.516.644.516h.13a.669.669 0 0 0 .515-.774l-.194-1.095c.13.128.323.257.452.386h.064c.065.065.129.13.193.13.13.128.258.193.452.257.129.258.322.322.515.451h.065c.064.065.193.065.322.13.258.064.516.128.71.193.128 0 .257.064.386.064h.774a5.778 5.778 0 0 0 5.801-5.801c.065-.323-.258-.58-.58-.58ZM.73 6.65c.387 0 .645-.258.645-.645a4.485 4.485 0 0 1 4.513-4.512h.45c.13 0 .194 0 .323.064.13 0 .194.065.323.065.064 0 .128.064.193.064.451.129.902.322 1.29.645.064.064.128.064.193.129.064.064.129.064.193.128.064.065.129.194.258.258l.129.13-1.16.193a.669.669 0 0 0-.516.773c.064.322.322.516.644.516h.13l2.707-.516a.669.669 0 0 0 .515-.773L11.045.526A.669.669 0 0 0 10.27.01a.669.669 0 0 0-.516.773L9.95 1.88c-.13-.129-.322-.322-.451-.386h-.065c-.064-.065-.129-.13-.193-.13-.13-.128-.258-.193-.451-.322-.194-.129-.387-.193-.58-.322h-.065C8.079.655 7.95.655 7.822.59a10.574 10.574 0 0 1-.71-.193c-.128 0-.257-.065-.386-.065h-.773A5.778 5.778 0 0 0 .15 6.134c-.064.258.194.516.58.516Z" fill="currentColor"/>
<path d="M10.98 5.36c-.387 0-.645.258-.645.645a4.485 4.485 0 0 1-4.512 4.512h-.451c-.13 0-.194 0-.322-.064-.13 0-.194-.065-.323-.065-.064 0-.129-.064-.193-.064-.451-.129-.903-.387-1.29-.645-.064-.064-.128-.064-.128-.129-.065-.064-.13-.129-.194-.129-.129-.064-.258-.193-.322-.257l-.129-.13 1.16-.193a.669.669 0 0 0 .516-.773.669.669 0 0 0-.773-.516l-2.128.387-.515.129a.435.435 0 0 0-.387.258 1.195 1.195 0 0 0-.13.45l.517 2.708c.064.323.322.516.644.516h.13a.669.669 0 0 0 .515-.774l-.194-1.095c.13.128.323.257.452.386h.064c.065.065.129.13.193.13.13.128.258.193.452.257.129.258.322.322.515.451h.065c.064.065.193.065.322.13.258.064.516.128.71.193.128 0 .257.064.386.064h.774a5.778 5.778 0 0 0 5.801-5.801c.065-.323-.258-.58-.58-.58ZM.73 6.65c.387 0 .645-.258.645-.645a4.485 4.485 0 0 1 4.513-4.512h.45c.13 0 .194 0 .323.064.13 0 .194.065.323.065.064 0 .128.064.193.064.451.129.902.322 1.29.645.064.064.128.064.193.129.064.064.129.064.193.128.064.065.129.194.258.258l.129.13-1.16.193a.669.669 0 0 0-.516.773c.064.322.322.516.644.516h.13l2.707-.516a.669.669 0 0 0 .515-.773L11.045.526A.669.669 0 0 0 10.27.01a.669.669 0 0 0-.516.773L9.95 1.88c-.13-.129-.322-.322-.451-.386h-.065c-.064-.065-.129-.13-.193-.13-.13-.128-.258-.193-.451-.322C8.596.913 8.403.849 8.21.72h-.065C8.079.655 7.95.655 7.822.59a10.574 10.574 0 0 1-.71-.193c-.128 0-.257-.065-.386-.065h-.773A5.778 5.778 0 0 0 .15 6.134c-.064.258.194.516.58.516Z" fill="currentColor"/>
</svg>
......@@ -23,11 +23,11 @@ const ChainIndicatorChart = ({ data }: Props) => {
const axesConfig = React.useMemo(() => {
return {
x: { ticks: 4 },
y: { ticks: 3, nice: true },
y: { ticks: 3, nice: true, noLabel: true },
};
}, [ ]);
const { rect, ref, axis, innerWidth, innerHeight } = useTimeChartController({
const { rect, ref, axes, innerWidth, innerHeight, chartMargin } = useTimeChartController({
data,
margin: CHART_MARGIN,
axesConfig,
......@@ -35,16 +35,16 @@ const ChainIndicatorChart = ({ data }: Props) => {
return (
<svg width="100%" height="100%" ref={ ref } cursor="pointer">
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ rect ? 1 : 0 }>
<g transform={ `translate(${ chartMargin.left || 0 },${ chartMargin.top || 0 })` } opacity={ rect ? 1 : 0 }>
<ChartArea
data={ data[0].items }
xScale={ axis.x.scale }
yScale={ axis.y.scale }
xScale={ axes.x.scale }
yScale={ axes.y.scale }
/>
<ChartLine
data={ data[0].items }
xScale={ axis.x.scale }
yScale={ axis.y.scale }
xScale={ axes.x.scale }
yScale={ axes.y.scale }
stroke={ lineColor }
animation="left"
strokeWidth={ 3 }
......@@ -54,8 +54,8 @@ const ChainIndicatorChart = ({ data }: Props) => {
anchorEl={ overlayRef.current }
width={ innerWidth }
height={ innerHeight }
xScale={ axis.x.scale }
yScale={ axis.y.scale }
xScale={ axes.x.scale }
yScale={ axes.y.scale }
data={ data }
/>
</ChartOverlay>
......
......@@ -82,7 +82,7 @@ const ChartTooltip = ({ xScale, yScale, width, tooltipWidth = 200, height, data,
.selectAll<Element, TimeChartData>('.ChartTooltip__value')
.filter((td, tIndex) => tIndex === i)
.text(
(data[i].valueFormatter?.(d.value) || d.value.toLocaleString()) +
(data[i].valueFormatter?.(d.value) || d.value.toLocaleString(undefined, { minimumSignificantDigits: 1 })) +
(data[i].units ? ` ${ data[i].units }` : ''),
)
.nodes();
......
......@@ -73,3 +73,61 @@ test('error', async({ mount }) => {
await expect(component).toHaveScreenshot();
});
test('small values', async({ mount, page }) => {
const modifiedProps = {
...props,
items: [
{ date: new Date('2023-02-13'), value: 0.000005041012112611958 },
{ date: new Date('2023-02-14'), value: 0.000004781545670577531 },
{ date: new Date('2023-02-15'), value: 0.00000520510604212437 },
{ date: new Date('2023-02-16'), value: 0.000005274901030625893 },
{ date: new Date('2023-02-17'), value: 0.00000534325322320271 },
{ date: new Date('2023-02-18'), value: 0.00000579140116207668 },
{ date: new Date('2023-02-19'), value: 0.000004878307079043056 },
{ date: new Date('2023-02-20'), value: 0.0000053454186920910215 },
{ date: new Date('2023-02-21'), value: 0.000005770588532081243 },
{ date: new Date('2023-02-22'), value: 0.00000589334810122426 },
{ date: new Date('2023-02-23'), value: 0.00000547040196358741 },
],
};
const component = await mount(
<TestApp>
<ChartWidget { ...modifiedProps }/>
</TestApp>,
);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Nativecoincirculatingsupply-small"]')?.getAttribute('opacity') === '1';
});
await expect(component).toHaveScreenshot();
});
test('small variations in big values', async({ mount, page }) => {
const modifiedProps = {
...props,
items: [
{ date: new Date('2023-02-13'), value: 8886203 },
{ date: new Date('2023-02-14'), value: 8890184 },
{ date: new Date('2023-02-15'), value: 8893483 },
{ date: new Date('2023-02-16'), value: 8897924 },
{ date: new Date('2023-02-17'), value: 8902268 },
{ date: new Date('2023-02-18'), value: 8906320 },
{ date: new Date('2023-02-19'), value: 8910264 },
{ date: new Date('2023-02-20'), value: 8914827 },
{ date: new Date('2023-02-21'), value: 8918592 },
{ date: new Date('2023-02-22'), value: 8921988 },
{ date: new Date('2023-02-23'), value: 8922206 },
],
};
const component = await mount(
<TestApp>
<ChartWidget { ...modifiedProps }/>
</TestApp>,
);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Nativecoincirculatingsupply-small"]')?.getAttribute('opacity') === '1';
});
await expect(component).toHaveScreenshot();
});
......@@ -2,7 +2,7 @@ import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
import type { ChartMargin, TimeChartItem } from 'ui/shared/chart/types';
import type { ChartMargin, TimeChartData, TimeChartItem } from 'ui/shared/chart/types';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -27,7 +27,7 @@ interface Props {
// temporarily turn off the data aggregation, we need a better algorithm for that
const MAX_SHOW_ITEMS = 100_000_000_000;
const DEFAULT_CHART_MARGIN = { bottom: 20, left: 40, right: 20, top: 10 };
const DEFAULT_CHART_MARGIN = { bottom: 20, left: 10, right: 20, top: 10 };
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin: marginProps, units }: Props) => {
const isMobile = useIsMobile();
......@@ -51,7 +51,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
}
}, [ isGroupedValues, rangedItems ]);
const chartData = React.useMemo(() => ([ { items: displayedData, name: 'Value', color, units } ]), [ color, displayedData, units ]);
const chartData: TimeChartData = React.useMemo(() => ([ { items: displayedData, name: 'Value', color, units } ]), [ color, displayedData, units ]);
const margin: ChartMargin = React.useMemo(() => ({ ...DEFAULT_CHART_MARGIN, ...marginProps }), [ marginProps ]);
const axesConfig = React.useMemo(() => {
......@@ -72,7 +72,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
innerWidth,
innerHeight,
chartMargin,
axis,
axes,
} = useTimeChartController({
data: chartData,
margin,
......@@ -96,7 +96,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
<g transform={ `translate(${ chartMargin?.left || 0 },${ chartMargin?.top || 0 })` }>
<ChartGridLine
type="horizontal"
scale={ axis.y.scale }
scale={ axes.y.scale }
ticks={ axesConfig.y.ticks }
size={ innerWidth }
disableAnimation
......@@ -106,14 +106,14 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
id={ chartId }
data={ displayedData }
color={ color }
xScale={ axis.x.scale }
yScale={ axis.y.scale }
xScale={ axes.x.scale }
yScale={ axes.y.scale }
/>
<ChartLine
data={ displayedData }
xScale={ axis.x.scale }
yScale={ axis.y.scale }
xScale={ axes.x.scale }
yScale={ axes.y.scale }
stroke={ color }
animation="none"
strokeWidth={ isMobile ? 1 : 2 }
......@@ -121,19 +121,19 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
<ChartAxis
type="left"
scale={ axis.y.scale }
scale={ axes.y.scale }
ticks={ axesConfig.y.ticks }
tickFormatGenerator={ axis.y.tickFormatter }
tickFormatGenerator={ axes.y.tickFormatter }
disableAnimation
/>
<ChartAxis
type="bottom"
scale={ axis.x.scale }
scale={ axes.x.scale }
transform={ `translate(0, ${ innerHeight })` }
ticks={ axesConfig.x.ticks }
anchorEl={ overlayRef.current }
tickFormatGenerator={ axis.x.tickFormatter }
tickFormatGenerator={ axes.x.tickFormatter }
disableAnimation
/>
......@@ -143,15 +143,15 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
width={ innerWidth }
tooltipWidth={ isGroupedValues ? 280 : 200 }
height={ innerHeight }
xScale={ axis.x.scale }
yScale={ axis.y.scale }
xScale={ axes.x.scale }
yScale={ axes.y.scale }
data={ chartData }
/>
<ChartSelectionX
anchorEl={ overlayRef.current }
height={ innerHeight }
scale={ axis.x.scale }
scale={ axes.x.scale }
data={ chartData }
onSelect={ handleRangeSelect }
/>
......
......@@ -29,6 +29,7 @@ export type TimeChartData = Array<TimeChartDataItem>;
export interface AxisConfig {
ticks?: number;
nice?: boolean;
noLabel?: boolean;
}
export interface AxesConfig {
......
......@@ -5,7 +5,7 @@ import type { AxesConfig, ChartMargin, TimeChartData } from 'ui/shared/chart/typ
import useClientRect from 'lib/hooks/useClientRect';
import calculateInnerSize from './utils/calculateInnerSize';
import { getAxisParams, DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS } from './utils/timeChartAxis';
import { getAxesParams } from './utils/timeChartAxis';
interface Props {
data: TimeChartData;
......@@ -19,29 +19,27 @@ export default function useTimeChartController({ data, margin, axesConfig }: Pro
// we need to recalculate the axis scale whenever the rect width changes
// eslint-disable-next-line react-hooks/exhaustive-deps
const axisParams = React.useMemo(() => getAxisParams(data, axesConfig), [ data, axesConfig, rect?.width ]);
const axesParams = React.useMemo(() => getAxesParams(data, axesConfig), [ data, axesConfig, rect?.width ]);
const chartMargin = React.useMemo(() => {
const exceedingDigits = (axisParams.y.labelFormatParams.maximumSignificantDigits ?? DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS) -
DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS;
const PIXELS_PER_DIGIT = 7;
const leftShift = PIXELS_PER_DIGIT * exceedingDigits;
const PIXELS_PER_DIGIT = 8;
const leftShift = axesConfig?.y?.noLabel ? 0 : PIXELS_PER_DIGIT * axesParams.y.labelFormatParams.maxLabelLength;
return {
...margin,
left: (margin?.left ?? 0) + leftShift,
};
}, [ axisParams.y.labelFormatParams.maximumSignificantDigits, margin ]);
}, [ axesParams.y.labelFormatParams.maxLabelLength, margin, axesConfig?.y?.noLabel ]);
const { innerWidth, innerHeight } = calculateInnerSize(rect, chartMargin);
const xScale = React.useMemo(() => {
return axisParams.x.scale.range([ 0, innerWidth ]);
}, [ axisParams.x.scale, innerWidth ]);
return axesParams.x.scale.range([ 0, innerWidth ]);
}, [ axesParams.x.scale, innerWidth ]);
const yScale = React.useMemo(() => {
return axisParams.y.scale.range([ innerHeight, 0 ]);
}, [ axisParams.y.scale, innerHeight ]);
return axesParams.y.scale.range([ innerHeight, 0 ]);
}, [ axesParams.y.scale, innerHeight ]);
return React.useMemo(() => {
return {
......@@ -50,16 +48,16 @@ export default function useTimeChartController({ data, margin, axesConfig }: Pro
chartMargin,
innerWidth,
innerHeight,
axis: {
axes: {
x: {
tickFormatter: axisParams.x.tickFormatter,
tickFormatter: axesParams.x.tickFormatter,
scale: xScale,
},
y: {
tickFormatter: axisParams.y.tickFormatter,
tickFormatter: axesParams.y.tickFormatter,
scale: yScale,
},
},
};
}, [ axisParams.x.tickFormatter, axisParams.y.tickFormatter, chartMargin, innerHeight, innerWidth, rect, ref, xScale, yScale ]);
}, [ axesParams.x.tickFormatter, axesParams.y.tickFormatter, chartMargin, innerHeight, innerWidth, rect, ref, xScale, yScale ]);
}
import * as d3 from 'd3';
import _maxBy from 'lodash/maxBy';
import _unique from 'lodash/uniq';
import type { AxesConfig, AxisConfig, TimeChartData } from '../types';
......@@ -8,8 +9,15 @@ import { WEEK, MONTH, YEAR } from 'lib/consts';
export const DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = 2;
export const DEFAULT_MAXIMUM_FRACTION_DIGITS = 3;
export const MAXIMUM_SIGNIFICANT_DIGITS_LIMIT = 8;
export const DEFAULT_LABEL_LENGTH = 5;
export function getAxisParams(data: TimeChartData, axesConfig?: AxesConfig) {
export interface LabelFormatParams extends Intl.NumberFormatOptions {
maxLabelLength: number;
}
type Data = TimeChartData;
export function getAxesParams(data: Data, axesConfig?: AxesConfig) {
const { labelFormatParams: labelFormatParamsY, scale: yScale } = getAxisParamsY(data, axesConfig?.y);
return {
......@@ -25,7 +33,7 @@ export function getAxisParams(data: TimeChartData, axesConfig?: AxesConfig) {
};
}
function getAxisParamsX(data: TimeChartData) {
function getAxisParamsX(data: Data) {
const min = d3.min(data, ({ items }) => d3.min(items, ({ date }) => date)) ?? new Date();
const max = d3.max(data, ({ items }) => d3.max(items, ({ date }) => date)) ?? new Date();
const scale = d3.scaleTime().domain([ min, max ]);
......@@ -53,7 +61,7 @@ const tickFormatterX = (axis: d3.Axis<d3.NumberValue>) => (d: d3.AxisDomain) =>
return format(d as Date);
};
function getAxisParamsY(data: TimeChartData, config?: AxisConfig) {
function getAxisParamsY(data: Data, config?: AxisConfig) {
const DEFAULT_TICKS_NUM = 3;
const min = d3.min(data, ({ items }) => d3.min(items, ({ value }) => value)) ?? 0;
const max = d3.max(data, ({ items }) => d3.max(items, ({ value }) => value)) ?? 0;
......@@ -72,27 +80,21 @@ function getAxisParamsY(data: TimeChartData, config?: AxisConfig) {
const getTickFormatterY = (params: Intl.NumberFormatOptions) => () => (d: d3.AxisDomain) => {
const num = Number(d);
if (num < 1) {
// for small number there are no algorithm to format label right now
// so we set it to 3 digits after dot maximum
return num.toLocaleString(undefined, { maximumFractionDigits: 3 });
}
return num.toLocaleString(undefined, params);
};
function getYLabelFormatParams(ticks: Array<number>, maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS): Intl.NumberFormatOptions {
function getYLabelFormatParams(ticks: Array<number>, maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS): LabelFormatParams {
const params = {
maximumFractionDigits: 3,
maximumFractionDigits: DEFAULT_MAXIMUM_FRACTION_DIGITS,
maximumSignificantDigits,
notation: 'compact' as const,
};
const uniqTicksStr = _unique(ticks.map((tick) => tick.toLocaleString(undefined, params)));
const maxLabelLength = _maxBy(uniqTicksStr, (items) => items.length)?.length ?? DEFAULT_LABEL_LENGTH;
if (uniqTicksStr.length === ticks.length || maximumSignificantDigits === MAXIMUM_SIGNIFICANT_DIGITS_LIMIT) {
return params;
return { ...params, maxLabelLength };
}
return getYLabelFormatParams(ticks, maximumSignificantDigits + 1);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment