From 7ade2c3aeb6f04d8b606d51861002d02785d3428 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 2 Oct 2020 18:11:49 +0200 Subject: [PATCH] [ML] update swimlane_container, add colors constant --- .../explorer/swimlane_container.tsx | 352 +++++++++--------- 1 file changed, 184 insertions(+), 168 deletions(-) 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 4cc8092120685..a5f0cfcd17e50 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiText, EuiLoadingChart, @@ -15,36 +15,38 @@ import { } from '@elastic/eui'; import { throttle } from 'lodash'; -import { Chart, Settings, Heatmap } from '@elastic/charts'; +import { + Chart, + Settings, + Heatmap, + HeatmapElementEvent, + HeatmapConfig, + ElementClickListener, +} from '@elastic/charts'; import moment from 'moment'; +import { HeatmapBrushEvent } from '@elastic/charts/dist/chart_types/heatmap/layout/types/config_types'; import { ExplorerSwimlaneProps } from './explorer_swimlane'; import { SwimLanePagination } from './swimlane_pagination'; import { ViewBySwimLaneData } from './explorer_utils'; -import { ANOMALY_THRESHOLD } from '../../../common'; +import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../common'; +import { DeepPartial } from '../../../common/types/common'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. */ const RESIZE_IGNORED_DIFF_PX = 20; const RESIZE_THROTTLE_TIME_MS = 500; +const CELL_HEIGHT = 30; +const LEGEND_HEIGHT = 70; export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData { return arg && arg.hasOwnProperty('cardinality'); } /** - * Anomaly swim lane container responsible for handling resizing, pagination and injecting - * tooltip service. - * - * @param children - * @param onResize - * @param perPage - * @param fromPage - * @param swimlaneLimit - * @param onPaginationChange - * @param props - * @constructor + * Anomaly swim lane container responsible for handling resizing, pagination and + * providing swim lane vis with required props. */ export const SwimlaneContainer: FC< Omit & { @@ -71,9 +73,8 @@ export const SwimlaneContainer: FC< }) => { const [chartWidth, setChartWidth] = useState(0); + // Holds the container height for previously fetched data const containerHeightRef = useRef(); - /** Amount of rows during the previous render */ - const prevRowsCount = useRef(); const resizeHandler = useCallback( throttle((e: { width: number; height: number }) => { @@ -100,173 +101,188 @@ export const SwimlaneContainer: FC< fromPage && perPage; - const CELL_HEIGHT = 30; - const rowsCount = props?.swimlaneData?.laneLabels?.length ?? 0; + const swimLanePoints = useMemo(() => { + return props.swimlaneData.points + .map((v) => ({ ...v, time: v.time * 1000 })) + .filter((v) => v.value > 0); + }, [props.swimlaneData.points]); + + const containerHeight = useMemo(() => { + // Persists container height during loading to prevent page from jumping + return isLoading ? containerHeightRef.current : rowsCount * CELL_HEIGHT + LEGEND_HEIGHT; + }, [isLoading, rowsCount]); + useEffect(() => { - // to prevent the visualization form jumping during loading - if (!isLoading || rowsCount !== prevRowsCount.current) { - containerHeightRef.current = rowsCount * CELL_HEIGHT + 58; - prevRowsCount.current = rowsCount; + if (!isLoading) { + containerHeightRef.current = containerHeight; } - }, [rowsCount, isLoading, prevRowsCount.current]); + }, [isLoading, containerHeight]); const highlightedData = props.selection ? { x: props.selection.times.map((v) => v * 1000), y: props.selection.lanes } : undefined; + const swimLaneConfig: DeepPartial = useMemo( + () => ({ + onBrushEnd: (e: HeatmapBrushEvent) => { + props.onCellsSelection({ + lanes: e.y as string[], + times: e.x.map((v) => (v as number) / 1000), + type: props.swimlaneType, + viewByFieldName: props.swimlaneData.fieldName, + }); + }, + grid: { + cellHeight: { + min: CELL_HEIGHT / 2, + max: CELL_HEIGHT, // 'fill', + }, + stroke: { + width: 1, + color: '#D3DAE6', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + label: { + visible: false, + }, + border: { + stroke: '#D3DAE6', + strokeWidth: 0, + }, + }, + yAxisLabel: { + visible: true, + width: 170, + // eui color subdued + fill: `#6a717d`, + padding: 8, + }, + xAxisLabel: { + // eui color subdued + fill: `#98A2B3`, + formatter: (v: number) => { + props.timeBuckets.setInterval(`${props.swimlaneData.interval}s`); + const a = props.timeBuckets.getScaledDateFormat(); + return moment(v).format(a); + }, + }, + }), + [props.swimlaneType, props.swimlaneData?.fieldName] + ); + + // @ts-ignore + const onElementClick: ElementClickListener = useCallback( + (e: HeatmapElementEvent[]) => { + const cell = e[0][0]; + const startTime = (cell.datum.x as number) / 1000; + const payload = { + lanes: [String(cell.datum.y)], + times: [startTime, startTime + props.swimlaneData.interval], + type: props.swimlaneType, + viewByFieldName: props.swimlaneData.fieldName, + }; + props.onCellsSelection(payload); + }, + [props.swimlaneType, props.swimlaneData?.fieldName, props.swimlaneData?.interval] + ); + + // A resize observer is required to compute the bucket span based on the chart width to fetch the data accordingly return ( - <> - - {(resizeRef) => ( - { - resizeRef(el); + + {(resizeRef) => ( + + - - {showSwimlane && !isLoading && ( - - { - const [[cell]] = e; - const payload = { - lanes: [cell.datum.y], - times: [cell.datum.x / 1000, cell.datum.x / 1000], - type: props.swimlaneType, - viewByFieldName: props.swimlaneData.fieldName, - }; - props.onCellsSelection(payload); - }} - showLegend={false} - legendPosition="top" - onBrushEnd={(e) => { - const payload = { - lanes: e.y, - times: e.x.map((v) => v / 1000), - type: props.swimlaneType, - viewByFieldName: props.swimlaneData.fieldName, - }; - props.onCellsSelection(payload); - }} - brushAxis="both" - xDomain={{ - min: props.swimlaneData.earliest * 1000, - max: props.swimlaneData.latest * 1000, - minInterval: props.swimlaneData.interval * 1000, - }} - /> - ({ ...v, time: v.time * 1000 })) - .filter((v) => v.value > 0)} - xAccessor={(d) => d.time} - yAccessor={(d) => { - return d.laneLabel; - }} - valueAccessor={(d) => { - return d.value; - }} - highlightedData={highlightedData} - valueFormatter={(d) => d.toFixed(0.2)} - xScaleType={'time'} - ySortPredicate="dataIndex" - config={{ - grid: { - cellHeight: { - min: CELL_HEIGHT, - max: CELL_HEIGHT, // 'fill', - }, - stroke: { - width: 1, - color: '#D3DAE6', - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 'fill', - label: { - visible: false, - }, - border: { - stroke: '#D3DAE6', - strokeWidth: 0, - }, - }, - yAxisLabel: { - visible: true, - width: 170, - // eui color subdued - fill: `#6a717d`, - padding: 8, - }, - xAxisLabel: { - // eui color subdued - fill: `#98A2B3`, - formatter: (v) => { - props.timeBuckets.setInterval(`${props.swimlaneData.interval}s`); - const a = props.timeBuckets.getScaledDateFormat(); - return moment(v).format(a); - }, - }, - }} - /> - - )} - - {isLoading && ( - - - - )} - {!isLoading && !showSwimlane && ( - {noDataWarning}} + {showSwimlane && !isLoading && ( + + - )} - + d.toFixed(0.2)} + xScaleType="time" + ySortPredicate="dataIndex" + config={swimLaneConfig} + /> + + )} - {isPaginationVisible && ( - - + - + )} - - )} - - + {!isLoading && !showSwimlane && ( + {noDataWarning}} + /> + )} + + + {isPaginationVisible && ( + + + + )} + + )} + ); };