diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss index f59c27dccc742..6a4fbd97b3bdd 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss @@ -1,4 +1,6 @@ .mlScatterplotMatrix { + overflow-x: auto; + .vega-bind span { font-size: $euiFontSizeXS; padding: 0 $euiSizeXS; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index ad9f63b10ebdd..b1ee9afb17788 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -30,7 +30,7 @@ import { i18n } from '@kbn/i18n'; import type { SearchResponse7 } from '../../../../common/types/es_client'; -import { ml } from '../../services/ml_api_service'; +import { useMlApiContext } from '../../contexts/kibana'; import { getProcessedFields } from '../data_grid'; import { useCurrentEuiTheme } from '../color_range_legend'; @@ -72,6 +72,8 @@ export const ScatterplotMatrix: FC = ({ color, legendType, }) => { + const { esSearch } = useMlApiContext(); + // dynamicSize is optionally used for outlier charts where the scatterplot marks // are sized according to outlier_score const [dynamicSize, setDynamicSize] = useState(false); @@ -147,7 +149,7 @@ export const ScatterplotMatrix: FC = ({ } : { match_all: {} }; - const resp: SearchResponse7 = await ml.esSearch({ + const resp: SearchResponse7 = await esSearch({ index, body: { fields: queryFields, diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts new file mode 100644 index 0000000000000..dd467161ff489 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { compile } from 'vega-lite/build-es5/vega-lite'; + +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; + +import { + getColorSpec, + getScatterplotMatrixVegaLiteSpec, + COLOR_OUTLIER, + COLOR_RANGE_NOMINAL, + DEFAULT_COLOR, + LEGEND_TYPES, +} from './scatterplot_matrix_vega_lite_spec'; + +describe('getColorSpec()', () => { + it('should return the default color for non-outlier specs', () => { + const colorSpec = getColorSpec(euiThemeLight, false); + + expect(colorSpec).toEqual({ value: DEFAULT_COLOR }); + }); + + it('should return a conditional spec for outliers', () => { + const colorSpec = getColorSpec(euiThemeLight, true); + + expect(colorSpec).toEqual({ + condition: { + test: "(datum['outlier_score'] >= mlOutlierScoreThreshold.cutoff)", + value: COLOR_OUTLIER, + }, + value: euiThemeLight.euiColorMediumShade, + }); + }); + + it('should return a field based spec for non-outlier specs with legendType supplied', () => { + const colorName = 'the-color-field'; + + const colorSpec = getColorSpec(euiThemeLight, false, colorName, LEGEND_TYPES.NOMINAL); + + expect(colorSpec).toEqual({ + field: colorName, + scale: { + range: COLOR_RANGE_NOMINAL, + }, + type: 'nominal', + }); + }); +}); + +describe('getScatterplotMatrixVegaLiteSpec()', () => { + it('should return the default spec for non-outliers without a legend', () => { + const data = [{ x: 1, y: 1 }]; + + const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec(data, ['x', 'y'], euiThemeLight); + + // A valid Vega Lite spec shouldn't throw an error when compiled. + expect(() => compile(vegaLiteSpec)).not.toThrow(); + + expect(vegaLiteSpec.repeat).toEqual({ + column: ['x', 'y'], + row: ['y', 'x'], + }); + expect(vegaLiteSpec.spec.transform).toEqual([ + { as: 'x', calculate: "datum['x']" }, + { as: 'y', calculate: "datum['y']" }, + ]); + expect(vegaLiteSpec.spec.data.values).toEqual(data); + expect(vegaLiteSpec.spec.mark).toEqual({ + opacity: 0.75, + size: 8, + type: 'circle', + }); + expect(vegaLiteSpec.spec.encoding.color).toEqual({ value: DEFAULT_COLOR }); + expect(vegaLiteSpec.spec.encoding.tooltip).toEqual([ + { field: 'x', type: 'quantitative' }, + { field: 'y', type: 'quantitative' }, + ]); + }); + + it('should return the spec for outliers', () => { + const data = [{ x: 1, y: 1 }]; + + const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec(data, ['x', 'y'], euiThemeLight, 'ml'); + + // A valid Vega Lite spec shouldn't throw an error when compiled. + expect(() => compile(vegaLiteSpec)).not.toThrow(); + + expect(vegaLiteSpec.repeat).toEqual({ + column: ['x', 'y'], + row: ['y', 'x'], + }); + expect(vegaLiteSpec.spec.transform).toEqual([ + { as: 'x', calculate: "datum['x']" }, + { as: 'y', calculate: "datum['y']" }, + { + as: 'outlier_score', + calculate: "datum['ml.outlier_score']", + }, + ]); + expect(vegaLiteSpec.spec.data.values).toEqual(data); + expect(vegaLiteSpec.spec.mark).toEqual({ + opacity: 0.75, + size: 8, + type: 'circle', + }); + expect(vegaLiteSpec.spec.encoding.color).toEqual({ + condition: { + test: "(datum['outlier_score'] >= mlOutlierScoreThreshold.cutoff)", + value: COLOR_OUTLIER, + }, + value: euiThemeLight.euiColorMediumShade, + }); + expect(vegaLiteSpec.spec.encoding.tooltip).toEqual([ + { field: 'x', type: 'quantitative' }, + { field: 'y', type: 'quantitative' }, + { + field: 'outlier_score', + format: '.3f', + type: 'quantitative', + }, + ]); + }); + + it('should return the spec for classification', () => { + const data = [{ x: 1, y: 1 }]; + + const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec( + data, + ['x', 'y'], + euiThemeLight, + undefined, + 'the-color-field', + LEGEND_TYPES.NOMINAL + ); + + // A valid Vega Lite spec shouldn't throw an error when compiled. + expect(() => compile(vegaLiteSpec)).not.toThrow(); + + expect(vegaLiteSpec.repeat).toEqual({ + column: ['x', 'y'], + row: ['y', 'x'], + }); + expect(vegaLiteSpec.spec.transform).toEqual([ + { as: 'x', calculate: "datum['x']" }, + { as: 'y', calculate: "datum['y']" }, + ]); + expect(vegaLiteSpec.spec.data.values).toEqual(data); + expect(vegaLiteSpec.spec.mark).toEqual({ + opacity: 0.75, + size: 8, + type: 'circle', + }); + expect(vegaLiteSpec.spec.encoding.color).toEqual({ + field: 'the-color-field', + scale: { + range: COLOR_RANGE_NOMINAL, + }, + type: 'nominal', + }); + expect(vegaLiteSpec.spec.encoding.tooltip).toEqual([ + { field: 'x', type: 'quantitative' }, + { field: 'y', type: 'quantitative' }, + ]); + }); +}); diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts index ee93074d9e034..9e0834dd8b922 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts @@ -24,12 +24,12 @@ export const OUTLIER_SCORE_FIELD = 'outlier_score'; const SCATTERPLOT_SIZE = 125; -const DEFAULT_COLOR = euiPaletteColorBlind()[0]; -const COLOR_OUTLIER = euiPaletteNegative(2)[1]; -const COLOR_RANGE_NOMINAL = euiPaletteColorBlind({ rotations: 2 }); -const COLOR_RANGE_QUANTITATIVE = euiPalettePositive(5); +export const DEFAULT_COLOR = euiPaletteColorBlind()[0]; +export const COLOR_OUTLIER = euiPaletteNegative(2)[1]; +export const COLOR_RANGE_NOMINAL = euiPaletteColorBlind({ rotations: 2 }); +export const COLOR_RANGE_QUANTITATIVE = euiPalettePositive(5); -const getColorSpec = ( +export const getColorSpec = ( euiTheme: typeof euiThemeLight, outliers = true, color?: string, @@ -72,10 +72,13 @@ export const getScatterplotMatrixVegaLiteSpec = ( calculate: `datum['${column}']`, as: column, })); - transform.push({ - calculate: `datum['${resultsField}.${OUTLIER_SCORE_FIELD}']`, - as: OUTLIER_SCORE_FIELD, - }); + + if (resultsField !== undefined) { + transform.push({ + calculate: `datum['${resultsField}.${OUTLIER_SCORE_FIELD}']`, + as: OUTLIER_SCORE_FIELD, + }); + } return { $schema: 'https://vega.github.io/schema/vega-lite/v4.17.0.json',