[Uptime] Scale waterfall chart to handle large number of requests (#88338)

This commit is contained in:
Shahzad 2021-01-19 17:18:26 +01:00 committed by GitHub
parent eb4cb3d1dc
commit 7d14229519
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 63 deletions

View file

@ -13,3 +13,6 @@ export const SIDEBAR_GROW_SIZE = 2;
// Axis height
// NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here.
export const FIXED_AXIS_HEIGHT = 32;
// number of items to display in canvas, since canvas can only have limited size
export const CANVAS_MAX_ITEMS = 150;

View file

@ -7,6 +7,7 @@
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui';
import { FIXED_AXIS_HEIGHT } from './constants';
const OuterContainer = styled.div`
width: 100%;
@ -29,10 +30,12 @@ const FirstChunk = styled.span`
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
line-height: ${FIXED_AXIS_HEIGHT}px;
`;
const LastChunk = styled.span`
flex-shrink: 0;
line-height: ${FIXED_AXIS_HEIGHT}px;
`;
export const getChunks = (text: string) => {

View file

@ -6,7 +6,7 @@
import React from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { SIDEBAR_GROW_SIZE } from './constants';
import { FIXED_AXIS_HEIGHT, SIDEBAR_GROW_SIZE } from './constants';
import { IWaterfallContext } from '../context/waterfall_chart';
import {
WaterfallChartSidebarContainer,
@ -18,14 +18,13 @@ import { WaterfallChartProps } from './waterfall_chart';
interface SidebarProps {
items: Required<IWaterfallContext>['sidebarItems'];
height: number;
render: Required<WaterfallChartProps>['renderSidebarItem'];
}
export const Sidebar: React.FC<SidebarProps> = ({ items, height, render }) => {
export const Sidebar: React.FC<SidebarProps> = ({ items, render }) => {
return (
<EuiFlexItem grow={SIDEBAR_GROW_SIZE}>
<WaterfallChartSidebarContainer height={height}>
<WaterfallChartSidebarContainer height={items.length * FIXED_AXIS_HEIGHT}>
<WaterfallChartSidebarContainerInnerPanel paddingSize="none">
<WaterfallChartSidebarContainerFlexGroup
direction="column"

View file

@ -47,6 +47,7 @@ export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)`
export const WaterfallChartFixedAxisContainer = euiStyled.div`
height: ${FIXED_AXIS_HEIGHT}px;
z-index: ${(props) => props.theme.eui.euiZLevel4};
`;
interface WaterfallChartSidebarContainer {
@ -54,7 +55,7 @@ interface WaterfallChartSidebarContainer {
}
export const WaterfallChartSidebarContainer = euiStyled.div<WaterfallChartSidebarContainer>`
height: ${(props) => `${props.height - FIXED_AXIS_HEIGHT}px`};
height: ${(props) => `${props.height}px`};
overflow-y: hidden;
`;
@ -76,12 +77,14 @@ export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)`
interface WaterfallChartChartContainer {
height: number;
chartIndex: number;
}
export const WaterfallChartChartContainer = euiStyled.div<WaterfallChartChartContainer>`
width: 100%;
height: ${(props) => `${props.height}px`};
margin-top: -${FIXED_AXIS_HEIGHT}px;
height: ${(props) => `${props.height + FIXED_AXIS_HEIGHT - 4}px`};
margin-top: -${FIXED_AXIS_HEIGHT - 4}px;
z-index: ${(props) => Math.round(props.theme.eui.euiZLevel3 / (props.chartIndex + 1))};
`;
export const WaterfallChartLegendContainer = euiStyled.div`

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useBarCharts } from './use_bar_charts';
import { renderHook } from '@testing-library/react-hooks';
import { IWaterfallContext } from '../context/waterfall_chart';
import { CANVAS_MAX_ITEMS } from './constants';
const generateTestData = (): IWaterfallContext['data'] => {
const numberOfItems = 1000;
const data: IWaterfallContext['data'] = [];
const testItem = {
x: 0,
y0: 0,
y: 4.345000023022294,
config: {
colour: '#b9a888',
showTooltip: true,
tooltipProps: { value: 'Queued / Blocked: 4.345ms', colour: '#b9a888' },
},
};
for (let i = 0; i < numberOfItems; i++) {
data.push(
{
...testItem,
x: i,
},
{
...testItem,
x: i,
y0: 7,
y: 25,
}
);
}
return data;
};
describe('useBarChartsHooks', () => {
it('returns result as expected', () => {
const { result, rerender } = renderHook((props) => useBarCharts(props), {
initialProps: { data: [] as IWaterfallContext['data'] },
});
expect(result.current).toHaveLength(0);
const newData = generateTestData();
rerender({ data: newData });
// Thousands items will result in 7 Canvas
expect(result.current.length).toBe(7);
const firstChartItems = result.current[0];
const lastChartItems = result.current[4];
// first chart items last item should be x 199, since we only display 150 items
expect(firstChartItems[firstChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS - 1);
// since here are 5 charts, last chart first item should be x 800
expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4);
expect(lastChartItems[lastChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS * 5 - 1);
});
});

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState } from 'react';
import { IWaterfallContext } from '../context/waterfall_chart';
import { CANVAS_MAX_ITEMS } from './constants';
export interface UseBarHookProps {
data: IWaterfallContext['data'];
}
export const useBarCharts = ({ data = [] }: UseBarHookProps) => {
const [charts, setCharts] = useState<Array<IWaterfallContext['data']>>([]);
useEffect(() => {
if (data.length > 0) {
let chartIndex = 1;
const firstCanvasItems = data.filter((item) => item.x <= CANVAS_MAX_ITEMS);
const chartsN: Array<IWaterfallContext['data']> = [firstCanvasItems];
data.forEach((item) => {
// Subtract 1 to account for x value starting from 0
if (item.x === CANVAS_MAX_ITEMS * chartIndex && !chartsN[item.x / CANVAS_MAX_ITEMS]) {
chartsN.push([]);
chartIndex++;
}
chartsN[chartIndex - 1].push(item);
});
setCharts(chartsN);
}
}, [data]);
return charts;
};

