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

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Stacktraces] Syncing color in the main chart and in the subchart
(#152832)](https://github.com/elastic/kibana/pull/152832)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Cauê
Marcondes","email":"55978943+cauemarcondes@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-03-08T13:54:05Z","message":"[Stacktraces]
Syncing color in the main chart and in the subchart (#152832)\n\ncloses
223480200-036f8d2c-5c08-48cc-a205-47ae1eb8e803.mov\r\n\r\nI
also refactored the components a bit and wrote a unit
test.\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"8b6611943188777ef59d3eae032d79f0b3d91b01","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","v8.7.0","v8.8.0"],"number":152832,"url":"https://github.com/elastic/kibana/pull/152832","mergeCommit":{"message":"[Stacktraces]
Syncing color in the main chart and in the subchart (#152832)\n\ncloses
223480200-036f8d2c-5c08-48cc-a205-47ae1eb8e803.mov\r\n\r\nI
also refactored the components a bit and wrote a unit
test.\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"8b6611943188777ef59d3eae032d79f0b3d91b01"}},"sourceBranch":"main","suggestedTargetBranches":["8.7"],"targetPullRequestStates":[{"branch":"8.7","label":"v8.7.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/152832","number":152832,"mergeCommit":{"message":"[Stacktraces]
Syncing color in the main chart and in the subchart (#152832)\n\ncloses
223480200-036f8d2c-5c08-48cc-a205-47ae1eb8e803.mov\r\n\r\nI
also refactored the components a bit and wrote a unit
test.\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"8b6611943188777ef59d3eae032d79f0b3d91b01"}}]}]
BACKPORT-->

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-03-08 16:08:28 -05:00 committed by GitHub
parent 486711c257
commit 941c496a0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 84 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,59 +15,25 @@ import {
StackMode,
timeFormatter,
Tooltip,
TooltipInfo,
XYChartElementEvent,
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;
}
@ -76,27 +42,50 @@ 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 = highlightedSubchart
? (props: TooltipInfo) => (
<SubchartTooltip
{...props}
highlightedSubchart={highlightedSubchart!}
highlightedSample={highlightedSample}
function CustomTooltipWithSubChart(props: TooltipInfo) {
if (!highlightedSample) {
return null;
}
const highlightedSubchart = chartsbyCategoryMap[highlightedSample.Category];
if (!highlightedSubchart) {
return null;
}
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={MAX_TOOLTIP_WIDTH}
showAxes={false}
onShowMoreClick={null}
padTitle={false}
/>
)
: () => <></>;
</EuiPanel>
);
}
return (
<Chart size={{ height }}>
@ -113,13 +102,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}