Skip to content

Commit

Permalink
[7.x] [APM] Add version annotations to timeseries charts (#526… (#53486)
Browse files Browse the repository at this point in the history
* [APM] Add version annotations to timeseries charts

Closes #51426.

* Don't subdue 'Version' text in tooltip

* Optimize version queries

* Don't pass radius/color to indicator

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
dgieselaar and elasticmachine authored Dec 24, 2019
1 parent 2c6ed6d commit 2e97fff
Show file tree
Hide file tree
Showing 20 changed files with 639 additions and 47 deletions.
16 changes: 16 additions & 0 deletions x-pack/legacy/plugins/apm/common/annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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.
*/

export enum AnnotationType {
VERSION = 'version'
}

export interface Annotation {
type: AnnotationType;
id: string;
time: number;
text: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 React from 'react';
import { VerticalGridLines } from 'react-vis';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import {
EuiIcon,
EuiToolTip,
EuiFlexGroup,
EuiFlexItem,
EuiText
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Maybe } from '../../../../../typings/common';
import { Annotation } from '../../../../../common/annotations';
import { PlotValues, SharedPlot } from './plotUtils';
import { asAbsoluteDateTime } from '../../../../utils/formatters';

interface Props {
annotations: Annotation[];
plotValues: PlotValues;
width: number;
overlay: Maybe<HTMLElement>;
}

const style = {
stroke: theme.euiColorSecondary,
strokeDasharray: 'none'
};

export function AnnotationsPlot(props: Props) {
const { plotValues, annotations } = props;

const tickValues = annotations.map(annotation => annotation.time);

return (
<>
<SharedPlot plotValues={plotValues}>
<VerticalGridLines tickValues={tickValues} style={style} />
</SharedPlot>
{annotations.map(annotation => (
<div
key={annotation.id}
style={{
position: 'absolute',
left: plotValues.x(annotation.time) - 8,
top: -2
}}
>
<EuiToolTip
title={asAbsoluteDateTime(annotation.time, 'seconds')}
content={
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<EuiText>
{i18n.translate('xpack.apm.version', {
defaultMessage: 'Version'
})}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>{annotation.text}</EuiFlexItem>
</EuiFlexGroup>
}
>
<EuiIcon type="tag" color={theme.euiColorSecondary} />
</EuiToolTip>
</div>
))}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
truncate
} from '../../../../style/variables';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import { EuiIcon } from '@elastic/eui';

const Container = styled.div`
display: flex;
Expand Down Expand Up @@ -73,9 +75,12 @@ export default function Legends({
noHits,
series,
seriesEnabledState,
truncateLegends
truncateLegends,
hasAnnotations,
showAnnotations,
onAnnotationsToggle
}) {
if (noHits) {
if (noHits && !hasAnnotations) {
return null;
}

Expand Down Expand Up @@ -107,6 +112,30 @@ export default function Legends({
/>
);
})}
{hasAnnotations && (
<Legend
key="annotations"
onClick={() => {
if (onAnnotationsToggle) {
onAnnotationsToggle();
}
}}
text={
<LegendContent>
{i18n.translate('xpack.apm.serviceVersion', {
defaultMessage: 'Service version'
})}
</LegendContent>
}
indicator={() => (
<div style={{ marginRight: px(units.quarter) }}>
<EuiIcon type="tag" color={theme.euiColorSecondary} />
</div>
)}
disabled={!showAnnotations}
color={theme.euiColorSecondary}
/>
)}
<MoreSeries hiddenSeriesCount={hiddenSeriesCount} />
</Container>
);
Expand All @@ -118,5 +147,8 @@ Legends.propTypes = {
noHits: PropTypes.bool.isRequired,
series: PropTypes.array.isRequired,
seriesEnabledState: PropTypes.array.isRequired,
truncateLegends: PropTypes.bool.isRequired
truncateLegends: PropTypes.bool.isRequired,
hasAnnotations: PropTypes.bool,
showAnnotations: PropTypes.bool,
onAnnotationsToggle: PropTypes.func
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Legends from './Legends';
import StaticPlot from './StaticPlot';
import InteractivePlot from './InteractivePlot';
import VoronoiPlot from './VoronoiPlot';
import { AnnotationsPlot } from './AnnotationsPlot';
import { createSelector } from 'reselect';
import { getPlotValues } from './plotUtils';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
Expand All @@ -28,7 +29,8 @@ export class InnerCustomPlot extends PureComponent {
seriesEnabledState: [],
isDrawing: false,
selectionStart: null,
selectionEnd: null
selectionEnd: null,
showAnnotations: true
};

getEnabledSeries = createSelector(
Expand Down Expand Up @@ -122,7 +124,7 @@ export class InnerCustomPlot extends PureComponent {
}

render() {
const { series, truncateLegends, width } = this.props;
const { series, truncateLegends, width, annotations } = this.props;

if (!width) {
return null;
Expand Down Expand Up @@ -166,6 +168,14 @@ export class InnerCustomPlot extends PureComponent {
tickFormatX={this.props.tickFormatX}
/>

{this.state.showAnnotations && !isEmpty(annotations) && (
<AnnotationsPlot
plotValues={plotValues}
width={width}
annotations={annotations || []}
/>
)}

<InteractivePlot
plotValues={plotValues}
hoverX={this.props.hoverX}
Expand All @@ -192,6 +202,13 @@ export class InnerCustomPlot extends PureComponent {
hiddenSeriesCount={hiddenSeriesCount}
clickLegend={this.clickLegend}
seriesEnabledState={this.state.seriesEnabledState}
hasAnnotations={!isEmpty(annotations)}
showAnnotations={this.state.showAnnotations}
onAnnotationsToggle={() => {
this.setState(({ showAnnotations }) => ({
showAnnotations: !showAnnotations
}));
}}
/>
</Fragment>
);
Expand All @@ -209,7 +226,14 @@ InnerCustomPlot.propTypes = {
truncateLegends: PropTypes.bool,
width: PropTypes.number.isRequired,
height: PropTypes.number,
stackBy: PropTypes.string
stackBy: PropTypes.string,
annotations: PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string,
id: PropTypes.string,
firstSeen: PropTypes.number
})
)
};

InnerCustomPlot.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

// @ts-ignore
import * as plotUtils from './plotUtils';
import { TimeSeries, Coordinate } from '../../../../../typings/timeseries';

describe('plotUtils', () => {
describe('getPlotValues', () => {
Expand Down Expand Up @@ -34,7 +35,10 @@ describe('plotUtils', () => {
expect(
plotUtils
.getPlotValues(
[{ data: { x: 0, y: 200 } }, { data: { x: 0, y: 300 } }],
[
{ data: [{ x: 0, y: 200 }] },
{ data: [{ x: 0, y: 300 }] }
] as Array<TimeSeries<Coordinate>>,
[],
{
height: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import d3 from 'd3';
import PropTypes from 'prop-types';
import React from 'react';

import { TimeSeries, Coordinate } from '../../../../../typings/timeseries';
import { unit } from '../../../../style/variables';
import { getTimezoneOffsetInMs } from './getTimezoneOffsetInMs';

Expand All @@ -22,20 +23,23 @@ const XY_MARGIN = {
bottom: unit * 2
};

const getXScale = (xMin, xMax, width) => {
const getXScale = (xMin: number, xMax: number, width: number) => {
return scaleLinear()
.domain([xMin, xMax])
.range([XY_MARGIN.left, width - XY_MARGIN.right]);
};

const getYScale = (yMin, yMax) => {
const getYScale = (yMin: number, yMax: number) => {
return scaleLinear()
.domain([yMin, yMax])
.range([XY_HEIGHT, 0])
.nice();
};

function getFlattenedCoordinates(visibleSeries, enabledSeries) {
function getFlattenedCoordinates(
visibleSeries: Array<TimeSeries<Coordinate>>,
enabledSeries: Array<TimeSeries<Coordinate>>
) {
const enabledCoordinates = flatten(enabledSeries.map(serie => serie.data));
if (!isEmpty(enabledCoordinates)) {
return enabledCoordinates;
Expand All @@ -44,10 +48,24 @@ function getFlattenedCoordinates(visibleSeries, enabledSeries) {
return flatten(visibleSeries.map(serie => serie.data));
}

export type PlotValues = ReturnType<typeof getPlotValues>;

export function getPlotValues(
visibleSeries,
enabledSeries,
{ width, yMin = 0, yMax = 'max', height, stackBy }
visibleSeries: Array<TimeSeries<Coordinate>>,
enabledSeries: Array<TimeSeries<Coordinate>>,
{
width,
yMin = 0,
yMax = 'max',
height,
stackBy
}: {
width: number;
yMin?: number | 'min';
yMax?: number | 'max';
height: number;
stackBy?: 'x' | 'y';
}
) {
const flattenedCoordinates = getFlattenedCoordinates(
visibleSeries,
Expand All @@ -59,10 +77,10 @@ export function getPlotValues(
const xMax = d3.max(flattenedCoordinates, d => d.x);

if (yMax === 'max') {
yMax = d3.max(flattenedCoordinates, d => d.y);
yMax = d3.max(flattenedCoordinates, d => d.y ?? 0);
}
if (yMin === 'min') {
yMin = d3.min(flattenedCoordinates, d => d.y);
yMin = d3.min(flattenedCoordinates, d => d.y ?? 0);
}

const [xMinZone, xMaxZone] = [xMin, xMax].map(x => {
Expand Down Expand Up @@ -101,11 +119,19 @@ export function getPlotValues(
};
}

export function SharedPlot({ plotValues, ...props }) {
export function SharedPlot({
plotValues,
...props
}: {
plotValues: PlotValues;
children: React.ReactNode;
}) {
const { XY_HEIGHT: height, XY_MARGIN: margin, XY_WIDTH: width } = plotValues;

return (
<div style={{ position: 'absolute', top: 0, left: 0 }}>
<div
style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}
>
<XYPlot
dontCheckIfEmpty
height={height}
Expand Down
Loading

0 comments on commit 2e97fff

Please sign in to comment.