View file

@ -33,9 +33,10 @@ import {
WaterfallChartTooltip,
} from './styles';
import { WaterfallData } from '../types';
import { BAR_HEIGHT, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE, FIXED_AXIS_HEIGHT } from './constants';
import { BAR_HEIGHT, CANVAS_MAX_ITEMS, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants';
import { Sidebar } from './sidebar';
import { Legend } from './legend';
import { useBarCharts } from './use_bar_charts';
const Tooltip = (tooltipInfo: TooltipInfo) => {
const { data, renderTooltipItem } = useWaterfallContext();
@ -69,20 +70,11 @@ export interface WaterfallChartProps {
fullHeight?: boolean;
}
const getUniqueBars = (data: WaterfallData) => {
return (data ?? []).reduce<Set<number>>((acc, item) => {
if (!acc.has(item.x)) {
acc.add(item.x);
return acc;
} else {
return acc;
}
}, new Set());
const getChartHeight = (data: WaterfallData, ind: number): number => {
// We get the last item x(number of bars) and adds 1 to cater for 0 index
return (data[data.length - 1]?.x + 1 - ind * CANVAS_MAX_ITEMS) * BAR_HEIGHT;
};
const getChartHeight = (data: WaterfallData): number =>
getUniqueBars(data).size * BAR_HEIGHT + FIXED_AXIS_HEIGHT;
export const WaterfallChart = ({
tickFormat,
domain,
@ -94,10 +86,6 @@ export const WaterfallChart = ({
}: WaterfallChartProps) => {
const { data, sidebarItems, legendItems } = useWaterfallContext();
const generatedHeight = useMemo(() => {
return getChartHeight(data);
}, [data]);
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
const theme = useMemo(() => {
@ -108,10 +96,8 @@ export const WaterfallChart = ({
const [height, setHeight] = useState<string>(maxHeight);
const shouldRenderSidebar =
sidebarItems && sidebarItems.length > 0 && renderSidebarItem ? true : false;
const shouldRenderLegend =
legendItems && legendItems.length > 0 && renderLegendItem ? true : false;
const shouldRenderSidebar = !!(sidebarItems && sidebarItems.length > 0 && renderSidebarItem);
const shouldRenderLegend = !!(legendItems && legendItems.length > 0 && renderLegendItem);
useEffect(() => {
if (fullHeight && chartWrapperDivRef.current) {
@ -120,6 +106,8 @@ export const WaterfallChart = ({
}
}, [chartWrapperDivRef, fullHeight]);
const chartsToDisplay = useBarCharts({ data });
return (
<WaterfallChartOuterContainer height={height}>
<>
@ -174,44 +162,48 @@ export const WaterfallChart = ({
style={{ paddingTop: '10px' }}
ref={chartWrapperDivRef}
>
{shouldRenderSidebar && (
<Sidebar items={sidebarItems!} height={generatedHeight} render={renderSidebarItem!} />
)}
{shouldRenderSidebar && <Sidebar items={sidebarItems!} render={renderSidebarItem!} />}
<EuiFlexItem grow={shouldRenderSidebar ? MAIN_GROW_SIZE : true}>
<WaterfallChartChartContainer height={generatedHeight}>
<Chart className="data-chart">
<Settings
showLegend={false}
rotation={90}
tooltip={{ customTooltip: Tooltip }}
theme={theme}
/>
{chartsToDisplay.map((chartData, ind) => (
<WaterfallChartChartContainer
height={getChartHeight(chartData, ind)}
chartIndex={ind}
key={ind}
>
<Chart className="data-chart">
<Settings
showLegend={false}
rotation={90}
tooltip={{ customTooltip: Tooltip }}
theme={theme}
/>
<Axis
id="time"
position={Position.Top}
tickFormat={tickFormat}
domain={domain}
showGridLines={true}
style={{
axisLine: {
visible: false,
},
}}
/>
<Axis
id="time"
position={Position.Top}
tickFormat={tickFormat}
domain={domain}
showGridLines={true}
style={{
axisLine: {
visible: false,
},
}}
/>
<BarSeries
id="waterfallItems"
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
y0Accessors={['y0']}
styleAccessor={barStyleAccessor}
data={data}
/>
</Chart>
</WaterfallChartChartContainer>
<BarSeries
id="waterfallItems"
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
y0Accessors={['y0']}
styleAccessor={barStyleAccessor}
data={chartData}
/>
</Chart>
</WaterfallChartChartContainer>
))}
</EuiFlexItem>
</EuiFlexGroup>
{shouldRenderLegend && <Legend items={legendItems!} render={renderLegendItem!} />}