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 = {
id: string;
title: string;
description: string;
units: string | null;
}
export type StatsChart = { chart: Array<StatsChartItem> };
......
......@@ -41,7 +41,7 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props
.x(({ date }) => xScale(date))
.y1(({ value }) => yScale(value))
.y0(() => yScale(yScale.domain()[0]))
.curve(d3.curveNatural);
.curve(d3.curveCatmullRom);
return area(data) || undefined;
}, [ xScale, yScale, data ]);
......
......@@ -62,7 +62,7 @@ const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => {
const line = d3.line<TimeChartItem>()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value))
.curve(d3.curveNatural);
.curve(d3.curveCatmullRom);
return (
<path
......
......@@ -9,7 +9,6 @@ import type { Pointer } from 'ui/shared/chart/utils/pointerTracker';
import { trackPointer } from 'ui/shared/chart/utils/pointerTracker';
interface Props {
chartId?: string;
width?: number;
tooltipWidth?: number;
height?: number;
......@@ -23,8 +22,9 @@ const TEXT_LINE_HEIGHT = 12;
const PADDING = 16;
const LINE_SPACE = 10;
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 titleColor = useToken('colors', 'blue.100');
const textColor = useToken('colors', 'white');
......@@ -78,10 +78,23 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da
);
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)
.text(data[i].valueFormatter?.(d.value) || d.value.toLocaleString());
}, [ data, chartId ]);
.text(
(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 xDate = xScale.invert(x);
......@@ -230,7 +243,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da
rx={ 12 }
ry={ 12 }
fill={ bgColor }
width={ tooltipWidth || 200 }
width={ tooltipWidth }
height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE }
/>
<g transform={ `translate(${ PADDING },${ PADDING })` }>
......@@ -246,7 +259,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da
</text>
<text
className="ChartTooltip__contentDate"
transform="translate(80,0)"
transform={ `translate(${ LABEL_WIDTH },0)` }
fontSize="12px"
fontWeight="500"
fill={ textColor }
......@@ -269,7 +282,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, da
{ name }
</text>
<text
transform="translate(80,0)"
transform={ `translate(${ LABEL_WIDTH },0)` }
className="ChartTooltip__value"
fontSize="12px"
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';
import ChartWidgetSkeleton from './ChartWidgetSkeleton';
import FullscreenChartModal from './FullscreenChartModal';
type Props = {
export type Props = {
items?: Array<TimeChartItem>;
title: string;
description?: string;
units?: string;
isLoading: boolean;
className?: string;
isError: boolean;
......@@ -44,7 +45,7 @@ type Props = {
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 [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
......@@ -151,6 +152,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
units={ units }
/>
</Box>
);
......
......@@ -20,16 +20,18 @@ import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
interface Props {
isEnlarged?: boolean;
title: string;
units?: string;
items: Array<TimeChartItem>;
onZoom: () => void;
isZoomResetInitial: boolean;
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 ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin }: Props) => {
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin, units }: Props) => {
const isMobile = useIsMobile();
const color = useToken('colors', 'blue.200');
const overlayRef = React.useRef<SVGRectElement>(null);
......@@ -53,7 +55,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
return 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({
data: [ { items: displayedData, name: title, color } ],
......@@ -121,7 +123,6 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<ChartTooltip
chartId={ chartId }
anchorEl={ overlayRef.current }
width={ innerWidth }
tooltipWidth={ isGroupedValues ? 280 : 200 }
......
......@@ -19,6 +19,7 @@ export interface ChartOffset {
export interface TimeChartDataItem {
items: Array<TimeChartItem>;
name: string;
units?: string;
color?: string;
valueFormatter?: (value: number) => string;
}
......
......@@ -11,6 +11,7 @@ type Props = {
id: string;
title: string;
description: string;
units?: string;
interval: StatsIntervalIds;
onLoadingError: () => void;
}
......@@ -19,7 +20,7 @@ function formatDate(date: Date) {
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 endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
......@@ -48,6 +49,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
isError={ isError }
items={ items }
title={ title }
units={ units }
description={ description }
isLoading={ isLoading }
/>
......
......@@ -99,6 +99,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }
title={ chart.title }
description={ chart.description }
interval={ interval }
units={ chart.units || undefined }
onLoadingError={ handleChartLoadingError }
/>
</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