Skip to content

Commit

Permalink
[7.x] [ML] Add annotation markers to time series brush area to indica…
Browse files Browse the repository at this point in the history
…te annotations exist outside of selected range (#81490) (#82632)

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
qn895 and kibanamachine authored Nov 4, 2020
1 parent 08037e2 commit ee42b4d
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class AnnotationsTableUI extends Component {
if (dataCounts.processed_record_count > 0) {
// Load annotations for the selected job.
ml.annotations
.getAnnotations({
.getAnnotations$({
jobIds: [job.job_id],
earliestMs: null,
latestMs: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jest.mock('../../../services/ml_api_service', () => {
return {
ml: {
annotations: {
getAnnotations: jest.fn().mockReturnValue(mockAnnotations$),
getAnnotations$: jest.fn().mockReturnValue(mockAnnotations$),
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval,

return new Promise((resolve) => {
ml.annotations
.getAnnotations({
.getAnnotations$({
jobIds,
earliestMs: timeRange.earliestMs,
latestMs: timeRange.latestMs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { http, http$ } from '../http_service';
import { basePath } from './index';

export const annotations = {
getAnnotations(obj: {
getAnnotations$(obj: {
jobIds: string[];
earliestMs: number;
latestMs: number;
Expand All @@ -30,6 +30,23 @@ export const annotations = {
});
},

getAnnotations(obj: {
jobIds: string[];
earliestMs: number | null;
latestMs: number | null;
maxAnnotations: number;
fields?: FieldToBucket[];
detectorIndex?: number;
entities?: any[];
}) {
const body = JSON.stringify(obj);
return http<GetAnnotationsResponse>({
path: `${basePath()}/annotations`,
method: 'POST',
body,
});
},

indexAnnotation(obj: Annotation) {
const body = JSON.stringify(obj);
return http<any>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,25 @@ $mlAnnotationRectDefaultFillOpacity: 0.05;
.mlAnnotationHidden {
display: none;
}

// context annotation marker
.mlContextAnnotationRect {
stroke: $euiColorFullShade;
stroke-width: $mlAnnotationBorderWidth;
stroke-opacity: $mlAnnotationRectDefaultStrokeOpacity;
transition: stroke-opacity $euiAnimSpeedFast;

fill: $euiColorFullShade;
fill-opacity: $mlAnnotationRectDefaultFillOpacity;
transition: fill-opacity $euiAnimSpeedFast;

shape-rendering: geometricPrecision;
}

.mlContextAnnotationRect-isBlur {
stroke-opacity: $mlAnnotationRectDefaultStrokeOpacity / 2;
transition: stroke-opacity $euiAnimSpeedFast;

fill-opacity: $mlAnnotationRectDefaultFillOpacity / 2;
transition: fill-opacity $euiAnimSpeedFast;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import d3 from 'd3';

import React from 'react';
import { Annotation } from '../../../../../common/types/annotations';
import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs';
import { ChartTooltipService } from '../../../components/chart_tooltip';
Expand All @@ -19,6 +20,33 @@ interface State {
annotation: Annotation | null;
}

export interface TimeseriesChart extends React.Component<Props, State> {
interface TimeseriesChartProps {
annotation: object;
autoZoomDuration: number;
bounds: object;
contextAggregationInterval: object;
contextChartData: any[];
contextForecastData: any[];
contextChartSelected: any;
detectorIndex: number;
focusAggregationInterval: object;
focusAnnotationData: Annotation[];
focusChartData: any[];
focusForecastData: any[];
modelPlotEnabled: boolean;
renderFocusChartOnly: boolean;
selectedJob: CombinedJob;
showForecast: boolean;
showModelBounds: boolean;
svgWidth: number;
swimlaneData: any[];
zoomFrom: object;
zoomTo: object;
zoomFromFocusLoaded: object;
zoomToFocusLoaded: object;
tooltipService: object;
}

declare class TimeseriesChart extends React.Component<Props, any> {
focusXScale: d3.scale.Ordinal<{}, number>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { annotation$ } from '../../../services/annotations_service';
import { formatValue } from '../../../formatters/format_value';
import {
LINE_CHART_ANOMALY_RADIUS,
ANNOTATION_SYMBOL_HEIGHT,
MULTI_BUCKET_SYMBOL_SIZE,
SCHEDULED_EVENT_SYMBOL_HEIGHT,
drawLineChartDots,
Expand All @@ -48,6 +49,7 @@ import {
renderAnnotations,
highlightFocusChartAnnotation,
unhighlightFocusChartAnnotation,
ANNOTATION_MIN_WIDTH,
} from './timeseries_chart_annotations';

const focusZoomPanelHeight = 25;
Expand All @@ -57,6 +59,8 @@ const contextChartHeight = 60;
const contextChartLineTopMargin = 3;
const chartSpacing = 25;
const swimlaneHeight = 30;
const ctxAnnotationMargin = 2;
const annotationHeight = ANNOTATION_SYMBOL_HEIGHT + ctxAnnotationMargin * 2;
const margin = { top: 10, right: 10, bottom: 15, left: 40 };

const ZOOM_INTERVAL_OPTIONS = [
Expand All @@ -80,9 +84,16 @@ const anomalyGrayScale = d3.scale
.domain([3, 25, 50, 75, 100])
.range(['#dce7ed', '#b0c5d6', '#b1a34e', '#b17f4e', '#c88686']);

function getSvgHeight() {
function getSvgHeight(showAnnotations) {
const adjustedAnnotationHeight = showAnnotations ? annotationHeight : 0;
return (
focusHeight + contextChartHeight + swimlaneHeight + chartSpacing + margin.top + margin.bottom
focusHeight +
contextChartHeight +
swimlaneHeight +
adjustedAnnotationHeight +
chartSpacing +
margin.top +
margin.bottom
);
}

Expand Down Expand Up @@ -225,7 +236,12 @@ class TimeseriesChartIntl extends Component {
}

componentDidUpdate(prevProps) {
if (this.props.renderFocusChartOnly === false || prevProps.svgWidth !== this.props.svgWidth) {
if (
this.props.renderFocusChartOnly === false ||
prevProps.svgWidth !== this.props.svgWidth ||
prevProps.showAnnotations !== this.props.showAnnotations ||
prevProps.annotationData !== this.props.annotationData
) {
this.renderChart();
this.drawContextChartSelection();
}
Expand All @@ -246,6 +262,7 @@ class TimeseriesChartIntl extends Component {
modelPlotEnabled,
selectedJob,
svgWidth,
showAnnotations,
} = this.props;

const createFocusChart = this.createFocusChart.bind(this);
Expand All @@ -254,7 +271,7 @@ class TimeseriesChartIntl extends Component {
const focusYAxis = this.focusYAxis;
const focusYScale = this.focusYScale;

const svgHeight = getSvgHeight();
const svgHeight = getSvgHeight(showAnnotations);

// Clear any existing elements from the visualization,
// then build the svg elements for the bubble chart.
Expand Down Expand Up @@ -367,7 +384,13 @@ class TimeseriesChartIntl extends Component {

// Draw each of the component elements.
createFocusChart(focus, this.vizWidth, focusHeight);
drawContextElements(context, this.vizWidth, contextChartHeight, swimlaneHeight);
drawContextElements(
context,
this.vizWidth,
contextChartHeight,
swimlaneHeight,
annotationHeight
);
}

contextChartInitialized = false;
Expand Down Expand Up @@ -947,10 +970,19 @@ class TimeseriesChartIntl extends Component {
}

drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) {
const { bounds, contextChartData, contextForecastData, modelPlotEnabled } = this.props;

const {
bounds,
contextChartData,
contextForecastData,
modelPlotEnabled,
annotationData,
showAnnotations,
} = this.props;
const data = contextChartData;

const showFocusChartTooltip = this.showFocusChartTooltip.bind(this);
const hideFocusChartTooltip = this.props.tooltipService.hide.bind(this.props.tooltipService);

this.contextXScale = d3.time
.scale()
.range([0, cxtWidth])
Expand Down Expand Up @@ -997,20 +1029,26 @@ class TimeseriesChartIntl extends Component {
.domain([chartLimits.min, chartLimits.max]);

const borders = cxtGroup.append('g').attr('class', 'axis');
const brushChartHeight = showAnnotations
? cxtChartHeight + swlHeight + annotationHeight
: cxtChartHeight + swlHeight;

// Add borders left and right.
borders.append('line').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', brushChartHeight);
borders
.append('line')
.attr('x1', 0)
.attr('x1', cxtWidth)
.attr('y1', 0)
.attr('x2', 0)
.attr('y2', cxtChartHeight + swlHeight);
.attr('x2', cxtWidth)
.attr('y2', brushChartHeight);

// Add bottom borders
borders
.append('line')
.attr('x1', cxtWidth)
.attr('y1', 0)
.attr('x1', 0)
.attr('y1', brushChartHeight)
.attr('x2', cxtWidth)
.attr('y2', cxtChartHeight + swlHeight);
.attr('y2', brushChartHeight);

// Add x axis.
const timeBuckets = getTimeBucketsFromCache();
Expand Down Expand Up @@ -1065,6 +1103,61 @@ class TimeseriesChartIntl extends Component {
cxtGroup.append('path').datum(data).attr('class', 'values-line').attr('d', contextValuesLine);
drawLineChartDots(data, cxtGroup, contextValuesLine, 1);

// Add annotation markers to the context area
cxtGroup.append('g').classed('mlContextAnnotations', true);

const [contextXRangeStart, contextXRangeEnd] = this.contextXScale.range();
const ctxAnnotations = cxtGroup
.select('.mlContextAnnotations')
.selectAll('g.mlContextAnnotation')
.data(showAnnotations && annotationData ? annotationData : [], (d) => d._id || '');

ctxAnnotations.enter().append('g').classed('mlContextAnnotation', true);

const ctxAnnotationRects = ctxAnnotations
.selectAll('.mlContextAnnotationRect')
.data((d) => [d]);

ctxAnnotationRects
.enter()
.append('rect')
.attr('rx', ctxAnnotationMargin)
.attr('ry', ctxAnnotationMargin)
.on('mouseover', function (d) {
showFocusChartTooltip(d, this);
})
.on('mouseout', () => hideFocusChartTooltip())
.classed('mlContextAnnotationRect', true);

ctxAnnotationRects
.attr('x', (d) => {
const date = moment(d.timestamp);
let xPos = this.contextXScale(date);

if (xPos - ANNOTATION_SYMBOL_HEIGHT <= contextXRangeStart) {
xPos = 0;
}
if (xPos + ANNOTATION_SYMBOL_HEIGHT >= contextXRangeEnd) {
xPos = contextXRangeEnd - ANNOTATION_SYMBOL_HEIGHT;
}

return xPos;
})
.attr('y', cxtChartHeight + swlHeight + 2)
.attr('height', ANNOTATION_SYMBOL_HEIGHT)
.attr('width', (d) => {
const start = this.contextXScale(moment(d.timestamp)) + 1;
const end =
typeof d.end_timestamp !== 'undefined'
? this.contextXScale(moment(d.end_timestamp)) - 1
: start + ANNOTATION_MIN_WIDTH;
const width = Math.max(ANNOTATION_MIN_WIDTH, end - start);
return width;
});

ctxAnnotations.classed('mlAnnotationHidden', !showAnnotations);
ctxAnnotationRects.exit().remove();

// Create the path elements for the forecast value line and bounds area.
if (contextForecastData !== undefined) {
cxtGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const ANNOTATION_DEFAULT_LEVEL = 1;
const ANNOTATION_LEVEL_HEIGHT = 28;
const ANNOTATION_UPPER_RECT_MARGIN = 0;
const ANNOTATION_UPPER_TEXT_MARGIN = -7;
const ANNOTATION_MIN_WIDTH = 2;
export const ANNOTATION_MIN_WIDTH = 2;
const ANNOTATION_RECT_BORDER_RADIUS = 2;
const ANNOTATION_TEXT_VERTICAL_OFFSET = 26;
const ANNOTATION_TEXT_RECT_VERTICAL_OFFSET = 12;
Expand Down
Loading

0 comments on commit ee42b4d

Please sign in to comment.