[Uptime] Scale waterfall chart to handle large number of requests (#88338)
This commit is contained in:
parent
eb4cb3d1dc
commit
7d14229519
|
@ -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;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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,11 +162,14 @@ 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}>
|
||||
{chartsToDisplay.map((chartData, ind) => (
|
||||
<WaterfallChartChartContainer
|
||||
height={getChartHeight(chartData, ind)}
|
||||
chartIndex={ind}
|
||||
key={ind}
|
||||
>
|
||||
<Chart className="data-chart">
|
||||
<Settings
|
||||
showLegend={false}
|
||||
|
@ -208,10 +199,11 @@ export const WaterfallChart = ({
|
|||
yAccessors={['y']}
|
||||
y0Accessors={['y0']}
|
||||
styleAccessor={barStyleAccessor}
|
||||
data={data}
|
||||
data={chartData}
|
||||
/>
|
||||
</Chart>
|
||||
</WaterfallChartChartContainer>
|
||||
))}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{shouldRenderLegend && <Legend items={legendItems!} render={renderLegendItem!} />}
|
||||
|
|
Loading…
Reference in a new issue