diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index ea6f2a4a233e5..a857707ca0c75 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -16,6 +16,7 @@ import { niceTimeFormatter, Placement, Position, + RectAnnotation, ScaleType, Settings, YDomainRange, @@ -27,12 +28,13 @@ import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { useChartTheme } from '../../../../../observability/public'; import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; -import { TimeSeries } from '../../../../typings/timeseries'; +import { RectCoordinate, TimeSeries } from '../../../../typings/timeseries'; import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { useTheme } from '../../../hooks/useTheme'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useAnnotations } from '../../../hooks/use_annotations'; import { useChartPointerEvent } from '../../../hooks/use_chart_pointer_event'; +import { AnomalySeries } from '../../../selectors/chart_selectors'; import { unit } from '../../../style/variables'; import { ChartContainer } from './chart_container'; import { onBrushEnd } from './helper/helper'; @@ -53,6 +55,7 @@ interface Props { yTickFormat?: (y: number) => string; showAnnotations?: boolean; yDomain?: YDomainRange; + anomalySeries?: AnomalySeries; } export function TimeseriesChart({ @@ -65,6 +68,7 @@ export function TimeseriesChart({ yTickFormat, showAnnotations = true, yDomain, + anomalySeries, }: Props) { const history = useHistory(); const chartRef = React.createRef(); @@ -102,7 +106,12 @@ export function TimeseriesChart({ onBrushEnd({ x, history })} - theme={chartTheme} + theme={{ + ...chartTheme, + areaSeriesStyle: { + line: { visible: false }, + }, + }} onPointerUpdate={setPointerEvent} externalPointerEvents={{ tooltip: { visible: true, placement: Placement.Bottom }, @@ -169,6 +178,36 @@ export function TimeseriesChart({ /> ); })} + + {anomalySeries?.bounderies && ( + false} + /> + )} + + {anomalySeries?.scores && ( + ({ + coordinates: { x0, x1 }, + }) + )} + style={{ fill: anomalySeries.scores.color }} + /> + )} ); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index 3f8071ec39f0f..ac117bbbd922a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -46,7 +46,7 @@ export function TransactionCharts({ }: TransactionChartProps) { const { transactionType } = urlParams; - const { responseTimeSeries, tpmSeries } = charts; + const { responseTimeSeries, tpmSeries, anomalySeries } = charts; const { formatter, toggleSerie } = useFormatter(responseTimeSeries); @@ -79,6 +79,7 @@ export function TransactionCharts({ id="transactionDuration" timeseries={responseTimeSeries || []} yLabelFormat={getResponseTimeTickFormatter(formatter)} + anomalySeries={anomalySeries} onToggleLegend={(serie) => { if (serie) { toggleSerie(serie); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index 52b0470d31552..ee5cd8960d335 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -84,7 +84,7 @@ export function MLHeader({ hasValidMlLicense, mlJobId }: Props) { transactionType={transactionType} > {i18n.translate('xpack.apm.metrics.transactionChart.viewJob', { - defaultMessage: 'View Job:', + defaultMessage: 'View Job', })} diff --git a/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts b/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts index a17faebc9aefa..c9e6177f2c721 100644 --- a/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts +++ b/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts @@ -23,13 +23,10 @@ describe('chart selectors', () => { it('should return anomalyScoreSeries', () => { const data = [{ x0: 0, x: 10 }]; expect(getAnomalyScoreSeries(data)).toEqual({ - areaColor: 'rgba(231,102,76,0.1)', - color: 'none', + color: '#e7664c', data: [{ x0: 0, x: 10 }], - hideLegend: true, - hideTooltipValue: true, title: 'Anomaly score', - type: 'areaMaxHeight', + type: 'rectAnnotation', }); }); }); @@ -55,9 +52,7 @@ describe('chart selectors', () => { }; it('should produce correct series', () => { - expect( - getResponseTimeSeries({ apmTimeseries, anomalyTimeseries: undefined }) - ).toEqual([ + expect(getResponseTimeSeries({ apmTimeseries })).toEqual([ { color: '#6092c0', data: [ @@ -92,10 +87,7 @@ describe('chart selectors', () => { }); it('should return 3 series', () => { - expect( - getResponseTimeSeries({ apmTimeseries, anomalyTimeseries: undefined }) - .length - ).toBe(3); + expect(getResponseTimeSeries({ apmTimeseries }).length).toBe(3); }); }); diff --git a/x-pack/plugins/apm/public/selectors/chart_selectors.ts b/x-pack/plugins/apm/public/selectors/chart_selectors.ts index 663fbc9028108..2fdcaf9e4e675 100644 --- a/x-pack/plugins/apm/public/selectors/chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/chart_selectors.ts @@ -30,10 +30,16 @@ export interface ITpmBucket { color: string; } +export interface AnomalySeries { + scores: TimeSeries; + bounderies: TimeSeries; +} + export interface ITransactionChartData { tpmSeries?: ITpmBucket[]; responseTimeSeries?: TimeSeries[]; mlJobId: string | undefined; + anomalySeries?: AnomalySeries; } const INITIAL_DATA: Partial = { @@ -58,16 +64,35 @@ export function getTransactionCharts( transactionCharts.responseTimeSeries = getResponseTimeSeries({ apmTimeseries, + }); + + transactionCharts.anomalySeries = getResponseTimeAnnomalySeries({ anomalyTimeseries, }); } return transactionCharts; } +function getResponseTimeAnnomalySeries({ + anomalyTimeseries, +}: { + anomalyTimeseries: TimeSeriesAPIResponse['anomalyTimeseries']; +}): AnomalySeries | undefined { + if (anomalyTimeseries) { + return { + bounderies: getAnomalyBoundariesSeries( + anomalyTimeseries.anomalyBoundaries + ), + scores: getAnomalyScoreSeries(anomalyTimeseries.anomalyScore), + }; + } +} + export function getResponseTimeSeries({ apmTimeseries, - anomalyTimeseries, -}: TimeSeriesAPIResponse) { +}: { + apmTimeseries: TimeSeriesAPIResponse['apmTimeseries']; +}) { const { overallAvgDuration } = apmTimeseries; const { avg, p95, p99 } = apmTimeseries.responseTimes; @@ -107,16 +132,6 @@ export function getResponseTimeSeries({ }, ]; - if (anomalyTimeseries) { - // insert after Avg. series - series.splice( - 1, - 0, - getAnomalyBoundariesSeries(anomalyTimeseries.anomalyBoundaries), - getAnomalyScoreSeries(anomalyTimeseries.anomalyScore) - ); - } - return series; } @@ -125,12 +140,9 @@ export function getAnomalyScoreSeries(data: RectCoordinate[]) { title: i18n.translate('xpack.apm.transactions.chart.anomalyScoreLabel', { defaultMessage: 'Anomaly score', }), - hideLegend: true, - hideTooltipValue: true, data, - type: 'areaMaxHeight', - color: 'none', - areaColor: rgba(theme.euiColorVis9, 0.1), + type: 'rectAnnotation', + color: theme.euiColorVis9, }; } @@ -142,12 +154,9 @@ function getAnomalyBoundariesSeries(data: Coordinate[]) { defaultMessage: 'Anomaly Boundaries', } ), - hideLegend: true, - hideTooltipValue: true, data, type: 'area', - color: 'none', - areaColor: rgba(theme.euiColorVis1, 0.1), + color: rgba(theme.euiColorVis1, 0.5), }; }