"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.ScatterplotMatrix = void 0; var _react = _interopRequireWildcard(require("react")); var _eui = require("@elastic/eui"); var _rison = _interopRequireDefault(require("@kbn/rison")); var _i18n = require("@kbn/i18n"); var _mlStringHash = require("@kbn/ml-string-hash"); var _mlErrorUtils = require("@kbn/ml-error-utils"); var _mlRuntimeFieldUtils = require("@kbn/ml-runtime-field-utils"); var _mlDataGrid = require("@kbn/ml-data-grid"); var _kibana = require("../../contexts/kibana"); var _vega_chart = require("../vega_chart"); var _vega_chart_loading = require("../vega_chart/vega_chart_loading"); var _scatterplot_matrix_vega_lite_spec = require("./scatterplot_matrix_vega_lite_spec"); require("./scatterplot_matrix.scss"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ // Separate imports for lazy loadable VegaChart and related code const SCATTERPLOT_MATRIX_DEFAULT_FIELDS = 4; const SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE = 1000; const SCATTERPLOT_MATRIX_DEFAULT_FETCH_MIN_SIZE = 1; const SCATTERPLOT_MATRIX_DEFAULT_FETCH_MAX_SIZE = 10000; const TOGGLE_ON = _i18n.i18n.translate('xpack.ml.splom.toggleOn', { defaultMessage: 'On' }); const TOGGLE_OFF = _i18n.i18n.translate('xpack.ml.splom.toggleOff', { defaultMessage: 'Off' }); const sampleSizeOptions = [100, 1000, 10000].map(d => ({ value: d, text: '' + d })); const OptionLabelWithIconTip = ({ label, tooltip }) => /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, label, /*#__PURE__*/_react.default.createElement(_eui.EuiIconTip, { content: tooltip, iconProps: { className: 'eui-alignTop' }, size: "s" })); function filterChartableItems(items, resultsField) { var _items$map$filter; return (_items$map$filter = items.map(d => { var _d$fields; return (0, _mlDataGrid.getProcessedFields)((_d$fields = d.fields) !== null && _d$fields !== void 0 ? _d$fields : {}, key => key.startsWith(`${resultsField}.feature_importance`)); }).filter(d => !Object.keys(d).some(field => Array.isArray(d[field])))) !== null && _items$map$filter !== void 0 ? _items$map$filter : []; } const ScatterplotMatrix = ({ fields: allFields, index, resultsField, color, legendType, searchQuery, runtimeMappings, indexPattern, query }) => { const { esSearch } = (0, _kibana.useMlApiContext)(); const kibana = (0, _kibana.useMlKibana)(); const { services: { application, data } } = kibana; // dynamicSize is optionally used for outlier charts where the scatterplot marks // are sized according to outlier_score const [dynamicSize, setDynamicSize] = (0, _react.useState)(false); // used to give the user the option to customize the fields used for the matrix axes const [fields, setFields] = (0, _react.useState)([]); (0, _react.useEffect)(() => { const defaultFields = allFields.length > SCATTERPLOT_MATRIX_DEFAULT_FIELDS ? allFields.slice(0, SCATTERPLOT_MATRIX_DEFAULT_FIELDS) : allFields; setFields(defaultFields); }, [allFields]); // the amount of documents to be fetched const [fetchSize, setFetchSize] = (0, _react.useState)(SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE); // flag to add a random score to the ES query to fetch documents const [randomizeQuery, setRandomizeQuery] = (0, _react.useState)(false); const [isLoading, setIsLoading] = (0, _react.useState)(false); // contains the fetched documents and columns to be passed on to the Vega spec. const [splom, setSplom] = (0, _react.useState)(); const { euiTheme } = (0, _kibana.useCurrentThemeVars)(); // formats the array of field names for EuiComboBox const fieldOptions = (0, _react.useMemo)(() => allFields.map(d => ({ label: d })), [allFields]); const fieldsOnChange = newFields => { setFields(newFields.map(d => d.label)); }; const fetchSizeOnChange = e => { setFetchSize(Math.min(Math.max(parseInt(e.target.value, 10), SCATTERPLOT_MATRIX_DEFAULT_FETCH_MIN_SIZE), SCATTERPLOT_MATRIX_DEFAULT_FETCH_MAX_SIZE)); }; const randomizeQueryOnChange = () => { setRandomizeQuery(!randomizeQuery); }; const dynamicSizeOnChange = () => { setDynamicSize(!dynamicSize); }; const getCustomVisualizationLink = (0, _react.useCallback)(() => { const { columns } = splom; const outlierScoreField = resultsField !== undefined ? `${resultsField}.${_scatterplot_matrix_vega_lite_spec.OUTLIER_SCORE_FIELD}` : undefined; const vegaSpec = (0, _scatterplot_matrix_vega_lite_spec.getScatterplotMatrixVegaLiteSpec)(true, [], [], columns, euiTheme, resultsField, color, legendType, dynamicSize); vegaSpec.$schema = 'https://vega.github.io/schema/vega-lite/v5.json'; vegaSpec.title = `Scatterplot matrix for ${index}`; const fieldsToFetch = [...columns, // Add outlier_score field in fetch if it's available so custom visualization can use it ...(outlierScoreField ? [outlierScoreField] : []), // Add field to color code by in fetch so custom visualization can use it - usually for classfication jobs ...(color ? [color] : [])]; vegaSpec.data = { url: { '%context%': true, ...(indexPattern !== null && indexPattern !== void 0 && indexPattern.timeFieldName ? { ['%timefield%']: `${indexPattern === null || indexPattern === void 0 ? void 0 : indexPattern.timeFieldName}` } : {}), index, body: { fields: fieldsToFetch, size: fetchSize, _source: false } }, format: { property: 'hits.hits' } }; const globalState = encodeURIComponent(_rison.default.encode({ filters: data.query.filterManager.getFilters(), refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), time: data.query.timefilter.timefilter.getTime() })); const appState = encodeURIComponent(_rison.default.encode({ filters: [], linked: false, query, uiState: {}, vis: { aggs: [], params: { spec: JSON.stringify(vegaSpec, null, 2) } } })); const basePath = `/create?type=vega&_g=${globalState}&_a=${appState}`; return { path: basePath }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [splom]); (0, _react.useEffect)(() => { if (fields.length === 0) { setSplom({ columns: [], items: [], backgroundItems: [], messages: [] }); setIsLoading(false); return; } async function fetchSplom(options) { setIsLoading(true); const messages = []; try { const outlierScoreField = `${resultsField}.${_scatterplot_matrix_vega_lite_spec.OUTLIER_SCORE_FIELD}`; const includeOutlierScoreField = resultsField !== undefined; const queryFields = [...fields, ...(color !== undefined ? [color] : []), ...(includeOutlierScoreField ? [outlierScoreField] : [])]; const foregroundQuery = randomizeQuery ? { function_score: { query: searchQuery, random_score: { seed: 10, field: '_seq_no' } } } : searchQuery; let backgroundQuery; // If it's not the default query then we do a background search excluding the current query if (searchQuery && (searchQuery.match_all && Object.keys(searchQuery.match_all).length > 0 || searchQuery.bool && Object.keys(searchQuery.bool).length > 0)) { backgroundQuery = randomizeQuery ? { function_score: { query: { bool: { must_not: [searchQuery] } }, random_score: { seed: 10, field: '_seq_no' } } } : { bool: { must_not: [searchQuery] } }; } const combinedRuntimeMappings = indexPattern && (0, _mlRuntimeFieldUtils.getCombinedRuntimeMappings)(indexPattern, runtimeMappings); const body = { fields: queryFields, _source: false, query: foregroundQuery, from: 0, size: fetchSize, ...((0, _mlRuntimeFieldUtils.isRuntimeMappings)(combinedRuntimeMappings) ? { runtime_mappings: combinedRuntimeMappings } : {}) }; const promises = [esSearch({ index, body })]; if (backgroundQuery) { promises.push(esSearch({ index, body: { ...body, query: backgroundQuery } })); } const [foregroundResp, backgroundResp] = await Promise.all(promises); if (!options.didCancel) { var _backgroundResp$hits$; const items = filterChartableItems(foregroundResp.hits.hits, resultsField); const backgroundItems = filterChartableItems((_backgroundResp$hits$ = backgroundResp === null || backgroundResp === void 0 ? void 0 : backgroundResp.hits.hits) !== null && _backgroundResp$hits$ !== void 0 ? _backgroundResp$hits$ : [], resultsField); const originalDocsCount = foregroundResp.hits.hits.length; const filteredDocsCount = originalDocsCount - items.length; if (originalDocsCount === filteredDocsCount) { messages.push(_i18n.i18n.translate('xpack.ml.splom.allDocsFilteredWarningMessage', { defaultMessage: 'All fetched documents included fields with arrays of values and cannot be visualized.' })); } else if (foregroundResp.hits.hits.length !== items.length) { messages.push(_i18n.i18n.translate('xpack.ml.splom.arrayFieldsWarningMessage', { defaultMessage: '{filteredDocsCount} out of {originalDocsCount} fetched documents include fields with arrays of values and cannot be visualized.', values: { originalDocsCount, filteredDocsCount } })); } setSplom({ columns: fields, items, backgroundItems, messages }); setIsLoading(false); } } catch (e) { setIsLoading(false); setSplom({ columns: [], items: [], backgroundItems: [], messages: [(0, _mlErrorUtils.extractErrorMessage)(e)] }); } } const options = { didCancel: false }; fetchSplom(options); return () => { options.didCancel = true; }; // stringify the fields array and search, otherwise the comparator will trigger on new but identical instances. // eslint-disable-next-line react-hooks/exhaustive-deps }, [fetchSize, JSON.stringify({ fields, searchQuery }), index, randomizeQuery, resultsField]); const vegaSpec = (0, _react.useMemo)(() => { if (splom === undefined) { return; } const { items, backgroundItems, columns } = splom; return (0, _scatterplot_matrix_vega_lite_spec.getScatterplotMatrixVegaLiteSpec)(false, items, backgroundItems, columns, euiTheme, resultsField, color, legendType, dynamicSize); // eslint-disable-next-line react-hooks/exhaustive-deps }, [resultsField, splom, color, legendType, dynamicSize]); return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, splom === undefined || vegaSpec === undefined ? /*#__PURE__*/_react.default.createElement(_vega_chart_loading.VegaChartLoading, null) : /*#__PURE__*/_react.default.createElement("div", { "data-test-subj": `mlScatterplotMatrix ${isLoading ? 'loading' : 'loaded'}` }, /*#__PURE__*/_react.default.createElement(_eui.EuiFlexGroup, null, /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, null, /*#__PURE__*/_react.default.createElement(_eui.EuiFormRow, { label: /*#__PURE__*/_react.default.createElement(OptionLabelWithIconTip, { label: _i18n.i18n.translate('xpack.ml.splom.fieldSelectionLabel', { defaultMessage: 'Fields' }), tooltip: _i18n.i18n.translate('xpack.ml.splom.fieldSelectionInfoTooltip', { defaultMessage: 'Pick fields to explore their relationships.' }) }), display: "rowCompressed", fullWidth: true }, /*#__PURE__*/_react.default.createElement(_eui.EuiComboBox, { compressed: true, fullWidth: true, placeholder: _i18n.i18n.translate('xpack.ml.splom.fieldSelectionPlaceholder', { defaultMessage: 'Select fields' }), options: fieldOptions, selectedOptions: fields.map(d => ({ label: d })), onChange: fieldsOnChange, isClearable: true, "data-test-subj": "mlScatterplotMatrixFieldsComboBox" }))), /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { style: { width: '200px' }, grow: false }, /*#__PURE__*/_react.default.createElement(_eui.EuiFormRow, { label: /*#__PURE__*/_react.default.createElement(OptionLabelWithIconTip, { label: _i18n.i18n.translate('xpack.ml.splom.sampleSizeLabel', { defaultMessage: 'Sample size' }), tooltip: _i18n.i18n.translate('xpack.ml.splom.sampleSizeInfoTooltip', { defaultMessage: 'Amount of documents to display in the scatterplot matrix.' }) }), display: "rowCompressed", fullWidth: true }, /*#__PURE__*/_react.default.createElement(_eui.EuiSelect, { "data-test-subj": "mlScatterplotMatrixSampleSizeSelect", compressed: true, options: sampleSizeOptions, value: fetchSize, onChange: fetchSizeOnChange }))), /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { style: { width: '120px' }, grow: false }, /*#__PURE__*/_react.default.createElement(_eui.EuiFormRow, { label: /*#__PURE__*/_react.default.createElement(OptionLabelWithIconTip, { label: _i18n.i18n.translate('xpack.ml.splom.randomScoringLabel', { defaultMessage: 'Random scoring' }), tooltip: _i18n.i18n.translate('xpack.ml.splom.randomScoringInfoTooltip', { defaultMessage: 'Uses a function score query to get randomly selected documents as the sample.' }) }), display: "rowCompressed", fullWidth: true }, /*#__PURE__*/_react.default.createElement(_eui.EuiSwitch, { "data-test-subj": "mlScatterplotMatrixRandomizeQuerySwitch", name: "mlScatterplotMatrixRandomizeQuery", label: randomizeQuery ? TOGGLE_ON : TOGGLE_OFF, checked: randomizeQuery, onChange: randomizeQueryOnChange, disabled: isLoading }))), resultsField !== undefined && legendType === undefined && /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { style: { width: '120px' }, grow: false }, /*#__PURE__*/_react.default.createElement(_eui.EuiFormRow, { label: /*#__PURE__*/_react.default.createElement(OptionLabelWithIconTip, { label: _i18n.i18n.translate('xpack.ml.splom.dynamicSizeLabel', { defaultMessage: 'Dynamic size' }), tooltip: _i18n.i18n.translate('xpack.ml.splom.dynamicSizeInfoTooltip', { defaultMessage: 'Scales the size of each point by its outlier score.' }) }), display: "rowCompressed", fullWidth: true }, /*#__PURE__*/_react.default.createElement(_eui.EuiSwitch, { name: "mlScatterplotMatrixDynamicSize", label: dynamicSize ? TOGGLE_ON : TOGGLE_OFF, checked: dynamicSize, onChange: dynamicSizeOnChange, disabled: isLoading }))), splom ? /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react.default.createElement(_eui.EuiLink, { onClick: async () => { const customVisLink = getCustomVisualizationLink(); await application.navigateToApp('visualize#', { path: customVisLink.path, openInNewTab: false }); }, "data-test-subj": "mlSplomExploreInCustomVisualizationLink" }, /*#__PURE__*/_react.default.createElement(_eui.EuiIconTip, { content: _i18n.i18n.translate('xpack.ml.splom.exploreInCustomVisualizationLabel', { defaultMessage: 'Explore scatterplot charts in Vega based custom visualization' }), type: "visVega", size: "l" }))) : null), splom.messages.length > 0 && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_eui.EuiSpacer, { size: "m" }), /*#__PURE__*/_react.default.createElement(_eui.EuiCallOut, { color: "warning" }, splom.messages.map(m => /*#__PURE__*/_react.default.createElement("span", { key: (0, _mlStringHash.stringHash)(m) }, m, /*#__PURE__*/_react.default.createElement("br", null))))), splom.items.length > 0 && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_vega_chart.VegaChart, { vegaSpec: vegaSpec }), splom.backgroundItems.length ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_eui.EuiSpacer, { size: "s" }), /*#__PURE__*/_react.default.createElement(_eui.EuiFormRow, { fullWidth: true, helpText: _i18n.i18n.translate('xpack.ml.splom.backgroundLayerHelpText', { defaultMessage: "If the data points match your filter, they're shown in color; otherwise, they're blurred gray." }) }, /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null))) : null))); }; exports.ScatterplotMatrix = ScatterplotMatrix;