Skip to content

Commit

Permalink
[ML] Data Frame Analytics: Scatterplot Matrix Fixes (#86357)
Browse files Browse the repository at this point in the history
- use ml API service from the Kibana context
- adds jest tests for the Vega Lite Spec generator
- fix chart layout overflow with too many fields selected
  • Loading branch information
walterra authored Dec 21, 2020
1 parent 34803ed commit d744eae
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.mlScatterplotMatrix {
overflow-x: auto;

.vega-bind span {
font-size: $euiFontSizeXS;
padding: 0 $euiSizeXS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -72,6 +72,8 @@ export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
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<boolean>(false);
Expand Down Expand Up @@ -147,7 +149,7 @@ export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
}
: { match_all: {} };

const resp: SearchResponse7 = await ml.esSearch({
const resp: SearchResponse7 = await esSearch({
index,
body: {
fields: queryFields,
Expand Down
Original file line number Diff line number Diff line change
@@ -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' },
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit d744eae

Please sign in to comment.