diff --git a/x-pack/plugins/file_data_visualizer/public/application/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx b/x-pack/plugins/file_data_visualizer/public/application/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx new file mode 100644 index 00000000000000..acbb77a7e0cac9 --- /dev/null +++ b/x-pack/plugins/file_data_visualizer/public/application/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx @@ -0,0 +1,126 @@ +/* + * 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. + */ + +import React, { FC, useMemo } from 'react'; +import { EuiFlexItem, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + FIELD_ORIGIN, + SOURCE_TYPES, + STYLE_TYPE, + COLOR_MAP_TYPE, +} from '../../../../../../../maps/common/constants'; +import { EMSTermJoinConfig } from '../../../../../../../maps/public'; +import { FieldVisStats } from '../../types'; +import { VectorLayerDescriptor } from '../../../../../../../maps/common/descriptor_types'; +import { EmbeddedMapComponent } from '../../../embedded_map'; + +export const getChoroplethTopValuesLayer = ( + fieldName: string, + topValues: Array<{ key: any; doc_count: number }>, + { layerId, field }: EMSTermJoinConfig +): VectorLayerDescriptor => { + return { + id: htmlIdGenerator()(), + label: i18n.translate('xpack.fileDataVisualizer.choroplethMap.topValuesCount', { + defaultMessage: 'Top values count for {fieldName}', + values: { fieldName }, + }), + joins: [ + { + // Left join is the id from the type of field (e.g. world_countries) + leftField: field, + right: { + id: 'anomaly_count', + type: SOURCE_TYPES.TABLE_SOURCE, + __rows: topValues, + __columns: [ + { + name: 'key', + type: 'string', + }, + { + name: 'doc_count', + type: 'number', + }, + ], + // Right join/term is the field in the doc you’re trying to join it to (foreign key - e.g. US) + term: 'key', + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: layerId, + }, + style: { + type: 'VECTOR', + // @ts-ignore missing style properties. Remove once 'VectorLayerDescriptor' type is updated + properties: { + icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } }, + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: 'doc_count', + origin: FIELD_ORIGIN.JOIN, + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: STYLE_TYPE.DYNAMIC, + options: { fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } }, + }, + isTimeAware: true, + }, + type: 'VECTOR', + }; +}; + +interface Props { + stats: FieldVisStats | undefined; + suggestion: EMSTermJoinConfig; +} + +export const ChoroplethMap: FC = ({ stats, suggestion }) => { + const { fieldName, isTopValuesSampled, topValues, topValuesSamplerShardSize } = stats!; + + const layerList: VectorLayerDescriptor[] = useMemo( + () => [getChoroplethTopValuesLayer(fieldName || '', topValues || [], suggestion)], + [suggestion, fieldName, topValues] + ); + + return ( + +
+ +
+ {isTopValuesSampled === true && ( + <> + + + + + + )} +
+ ); +}; diff --git a/x-pack/plugins/file_data_visualizer/public/application/components/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/file_data_visualizer/public/application/components/stats_table/components/field_data_expanded_row/keyword_content.tsx index 3f1a7aad5463f5..6448883bfce735 100644 --- a/x-pack/plugins/file_data_visualizer/public/application/components/stats_table/components/field_data_expanded_row/keyword_content.tsx +++ b/x-pack/plugins/file_data_visualizer/public/application/components/stats_table/components/field_data_expanded_row/keyword_content.tsx @@ -5,21 +5,55 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import type { FieldDataRowProps } from '../../types/field_data_row'; import { TopValues } from '../../../top_values'; +import { EMSTermJoinConfig } from '../../../../../../../maps/public'; +import { useFileDataVisualizerKibana } from '../../../../kibana_context'; import { DocumentStatsTable } from './document_stats'; import { ExpandedRowContent } from './expanded_row_content'; +import { ChoroplethMap } from './choropleth_map'; + +const COMMON_EMS_LAYER_IDS = [ + 'world_countries', + 'administrative_regions_lvl2', + 'usa_zip_codes', + 'usa_states', +]; export const KeywordContent: FC = ({ config }) => { - const { stats } = config; + const [EMSSuggestion, setEMSSuggestion] = useState(); + const { stats, fieldName } = config; const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; + const { + services: { maps: mapsPlugin }, + } = useFileDataVisualizerKibana(); + + const loadEMSTermSuggestions = useCallback(async () => { + if (!mapsPlugin) return; + const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ + emsLayerIds: COMMON_EMS_LAYER_IDS, + sampleValues: Array.isArray(stats?.topValues) + ? stats?.topValues.map((value) => value.key) + : [], + sampleValuesColumnName: fieldName || '', + }); + setEMSSuggestion(suggestion); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fieldName]); + + useEffect( + function getInitialEMSTermSuggestion() { + loadEMSTermSuggestions(); + }, + [loadEMSTermSuggestions] + ); return ( - + {EMSSuggestion && stats && } ); }; diff --git a/x-pack/plugins/ml/common/constants/embeddable_map.ts b/x-pack/plugins/ml/common/constants/embeddable_map.ts new file mode 100644 index 00000000000000..6cb345bae630ec --- /dev/null +++ b/x-pack/plugins/ml/common/constants/embeddable_map.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export const COMMON_EMS_LAYER_IDS = [ + 'world_countries', + 'administrative_regions_lvl2', + 'usa_zip_codes', + 'usa_states', +]; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/choropleth_map/choropleth_map.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/choropleth_map/choropleth_map.tsx new file mode 100644 index 00000000000000..8b7cbf83f79967 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/choropleth_map/choropleth_map.tsx @@ -0,0 +1,126 @@ +/* + * 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. + */ + +import React, { FC, useMemo } from 'react'; +import { EuiFlexItem, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + FIELD_ORIGIN, + SOURCE_TYPES, + STYLE_TYPE, + COLOR_MAP_TYPE, +} from '../../../../../../../../maps/common/constants'; +import { EMSTermJoinConfig } from '../../../../../../../../maps/public'; +import { FieldVisStats } from '../../../../stats_table/types'; +import { VectorLayerDescriptor } from '../../../../../../../../maps/common/descriptor_types'; +import { MlEmbeddedMapComponent } from '../../../../../components/ml_embedded_map'; + +export const getChoroplethTopValuesLayer = ( + fieldName: string, + topValues: Array<{ key: any; doc_count: number }>, + { layerId, field }: EMSTermJoinConfig +): VectorLayerDescriptor => { + return { + id: htmlIdGenerator()(), + label: i18n.translate('xpack.ml.dataviz.choroplethMap.topValuesCount', { + defaultMessage: 'Top values count for {fieldName}', + values: { fieldName }, + }), + joins: [ + { + // Left join is the id from the type of field (e.g. world_countries) + leftField: field, + right: { + id: 'anomaly_count', + type: SOURCE_TYPES.TABLE_SOURCE, + __rows: topValues, + __columns: [ + { + name: 'key', + type: 'string', + }, + { + name: 'doc_count', + type: 'number', + }, + ], + // Right join/term is the field in the doc you’re trying to join it to (foreign key - e.g. US) + term: 'key', + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: layerId, + }, + style: { + type: 'VECTOR', + // @ts-ignore missing style properties. Remove once 'VectorLayerDescriptor' type is updated + properties: { + icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } }, + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: 'doc_count', + origin: FIELD_ORIGIN.JOIN, + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: STYLE_TYPE.DYNAMIC, + options: { fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } }, + }, + isTimeAware: true, + }, + type: 'VECTOR', + }; +}; + +interface Props { + stats: FieldVisStats | undefined; + suggestion: EMSTermJoinConfig; +} + +export const ChoroplethMap: FC = ({ stats, suggestion }) => { + const { fieldName, isTopValuesSampled, topValues, topValuesSamplerShardSize } = stats!; + + const layerList: VectorLayerDescriptor[] = useMemo( + () => [getChoroplethTopValuesLayer(fieldName || '', topValues || [], suggestion)], + [suggestion, stats] + ); + + return ( + +
+ +
+ {isTopValuesSampled === true && ( + <> + + + + + + )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/choropleth_map/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/choropleth_map/index.ts new file mode 100644 index 00000000000000..6159b5e2ad9bbf --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/choropleth_map/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { ChoroplethMap } from './choropleth_map'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx index 9d4e13c2916568..9239632a3f909c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx @@ -5,21 +5,50 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import type { FieldDataRowProps } from '../../types/field_data_row'; import { TopValues } from '../../../index_based/components/field_data_row/top_values'; +import { ChoroplethMap } from '../../../index_based/components/field_data_row/choropleth_map'; +import { useMlKibana } from '../../../../../application/contexts/kibana'; +import { EMSTermJoinConfig } from '../../../../../../../maps/public'; +import { COMMON_EMS_LAYER_IDS } from '../../../../../../common/constants/embeddable_map'; import { DocumentStatsTable } from './document_stats'; import { ExpandedRowContent } from './expanded_row_content'; export const KeywordContent: FC = ({ config }) => { - const { stats } = config; + const [EMSSuggestion, setEMSSuggestion] = useState(); + const { stats, fieldName } = config; const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; + const { + services: { maps: mapsPlugin }, + } = useMlKibana(); + + const loadEMSTermSuggestions = async () => { + if (!mapsPlugin) return; + const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ + emsLayerIds: COMMON_EMS_LAYER_IDS, + sampleValues: Array.isArray(stats?.topValues) + ? stats?.topValues.map((value) => value.key) + : [], + sampleValuesColumnName: fieldName || '', + }); + setEMSSuggestion(suggestion); + }; + + useEffect( + function getInitialEMSTermSuggestion() { + loadEMSTermSuggestions(); + }, + [config?.fieldName] + ); return ( - - + {EMSSuggestion && stats && } + {EMSSuggestion === null && ( + + )} ); }; diff --git a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx index 7914061dc81c79..73a6a9d64b60e3 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx @@ -28,14 +28,9 @@ import { isDefined } from '../../../common/types/guards'; import { MlEmbeddedMapComponent } from '../components/ml_embedded_map'; import { EMSTermJoinConfig } from '../../../../maps/public'; import { AnomaliesTableRecord } from '../../../common/types/anomalies'; +import { COMMON_EMS_LAYER_IDS } from '../../../common/constants/embeddable_map'; const MAX_ENTITY_VALUES = 3; -const COMMON_EMS_LAYER_IDS = [ - 'world_countries', - 'administrative_regions_lvl2', - 'usa_zip_codes', - 'usa_states', -]; function getAnomalyRows(anomalies: AnomaliesTableRecord[], jobId: string) { const anomalyRows: Record<