mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
b686e0fa99
commit
8b66119431
4 changed files with 108 additions and 87 deletions
35
x-pack/plugins/profiling/common/topn.test.ts
Normal file
35
x-pack/plugins/profiling/common/topn.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue