Commit 644c0070 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #658 from blockscout/charts-fixes

charts fixes & units
parents 80bda612 ee7ef6ce
...@@ -44,6 +44,7 @@ export type StatsChartInfo = { ...@@ -44,6 +44,7 @@ export type StatsChartInfo = {
id: string; id: string;
title: string; title: string;
description: string; description: string;
units: string | null;
} }
export type StatsChart = { chart: Array<StatsChartItem> }; export type StatsChart = { chart: Array<StatsChartItem> };
......
...@@ -41,7 +41,7 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props ...@@ -41,7 +41,7 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props
.x(({ date }) => xScale(date)) .x(({ date }) => xScale(date))
.y1(({ value }) => yScale(value)) .y1(({ value }) => yScale(value))
.y0(() => yScale(yScale.domain()[0])) .y0(() => yScale(yScale.domain()[0]))
.curve(d3.curveNatural); .curve(d3.curveCatmullRom);
return area(data) || undefined; return area(data) || undefined;
}, [ xScale, yScale, data ]); }, [ xScale, yScale, data ]);
......
...@@ -62,7 +62,7 @@ const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => { ...@@ -62,7 +62,7 @@ const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => {
const line = d3.line<TimeChartItem>() const line = d3.line<TimeChartItem>()
.x((d) => xScale(d.date)) .x((d) => xScale(d.date))
.y((d) => yScale(d.value)) .y((d) => yScale(d.value))
.curve(d3.curveNatural); .curve(d3.curveCatmullRom);
return ( return (
<path <path
......
...@@ -9,7 +9,6 @@ import type { Pointer } from 'ui/shared/chart/utils/pointerTracker'; ...@@ -9,7 +9,6 @@ import type { Pointer } from 'ui/shared/chart/utils/pointerTracker';
import { trackPointer } from 'ui/shared/chart/utils/pointerTracker'; import { trackPointer } from 'ui/shared/chart/utils/pointerTracker';
interface Props { interface Props {
chartId?: string;
width?: number; width?: number;
tooltipWidth?: number; tooltipWidth?: number;
height?: number; height?: number;
...@@ -23,8 +22,9 @@ const TEXT_LINE_HEIGHT = 12; ...@@ -23,8 +22,9 @@ const TEXT_LINE_HEIGHT = 12;
const PADDING = 16; const PADDING = 16;
const LINE_SPACE = 10; const LINE_SPACE = 10;
const POINT_SIZE = 16; const POINT_SIZE = 16;
const LABEL_WIDTH = 80;
const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, data, anchorEl, ...props }: Props) => { const ChartTooltip = ({ xScale, yScale, width, tooltipWidth = 200, height, data, anchorEl, ...props }: Props) => {
const lineColor = useToken('colors', 'gray.400'); const lineColor = useToken('colors', 'gray.400');
const titleColor = useToken('colors', 'blue.100'); const titleColor = useToken('colors', 'blue.100');
const textColor = useToken('colors', 'white'); const textColor = useToken('colors', 'white');
...@@ -78,10 +78,23 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da ...@@ -78,10 +78,23 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da
); );
const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => { const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => {
d3.selectAll(`${ chartId ? `#${ chartId }` : '' } .ChartTooltip__value`) const nodes = d3.select(ref.current)
.selectAll<Element, TimeChartData>('.ChartTooltip__value')
.filter((td, tIndex) => tIndex === i) .filter((td, tIndex) => tIndex === i)
.text(data[i].valueFormatter?.(d.value) || d.value.toLocaleString()); .text(
}, [ data, chartId ]); (data[i].valueFormatter?.(d.value) || d.value.toLocaleString()) +
(data[i].units ? ` ${ data[i].units }` : ''),
)
.nodes();
const widthLimit = tooltipWidth - 2 * PADDING - LABEL_WIDTH;
const width = nodes.map((node) => node?.getBoundingClientRect?.().width);
const maxNodeWidth = Math.max(...width);
d3.select(ref.current)
.select('.ChartTooltip__contentBg')
.attr('width', tooltipWidth + Math.max(0, (maxNodeWidth - widthLimit)));
}, [ data, tooltipWidth ]);
const drawPoints = React.useCallback((x: number) => { const drawPoints = React.useCallback((x: number) => {
const xDate = xScale.invert(x); const xDate = xScale.invert(x);
...@@ -230,7 +243,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da ...@@ -230,7 +243,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da
rx={ 12 } rx={ 12 }
ry={ 12 } ry={ 12 }
fill={ bgColor } fill={ bgColor }
width={ tooltipWidth || 200 } width={ tooltipWidth }
height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE } height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE }
/> />
<g transform={ `translate(${ PADDING },${ PADDING })` }> <g transform={ `translate(${ PADDING },${ PADDING })` }>
...@@ -246,7 +259,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da ...@@ -246,7 +259,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da
</text> </text>
<text <text
className="ChartTooltip__contentDate" className="ChartTooltip__contentDate"
transform="translate(80,0)" transform={ `translate(${ LABEL_WIDTH },0)` }
fontSize="12px" fontSize="12px"
fontWeight="500" fontWeight="500"
fill={ textColor } fill={ textColor }
...@@ -269,7 +282,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da ...@@ -269,7 +282,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da
{ name } { name }
</text> </text>
<text <text
transform="translate(80,0)" transform={ `translate(${ LABEL_WIDTH },0)` }
className="ChartTooltip__value" className="ChartTooltip__value"
fontSize="12px" fontSize="12px"
fill={ textColor } fill={ textColor }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import type { Props } from './ChartWidget';
import ChartWidget from './ChartWidget';
test.use({ viewport: { width: 400, height: 300 } });
const props: Props = {
items: [
{ date: new Date('2023-02-13'), value: 1.7631568828337087 },
{ date: new Date('2023-02-14'), value: 9547912.248607066 },
{ date: new Date('2023-02-15'), value: 19795391.669569757 },
{ date: new Date('2023-02-16'), value: 18338481.6037719 },
{ date: new Date('2023-02-17'), value: 18716729.946751505 },
{ date: new Date('2023-02-18'), value: 32164355.603443976 },
{ date: new Date('2023-02-19'), value: 20856850.45498412 },
{ date: new Date('2023-02-20'), value: 18846303.416569296 },
{ date: new Date('2023-02-21'), value: 26366091.117737416 },
{ date: new Date('2023-02-22'), value: 30446292.68212635 },
{ date: new Date('2023-02-23'), value: 25136740.887217894 },
],
title: 'Native coin circulating supply',
description: 'Amount of token circulating supply for the period',
units: 'ETH',
isLoading: false,
isError: false,
};
test('base view +@dark-mode', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ChartWidget { ...props }/>
</TestApp>,
);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Nativecoincirculatingsupply-small"]')?.getAttribute('opacity') === '1';
});
await expect(component).toHaveScreenshot();
await component.locator('.chakra-menu__menu-button').click();
await expect(component).toHaveScreenshot();
await page.mouse.move(0, 0);
await page.mouse.click(0, 0);
await page.mouse.move(100, 150);
await expect(component).toHaveScreenshot();
await page.mouse.down();
await page.mouse.move(300, 150);
await page.mouse.up();
await expect(component).toHaveScreenshot();
});
test('loading', async({ mount }) => {
const component = await mount(
<TestApp>
<ChartWidget { ...props } isLoading/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('error', async({ mount }) => {
const component = await mount(
<TestApp>
<ChartWidget { ...props } isError/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
...@@ -33,10 +33,11 @@ import ChartWidgetGraph from './ChartWidgetGraph'; ...@@ -33,10 +33,11 @@ import ChartWidgetGraph from './ChartWidgetGraph';
import ChartWidgetSkeleton from './ChartWidgetSkeleton'; import ChartWidgetSkeleton from './ChartWidgetSkeleton';
import FullscreenChartModal from './FullscreenChartModal'; import FullscreenChartModal from './FullscreenChartModal';
type Props = { export type Props = {
items?: Array<TimeChartItem>; items?: Array<TimeChartItem>;
title: string; title: string;
description?: string; description?: string;
units?: string;
isLoading: boolean; isLoading: boolean;
className?: string; className?: string;
isError: boolean; isError: boolean;
...@@ -44,7 +45,7 @@ type Props = { ...@@ -44,7 +45,7 @@ type Props = {
const DOWNLOAD_IMAGE_SCALE = 5; const DOWNLOAD_IMAGE_SCALE = 5;
const ChartWidget = ({ items, title, description, isLoading, className, isError }: Props) => { const ChartWidget = ({ items, title, description, isLoading, className, isError, units }: Props) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [ isFullscreen, setIsFullscreen ] = useState(false); const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
...@@ -151,6 +152,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError ...@@ -151,6 +152,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError
onZoom={ handleZoom } onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial } isZoomResetInitial={ isZoomResetInitial }
title={ title } title={ title }
units={ units }
/> />
</Box> </Box>
); );
......
...@@ -20,16 +20,18 @@ import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize'; ...@@ -20,16 +20,18 @@ import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
interface Props { interface Props {
isEnlarged?: boolean; isEnlarged?: boolean;
title: string; title: string;
units?: string;
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
onZoom: () => void; onZoom: () => void;
isZoomResetInitial: boolean; isZoomResetInitial: boolean;
margin?: ChartMargin; margin?: ChartMargin;
} }
const MAX_SHOW_ITEMS = 100; // 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: 40, right: 20, top: 10 };
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin }: Props) => { const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin, units }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const color = useToken('colors', 'blue.200'); const color = useToken('colors', 'blue.200');
const overlayRef = React.useRef<SVGRectElement>(null); const overlayRef = React.useRef<SVGRectElement>(null);
...@@ -53,7 +55,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -53,7 +55,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
return rangedItems; return rangedItems;
} }
}, [ isGroupedValues, rangedItems ]); }, [ isGroupedValues, rangedItems ]);
const chartData = [ { items: displayedData, name: 'Value', color } ]; const chartData = React.useMemo(() => ([ { items: displayedData, name: 'Value', color, units } ]), [ color, displayedData, units ]);
const { xTickFormat, yTickFormat, xScale, yScale } = useTimeChartController({ const { xTickFormat, yTickFormat, xScale, yScale } = useTimeChartController({
data: [ { items: displayedData, name: title, color } ], data: [ { items: displayedData, name: title, color } ],
...@@ -121,7 +123,6 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -121,7 +123,6 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }> <ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<ChartTooltip <ChartTooltip
chartId={ chartId }
anchorEl={ overlayRef.current } anchorEl={ overlayRef.current }
width={ innerWidth } width={ innerWidth }
tooltipWidth={ isGroupedValues ? 280 : 200 } tooltipWidth={ isGroupedValues ? 280 : 200 }
......
...@@ -19,6 +19,7 @@ export interface ChartOffset { ...@@ -19,6 +19,7 @@ export interface ChartOffset {
export interface TimeChartDataItem { export interface TimeChartDataItem {
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
name: string; name: string;
units?: string;
color?: string; color?: string;
valueFormatter?: (value: number) => string; valueFormatter?: (value: number) => string;
} }
......
...@@ -11,6 +11,7 @@ type Props = { ...@@ -11,6 +11,7 @@ type Props = {
id: string; id: string;
title: string; title: string;
description: string; description: string;
units?: string;
interval: StatsIntervalIds; interval: StatsIntervalIds;
onLoadingError: () => void; onLoadingError: () => void;
} }
...@@ -19,7 +20,7 @@ function formatDate(date: Date) { ...@@ -19,7 +20,7 @@ function formatDate(date: Date) {
return date.toISOString().substring(0, 10); return date.toISOString().substring(0, 10);
} }
const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError }: Props) => { const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units }: Props) => {
const selectedInterval = STATS_INTERVALS[interval]; const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined; const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
...@@ -48,6 +49,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError ...@@ -48,6 +49,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
isError={ isError } isError={ isError }
items={ items } items={ items }
title={ title } title={ title }
units={ units }
description={ description } description={ description }
isLoading={ isLoading } isLoading={ isLoading }
/> />
......
...@@ -99,6 +99,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval } ...@@ -99,6 +99,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }
title={ chart.title } title={ chart.title }
description={ chart.description } description={ chart.description }
interval={ interval } interval={ interval }
units={ chart.units || undefined }
onLoadingError={ handleChartLoadingError } onLoadingError={ handleChartLoadingError }
/> />
</GridItem> </GridItem>
......
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