[Stacktraces] Syncing color in the main chart and in the subchart (#152832)

closes https://github.com/elastic/prodfiler/issues/3069


https://user-images.githubusercontent.com/55978943/223480200-036f8d2c-5c08-48cc-a205-47ae1eb8e803.mov

I also refactored the components a bit and wrote a unit test.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-03-08 08:54:05 -05:00 committed by GitHub
parent b686e0fa99
commit 8b66119431
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 108 additions and 87 deletions

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { euiPaletteColorBlind } from '@elastic/eui';
import { getCategoryColor } from './topn';
describe('topn', () => {
describe('getCategoryColor', () => {
const categories = [
{ category: 'elasticsearch', expectedColor: '#D6BF57' },
{ category: 'metricbeat', expectedColor: '#B9A888' },
{ category: 'auditbeat', expectedColor: '#E7664C' },
{ category: 'dockerd', expectedColor: '#B9A888' },
{ category: 'Other', expectedColor: '#CA8EAE' },
{ category: 'node', expectedColor: '#D36086' },
{ category: 'filebeat', expectedColor: '#54B399' },
{ category: 'containerd', expectedColor: '#DA8B45' },
{ category: 'C2 CompilerThre', expectedColor: '#6092C0' },
{ category: '[metrics]>worke', expectedColor: '#D6BF57' },
];
const colors = euiPaletteColorBlind({
rotations: Math.ceil(categories.length / 10),
});
categories.map(({ category, expectedColor }) => {
it(`returns correct color for category ${category}`, () => {
expect(getCategoryColor({ category, subChartSize: categories.length, colors })).toEqual(
expectedColor
);
});
});
});
});

View file

@ -220,6 +220,31 @@ export interface TopNSubchart {
Metadata: StackFrameMetadata[];
}
export function getCategoryColor({
category,
subChartSize,
colors,
}: {
category: string;
subChartSize: number;
colors: ReturnType<typeof euiPaletteColorBlind>;
}) {
// We want the mapping from the category string to the color to be constant,
// so that the same category string will always map to the same color.
const stringhash = (s: string): number => {
let hash: number = 0;
for (let i = 0; i < s.length; i++) {
const ch = s.charCodeAt(i);
hash = (hash << 5) - hash + ch; // eslint-disable-line no-bitwise
// Apply bit mask to ensure positive value.
hash &= 0x7fffffff; // eslint-disable-line no-bitwise
}
return hash % subChartSize;
};
return colors[stringhash(category)];
}
export function groupSamplesByCategory({
samples,
totalCount,
@ -264,22 +289,10 @@ export function groupSamplesByCategory({
rotations: Math.ceil(subcharts.length / 10),
});
// We want the mapping from the category string to the color to be constant,
// so that the same category string will always map to the same color.
const stringhash = (s: string): number => {
let hash: number = 0;
for (let i = 0; i < s.length; i++) {
const ch = s.charCodeAt(i);
hash = (hash << 5) - hash + ch; // eslint-disable-line no-bitwise
hash &= hash; // eslint-disable-line no-bitwise
}
return hash % subcharts.length;
};
return orderBy(subcharts, ['Percentage', 'Category'], ['desc', 'asc']).map((chart, index) => {
return {
...chart,
Color: colors[stringhash(chart.Category)],
Color: getCategoryColor({ category: chart.Category, colors, subChartSize: subcharts.length }),
Index: index + 1,
Series: chart.Series.map((value) => {
return {

View file

@ -6,9 +6,9 @@
*/
import { EuiButton, EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import React from 'react';
import { StackTracesDisplayOption, TopNType } from '../../../common/stack_traces';
import { groupSamplesByCategory, TopNResponse, TopNSample } from '../../../common/topn';
import { groupSamplesByCategory, TopNResponse } from '../../../common/topn';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path';
@ -78,13 +78,6 @@ export function StackTracesView() {
[topNType, timeRange.start, timeRange.end, fetchTopN, kuery]
);
const [highlightedSample, setHighlightedSample] = useState<TopNSample | null>(null);
const highlightedSubchart =
(highlightedSample &&
state.data?.charts.find((chart) => chart.Category === highlightedSample?.Category)) ||
null;
const { data } = state;
return (
@ -146,14 +139,6 @@ export function StackTracesView() {
},
});
}}
onSampleOver={(sample) => {
setHighlightedSample(sample);
}}
onSampleOut={() => {
setHighlightedSample(null);
}}
highlightedSample={highlightedSample}
highlightedSubchart={highlightedSubchart}
showFrames={topNType === TopNType.Traces}
/>
</AsyncComponent>

View file

@ -15,61 +15,26 @@ import {
StackMode,
timeFormatter,
Tooltip,
TooltipInfo,
XYChartElementEvent,
CustomTooltip,
TooltipContainer,
TooltipInfo,
} from '@elastic/charts';
import { EuiPanel } from '@elastic/eui';
import React from 'react';
import { keyBy } from 'lodash';
import React, { useMemo, useState } from 'react';
import { TopNSample, TopNSubchart } from '../../common/topn';
import { useKibanaTimeZoneSetting } from '../hooks/use_kibana_timezone_setting';
import { useProfilingChartsTheme } from '../hooks/use_profiling_charts_theme';
import { asPercentage } from '../utils/formatters/as_percentage';
import { SubChart } from './subchart';
const SubchartTooltip = ({
highlightedSubchart,
highlightedSample,
showFrames,
}: TooltipInfo & {
highlightedSubchart: TopNSubchart;
highlightedSample: TopNSample | null;
showFrames: boolean;
}) => {
// max tooltip width - 2 * padding (16px)
const width = 224;
return (
<EuiPanel>
<SubChart
index={highlightedSubchart.Index}
color={highlightedSubchart.Color}
category={highlightedSubchart.Category}
label={highlightedSubchart.Label}
data={highlightedSubchart.Series}
percentage={highlightedSubchart.Percentage}
sample={highlightedSample}
showFrames={showFrames}
/* we don't show metadata in tooltips */
metadata={[]}
height={128}
width={width}
showAxes={false}
onShowMoreClick={null}
padTitle={false}
/>
</EuiPanel>
);
};
// 2 * padding (16px)
const MAX_TOOLTIP_WIDTH = 224;
export interface StackedBarChartProps {
height: number;
asPercentages: boolean;
onBrushEnd: (range: { rangeFrom: string; rangeTo: string }) => void;
onSampleOver: (sample: TopNSample) => void;
onSampleOut: () => void;
highlightedSample: TopNSample | null;
highlightedSubchart: TopNSubchart | null;
charts: TopNSubchart[];
showFrames: boolean;
}
@ -78,29 +43,52 @@ export const StackedBarChart: React.FC<StackedBarChartProps> = ({
height,
asPercentages,
onBrushEnd,
onSampleOut,
onSampleOver,
highlightedSample,
highlightedSubchart,
charts,
showFrames,
}) => {
const chartsbyCategoryMap = useMemo(() => {
return keyBy(charts, 'Category');
}, [charts]);
const timeZone = useKibanaTimeZoneSetting();
const [highlightedSample, setHighlightedSample] = useState<TopNSample | undefined>();
const { chartsBaseTheme, chartsTheme } = useProfilingChartsTheme();
const customTooltip: CustomTooltip = highlightedSubchart
? (props) => (
<TooltipContainer>
<SubchartTooltip
{...props}
highlightedSubchart={highlightedSubchart!}
highlightedSample={highlightedSample}
function CustomTooltipWithSubChart(props: TooltipInfo) {
if (!highlightedSample) {
return null;
}
const highlightedSubchart = chartsbyCategoryMap[highlightedSample.Category];
if (!highlightedSubchart) {
return null;
}
return (
<TooltipContainer>
<EuiPanel>
<SubChart
index={highlightedSubchart.Index}
color={highlightedSubchart.Color}
category={highlightedSubchart.Category}
label={highlightedSubchart.Label}
data={highlightedSubchart.Series}
percentage={highlightedSubchart.Percentage}
sample={highlightedSample}
showFrames={showFrames}
/* we don't show metadata in tooltips */
metadata={[]}
height={128}
width={MAX_TOOLTIP_WIDTH}
showAxes={false}
onShowMoreClick={null}
padTitle={false}
/>
</TooltipContainer>
)
: () => <></>;
</EuiPanel>
</TooltipContainer>
);
}
return (
<Chart size={{ height }}>
@ -117,13 +105,13 @@ export const StackedBarChart: React.FC<StackedBarChartProps> = ({
theme={chartsTheme}
onElementOver={(events) => {
const [value] = events[0] as XYChartElementEvent;
onSampleOver(value.datum as TopNSample);
setHighlightedSample(value.datum as TopNSample);
}}
onElementOut={() => {
onSampleOut();
setHighlightedSample(undefined);
}}
/>
<Tooltip customTooltip={customTooltip} />
<Tooltip customTooltip={CustomTooltipWithSubChart} />
{charts.map((chart) => (
<HistogramBarSeries
key={chart.Category}