Skip to content

Commit

Permalink
[ML][Embeddables Rebuild] Migrate Anomaly Chart (#183456)
Browse files Browse the repository at this point in the history
  • Loading branch information
qn895 authored May 27, 2024
1 parent 107075c commit d901467
Show file tree
Hide file tree
Showing 53 changed files with 1,346 additions and 1,627 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ import {
import type { TimeRangeBounds } from '@kbn/ml-time-buckets';
import { useTableSeverity } from '../components/controls/select_severity';
import type { JobId } from '../../../common/types/anomaly_detection_jobs';
import { getDefaultExplorerChartsPanelTitle } from '../../embeddables/anomaly_charts/anomaly_charts_embeddable';
import { MAX_ANOMALY_CHARTS_ALLOWED } from '../../embeddables/anomaly_charts/anomaly_charts_initializer';
import { useAnomalyExplorerContext } from './anomaly_explorer_context';
import { escapeKueryForEmbeddableFieldValuePair } from '../util/string_utils';
import { useCasesModal } from '../contexts/kibana/use_cases_modal';
import { DEFAULT_MAX_SERIES_TO_PLOT } from '../services/anomaly_explorer_charts_service';
import type { AnomalyChartsEmbeddableInput } from '../../embeddables';
import type { AnomalyChartsEmbeddableState } from '../../embeddables';
import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../embeddables';
import { useMlKibana } from '../contexts/kibana';
import type { AppStateSelectedCells, ExplorerJob } from './explorer_utils';
import { getSelectionInfluencers, getSelectionTimeRange } from './explorer_utils';
import { getDefaultExplorerChartsPanelTitle } from '../../embeddables/anomaly_charts/utils';

interface AnomalyContextMenuProps {
selectedJobs: ExplorerJob[];
Expand Down Expand Up @@ -133,7 +133,9 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
}, [chartsData.seriesToPlot, globalTimeRange, selectedCells, bounds, interval]);

const isMaxSeriesToPlotValid =
maxSeriesToPlot >= 1 && maxSeriesToPlot <= MAX_ANOMALY_CHARTS_ALLOWED;
typeof maxSeriesToPlot === 'number' &&
maxSeriesToPlot >= 1 &&
maxSeriesToPlot <= MAX_ANOMALY_CHARTS_ALLOWED;

const jobIds = selectedJobs.map(({ id }) => id);

Expand Down Expand Up @@ -180,7 +182,7 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
({ dashboardId, newTitle, newDescription }) => {
const stateTransfer = embeddable!.getStateTransfer();

const embeddableInput: Partial<AnomalyChartsEmbeddableInput> = {
const embeddableInput: Partial<AnomalyChartsEmbeddableState> = {
...getEmbeddableInput(),
title: newTitle,
description: newDescription,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*/

export const TRANSPARENT_BACKGROUND = 'rgba(0, 0, 0, 0)';
export const CHART_HEIGHT = 170;
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export const ExplorerAnomaliesContainer: FC<ExplorerAnomaliesContainerProps> = (
timeRange,
}) => {
return (
<>
// TODO: Remove data-shared-item and data-rendering-count as part of https://github.com/elastic/kibana/issues/179376
// These attributes are temporarily needed for reporting to not have any warning
<div data-shared-item="" data-rendering-count={1}>
<EuiFlexGroup id={id} direction="row" gutterSize="l" responsive={true}>
<EuiFlexItem grow={false}>
<SelectSeverityUI severity={severity} onChange={setSeverity} />
Expand Down Expand Up @@ -96,9 +98,10 @@ export const ExplorerAnomaliesContainer: FC<ExplorerAnomaliesContainerProps> = (
tooManyBucketsCalloutMsg,
showSelectedInterval,
chartsService,
id,
}}
/>
)}
</>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@ import {
numTicksForDateFormat,
removeLabelOverlap,
chartExtendedLimits,
LINE_CHART_ANOMALY_RADIUS,
} from '../../util/chart_utils';
import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator';

import { CHART_TYPE } from '../explorer_constants';
import { TRANSPARENT_BACKGROUND } from './constants';
import { CHART_HEIGHT, TRANSPARENT_BACKGROUND } from './constants';
import { filter } from 'rxjs';
import { drawCursor } from './utils/draw_anomaly_explorer_charts_cursor';

const CONTENT_WRAPPER_HEIGHT = 215;
const SCHEDULED_EVENT_MARKER_HEIGHT = 5;

// If a rare/event-distribution chart has a cardinality of 10 or less,
// then the chart will display the y axis labels for each lane of events.
Expand All @@ -54,10 +58,32 @@ export class ExplorerChartDistribution extends React.Component {
seriesConfig: PropTypes.object,
severity: PropTypes.number,
tooltipService: PropTypes.object.isRequired,
cursor$: PropTypes.object,
};

constructor(props) {
super(props);
this.chartScales = undefined;
this.cursorStateSubscription = undefined;
}
componentDidMount() {
this.renderChart();
this.cursorStateSubscription = this.props.cursor$
.pipe(filter((c) => c.isDateHistogram))
.subscribe((cursor) => {
drawCursor(
cursor.cursor,
this.rootNode,
this.props.id,
this.props.seriesConfig,
this.chartScales,
this.props.chartTheme
);
});
}

componentWillUnmount() {
this.cursorStateSubscription?.unsubscribe();
}

componentDidUpdate() {
Expand All @@ -71,8 +97,7 @@ export class ExplorerChartDistribution extends React.Component {
timeBuckets,
showSelectedInterval,
onPointerUpdate,
chartTheme,
cursor,
id: chartId,
} = this.props;

const element = this.rootNode;
Expand All @@ -90,10 +115,6 @@ export class ExplorerChartDistribution extends React.Component {
);

let vizWidth = 0;
const chartHeight = 170;
const LINE_CHART_ANOMALY_RADIUS = 7;
const SCHEDULED_EVENT_MARKER_HEIGHT = 5;

const chartType = getChartType(config);

// Left margin is adjusted later for longest y-axis label.
Expand Down Expand Up @@ -122,11 +143,12 @@ export class ExplorerChartDistribution extends React.Component {
chartElement.select('svg').remove();

const svgWidth = element.clientWidth;
const svgHeight = chartHeight + margin.top + margin.bottom;
const svgHeight = CHART_HEIGHT + margin.top + margin.bottom;

const svg = chartElement
.append('svg')
.classed('ml-explorer-chart-svg', true)
.attr('id', 'ml-explorer-chart-svg' + chartId)
.attr('width', svgWidth)
.attr('height', svgHeight);

Expand Down Expand Up @@ -168,15 +190,15 @@ export class ExplorerChartDistribution extends React.Component {

lineChartYScale = d3.scale
.linear()
.range([chartHeight, 0])
.range([CHART_HEIGHT, 0])
.domain([yScaleDomainMin < 0 ? yScaleDomainMin : 0, yScaleDomainMax])
.nice();
} else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) {
// avoid overflowing the border of the highlighted area
const rowMargin = 5;
lineChartYScale = d3.scale
.ordinal()
.rangePoints([rowMargin, chartHeight - rowMargin])
.rangePoints([rowMargin, CHART_HEIGHT - rowMargin])
.domain(scaleCategories);
} else {
throw new Error(`chartType '${chartType}' not supported`);
Expand Down Expand Up @@ -260,25 +282,25 @@ export class ExplorerChartDistribution extends React.Component {
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('height', chartHeight)
.attr('height', CHART_HEIGHT)
.attr('width', vizWidth)
.style('stroke', '#cccccc')
.style('fill', 'none')
.style('stroke-width', 1);

drawRareChartAxes();
drawRareChartHighlightedSpan();
drawSyncedCursorLine(lineChartGroup);
drawCursorListener(lineChartGroup);
drawRareChartDots(data, lineChartGroup, lineChartValuesLine);
drawRareChartMarkers(data);
}

function drawSyncedCursorLine(lineChartGroup) {
function drawCursorListener(lineChartGroup) {
lineChartGroup
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('height', chartHeight)
.attr('height', CHART_HEIGHT)
.attr('width', vizWidth)
.on('mouseout', function () {
onPointerUpdate({
Expand All @@ -293,46 +315,19 @@ export class ExplorerChartDistribution extends React.Component {
.on('mousemove', function () {
const mouse = d3.mouse(this);

onPointerUpdate({
chartId: 'ml-anomaly-chart-metric',
scale: 'time',
smHorizontalValue: null,
smVerticalValue: null,
type: 'Over',
unit: undefined,
x: moment(lineChartXScale.invert(mouse[0])).unix() * 1000,
});
if (onPointerUpdate) {
onPointerUpdate({
chartId: 'ml-anomaly-chart-metric',
scale: 'time',
smHorizontalValue: null,
smVerticalValue: null,
type: 'Over',
unit: undefined,
x: moment(lineChartXScale.invert(mouse[0])).unix() * 1000,
});
}
})
.style('fill', TRANSPARENT_BACKGROUND);

const cursorData =
cursor &&
cursor.type === 'Over' &&
cursor.x >= config.plotEarliest &&
cursor.x <= config.plotLatest
? [cursor.x]
: [];

const cursorMouseLine = lineChartGroup
.append('g')
.attr('class', 'ml-anomaly-chart-cursor')
.selectAll('.ml-anomaly-chart-cursor-line')
.data(cursorData);

cursorMouseLine
.enter()
.append('path')
.attr('class', 'ml-anomaly-chart-cursor-line')
.attr('d', (ts) => {
const xPosition = lineChartXScale(ts);
return `M${xPosition},${chartHeight} ${xPosition},0`;
})
// Use elastic chart's cursor line style if possible
.style('stroke', chartTheme.crosshair.line.stroke)
.style('stroke-width', `${chartTheme.crosshair.line.strokeWidth}px`)
.style('stroke-dasharray', chartTheme.crosshair.line.dash?.join(',') ?? '4,4');

cursorMouseLine.exit().remove();
}

function drawRareChartAxes() {
Expand All @@ -350,7 +345,7 @@ export class ExplorerChartDistribution extends React.Component {
.axis()
.scale(lineChartXScale)
.orient('bottom')
.innerTickSize(-chartHeight)
.innerTickSize(-CHART_HEIGHT)
.outerTickSize(0)
.tickPadding(10)
.tickFormat((d) => moment(d).format(xAxisTickFormat));
Expand Down Expand Up @@ -389,7 +384,7 @@ export class ExplorerChartDistribution extends React.Component {
const gAxis = axes
.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + chartHeight + ')')
.attr('transform', 'translate(0,' + CHART_HEIGHT + ')')
.call(xAxis);

axes.append('g').attr('class', 'y axis').call(yAxis);
Expand Down Expand Up @@ -450,7 +445,7 @@ export class ExplorerChartDistribution extends React.Component {
.attr('rx', 3)
.attr('ry', 3)
.attr('width', rectWidth - 4)
.attr('height', chartHeight - 4);
.attr('height', CHART_HEIGHT - 4);
}

function drawRareChartMarkers(data) {
Expand Down Expand Up @@ -635,6 +630,7 @@ export class ExplorerChartDistribution extends React.Component {
y: LINE_CHART_ANOMALY_RADIUS * 2,
});
}
this.chartScales = { lineChartXScale, margin };
}

shouldComponentUpdate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { chartData as mockChartData } from './__mocks__/mock_chart_data_rare';
import seriesConfig from './__mocks__/mock_series_config_rare.json';
import { BehaviorSubject } from 'rxjs';

import { mountWithIntl } from '@kbn/test-jest-helpers';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
Expand All @@ -20,9 +21,7 @@ const utilityProps = {
timeBuckets: timeBucketsMock,
chartTheme: kibanaContextMock.services.charts.theme.useChartsBaseTheme(),
onPointerUpdate: jest.fn(),
cursor: {
x: 10432423,
},
cursor$: new BehaviorSubject({ isDataHistorgram: true, cursor: { x: 10432423 } }),
};

describe('ExplorerChart', () => {
Expand Down
Loading

0 comments on commit d901467

Please sign in to comment.