Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Add annotation markers to time series brush area to indicate annotations exist outside of selected range #81490

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
697b60d
[ML] Remove redundant console.log
qn895 Oct 20, 2020
5f0d791
[ML] Rename old getAnnotation observable to getAnnotations$, add new …
qn895 Oct 20, 2020
ae65629
[ML] Change loadAnnotations to load separately from loadForJobID, rem…
qn895 Oct 21, 2020
ab4b600
[ML] Add annotation markers in time series brush area
qn895 Oct 21, 2020
5f904a9
[ML] Remove chart point value change
qn895 Oct 21, 2020
8148479
[ML] Add annotation markers to the bottom of context chart
qn895 Oct 21, 2020
1016d68
[ML] make annotations match the width in the focus area
qn895 Oct 22, 2020
e95be94
[ML] Remove duplicate scss stylings
qn895 Oct 22, 2020
bca6913
[ML] Remove redundant code
qn895 Oct 22, 2020
26cb1c8
Merge branch 'master' into ml-add-annotation-markers-in-context-brush…
kibanamachine Oct 27, 2020
aff3ab7
Merge remote-tracking branch 'upstream/master' into ml-add-annotation…
qn895 Oct 27, 2020
7945876
Merge remote-tracking branch 'upstream/master' into ml-add-annotation…
qn895 Oct 28, 2020
e84846f
[ML] Show tooltip
qn895 Oct 28, 2020
149ef85
[ML] Replace ctx annotation margin
qn895 Oct 28, 2020
3c6cd28
[ML] Add extra check to make annotationData safer
qn895 Oct 28, 2020
17c7903
Merge remote-tracking branch 'upstream/master' into ml-add-annotation…
qn895 Oct 28, 2020
f2253c0
Merge upstream/master into ml-add-annotation-markers-in-context-bru…
qn895 Nov 2, 2020
e12c721
[ML] Hide annotation strip
qn895 Nov 2, 2020
b55dacc
[ML] Change interface TimeseriesChart to class
qn895 Nov 2, 2020
af773a4
[ML] Fix i18n
qn895 Nov 2, 2020
d20aa19
Merge remote-tracking branch 'upstream/master' into ml-add-annotation…
qn895 Nov 3, 2020
3fb8c08
[ML] Fix toast notif show if no annotation data
qn895 Nov 3, 2020
d54048a
Merge remote-tracking branch 'upstream/master' into ml-add-annotation…
qn895 Nov 4, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -40,7 +40,6 @@ export class JobDetailsUI extends Component {
}

render() {
console.log('this.props', this.props);
const { job } = this.state;
const {
services: {
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, there are 1px $euiBorderWidthThin and 2px $euiBorderWidthThick variables, if you're interested in using them here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking it over 🙏 I think I'll keep mlAnnotationBorderWidth here since it's used for other parts as well but good to know we do have those variables available.

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