diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index d20042ddd9443..6091ab22692af 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -38,6 +38,9 @@ import { NoOverallData } from './components/no_overall_data'; import { SeverityControl } from '../components/severity_control'; import { AnomalyTimelineHelpPopover } from './anomaly_timeline_help_popover'; import { isDefined } from '../../../common/types/guards'; +import { MlTooltipComponent } from '../components/chart_tooltip'; +import { SwimlaneAnnotationContainer } from './swimlane_annotation_container'; +import { AnomalyTimelineService } from '../services/anomaly_timeline_service'; function mapSwimlaneOptionsToEuiOptions(options: string[]) { return options.map((option) => ({ @@ -92,6 +95,7 @@ export const AnomalyTimeline: FC = React.memo( swimLaneSeverity, overallSwimlaneData, viewBySwimlaneData, + swimlaneContainerWidth, } = explorerState; const [severityUpdate, setSeverityUpdate] = useState(swimLaneSeverity); @@ -140,6 +144,18 @@ export const AnomalyTimeline: FC = React.memo( }; }, [selectedCells]); + const annotationXDomain = useMemo( + () => + AnomalyTimelineService.isOverallSwimlaneData(overallSwimlaneData) + ? { + min: overallSwimlaneData.earliest * 1000, + max: overallSwimlaneData.latest * 1000, + minInterval: overallSwimlaneData.interval * 1000, + } + : undefined, + [overallSwimlaneData] + ); + return ( <> @@ -259,6 +275,21 @@ export const AnomalyTimeline: FC = React.memo( + {annotationXDomain && Array.isArray(annotations) && annotations.length > 0 ? ( + <> + + {(tooltipService) => ( + + )} + + + + ) : null} = React.memo( /> - {viewBySwimlaneOptions.length > 0 && ( = const dimensions = canvasRef.current.getBoundingClientRect(); const startingXPos = Y_AXIS_LABEL_WIDTH + 2 * Y_AXIS_LABEL_PADDING; - const endingXPos = dimensions.width - 2 * Y_AXIS_LABEL_PADDING - 4; + const endingXPos = dimensions.width - X_AXIS_RIGHT_OVERFLOW; const svg = chartElement .append('svg') @@ -82,18 +83,23 @@ export const SwimlaneAnnotationContainer: FC = // Add annotation marker annotationsData.forEach((d) => { - const annotationWidth = d.end_timestamp - ? xScale(Math.min(d.end_timestamp, domain.max)) - - Math.max(xScale(d.timestamp), startingXPos) - : 0; - + const annotationWidth = Math.max( + d.end_timestamp + ? xScale(Math.min(d.end_timestamp, domain.max)) - + Math.max(xScale(d.timestamp), startingXPos) + : 0, + ANNOTATION_MIN_WIDTH + ); + + const xPos = d.timestamp >= domain.min ? xScale(d.timestamp) : startingXPos; svg .append('rect') .classed('mlAnnotationRect', true) - .attr('x', d.timestamp >= domain.min ? xScale(d.timestamp) : startingXPos) + // If annotation is at the end, prevent overflow by shifting it back + .attr('x', xPos + annotationWidth >= endingXPos ? endingXPos - annotationWidth : xPos) .attr('y', 0) .attr('height', ANNOTATION_CONTAINER_HEIGHT) - .attr('width', Math.max(annotationWidth, ANNOTATION_MIN_WIDTH)) + .attr('width', annotationWidth) .on('mouseover', function () { const startingTime = formatHumanReadableDateTimeSeconds(d.timestamp); const endingTime = diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 86ec2014c8339..c58ced33b277d 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -38,17 +38,17 @@ import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../common'; import { TimeBuckets as TimeBucketsClass } from '../util/time_buckets'; import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; import { mlEscape } from '../util/string_utils'; -import { FormattedTooltip, MlTooltipComponent } from '../components/chart_tooltip/chart_tooltip'; +import { FormattedTooltip } from '../components/chart_tooltip/chart_tooltip'; import { formatHumanReadableDateTime } from '../../../common/util/date_utils'; import './_explorer.scss'; import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; import { useUiSettings } from '../contexts/kibana'; import { - SwimlaneAnnotationContainer, Y_AXIS_LABEL_WIDTH, Y_AXIS_LABEL_PADDING, Y_AXIS_LABEL_FONT_COLOR, + X_AXIS_RIGHT_OVERFLOW, } from './swimlane_annotation_container'; import { AnnotationsTable } from '../../../common/types/annotations'; @@ -333,6 +333,8 @@ export const SwimlaneContainer: FC = ({ return moment(v).format(scaledDateFormat); }, fontSize: 12, + // Required to calculate where the swimlane ends + width: X_AXIS_RIGHT_OVERFLOW * 2, }, brushMask: { fill: isDarkTheme ? 'rgb(30,31,35,80%)' : 'rgb(247,247,247,50%)', @@ -480,21 +482,6 @@ export const SwimlaneContainer: FC = ({ )} - {swimlaneType === SWIMLANE_TYPE.OVERALL && - showSwimlane && - xDomain !== undefined && - !isLoading && ( - - {(tooltipService) => ( - - )} - - )} diff --git a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts index 6f2b5417eff5f..8585495c08778 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts @@ -27,6 +27,7 @@ import { OVERALL_LABEL, VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants import { MlResultsService } from './results_service'; import { EntityField } from '../../../common/util/anomaly_utils'; import { InfluencersFilterQuery } from '../../../common/types/es_client'; +import { isPopulatedObject } from '../../../common'; /** * Service for retrieving anomaly swim lanes data. @@ -49,6 +50,21 @@ export class AnomalyTimelineService { this.timeFilter.enableTimeRangeSelector(); } + public static isSwimlaneData(arg: unknown): arg is SwimlaneData { + return isPopulatedObject(arg, ['interval', 'points', 'laneLabels']); + } + + public static isOverallSwimlaneData(arg: unknown): arg is OverallSwimlaneData { + // Important to check if all laneLabels are 'Overall' + // because ViewBySwimLaneData also extends OverallSwimlaneData + return ( + this.isSwimlaneData(arg) && + isPopulatedObject(arg, ['earliest', 'latest']) && + arg.laneLabels.length === 1 && + arg.laneLabels[0] === OVERALL_LABEL + ); + } + public setTimeRange(timeRange: TimeRange) { this._customTimeRange = timeRange; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 73c5f58fb80db..c314fe259f7f9 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -1132,10 +1132,12 @@ class TimeseriesChartIntl extends Component { .attr('height', ANNOTATION_SYMBOL_HEIGHT) .attr('width', (d) => { const start = Math.max(this.contextXScale(moment(d.timestamp)) + 1, contextXRangeStart); - const end = + const end = Math.min( + contextXRangeEnd, typeof d.end_timestamp !== 'undefined' ? this.contextXScale(moment(d.end_timestamp)) - 1 - : start + ANNOTATION_MIN_WIDTH; + : start + ANNOTATION_MIN_WIDTH + ); const width = Math.max(ANNOTATION_MIN_WIDTH, end - start); return width; });