diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx index 4e3ed74c4470e9..cb4f55b49cb41f 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx @@ -15,18 +15,14 @@ import { EuiPanel, EuiSpacer, EuiCallOut, - EuiRange, EuiSelect, EuiFormRow, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { debounce, sortedIndex } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; import { isDefined } from '@kbn/ml-is-defined'; import type { DocumentCountChartPoint } from './document_count_chart'; import { - RANDOM_SAMPLER_STEP, - RANDOM_SAMPLER_PROBABILITIES, RandomSamplerOption, RANDOM_SAMPLER_SELECT_OPTIONS, RANDOM_SAMPLER_OPTION, @@ -34,17 +30,43 @@ import { import { TotalCountHeader } from './total_count_header'; import type { DocumentCountStats } from '../../../../../common/types/field_stats'; import { DocumentCountChart } from './document_count_chart'; +import { RandomSamplerRangeSlider } from './random_sampler_range_slider'; export interface Props { documentCountStats?: DocumentCountStats; totalCount: number; samplingProbability?: number | null; - setSamplingProbability?: (value: number) => void; + setSamplingProbability?: (value: number | null) => void; randomSamplerPreference?: RandomSamplerOption; setRandomSamplerPreference: (value: RandomSamplerOption) => void; loading: boolean; } +const ProbabilityUsedMessage = ({ samplingProbability }: Pick) => { + return isDefined(samplingProbability) ? ( +
+ + + +
+ ) : null; +}; + +const CalculatingProbabilityMessage = ( +
+ + + +
+); + export const DocumentCountContent: FC = ({ documentCountStats, totalCount, @@ -64,24 +86,6 @@ export const DocumentCountContent: FC = ({ setShowSamplingOptionsPopover(false); }, [setShowSamplingOptionsPopover]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const updateSamplingProbability = useCallback( - debounce((newProbability: number) => { - if (setSamplingProbability) { - const idx = sortedIndex(RANDOM_SAMPLER_PROBABILITIES, newProbability); - const closestPrev = RANDOM_SAMPLER_PROBABILITIES[idx - 1]; - const closestNext = RANDOM_SAMPLER_PROBABILITIES[idx]; - const closestProbability = - Math.abs(closestPrev - newProbability) < Math.abs(closestNext - newProbability) - ? closestPrev - : closestNext; - - setSamplingProbability(closestProbability / 100); - } - }, 100), - [setSamplingProbability] - ); - const calloutInfoMessage = useMemo(() => { switch (randomSamplerPreference) { case RANDOM_SAMPLER_OPTION.OFF: @@ -126,19 +130,6 @@ export const DocumentCountContent: FC = ({ const approximate = documentCountStats.randomlySampled === true; - const ProbabilityUsed = - randomSamplerPreference !== RANDOM_SAMPLER_OPTION.OFF && isDefined(samplingProbability) ? ( -
- - - -
- ) : null; - return ( <> @@ -195,37 +186,19 @@ export const DocumentCountContent: FC = ({ {randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_MANUAL ? ( - - - - ({ - value: d, - label: d === 0.001 || d >= 5 ? `${d}%` : '', - }))} - onChange={(e) => updateSamplingProbability(Number(e.currentTarget.value))} - step={RANDOM_SAMPLER_STEP} - data-test-subj="dvRandomSamplerProbabilityRange" - /> - - - ) : ( - ProbabilityUsed - )} + + ) : null} + + {randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_AUTOMATIC ? ( + loading ? ( + CalculatingProbabilityMessage + ) : ( + + ) + ) : null} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/random_sampler_range_slider.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/random_sampler_range_slider.tsx new file mode 100644 index 00000000000000..6b689e08273f9b --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/random_sampler_range_slider.tsx @@ -0,0 +1,112 @@ +/* + * 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 { EuiButton, EuiFlexItem, EuiFormRow, EuiRange, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isDefined } from '@kbn/ml-is-defined'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useState } from 'react'; +import { roundToDecimalPlace } from '../utils'; +import { + MIN_SAMPLER_PROBABILITY, + RANDOM_SAMPLER_PROBABILITIES, + RANDOM_SAMPLER_STEP, +} from '../../../index_data_visualizer/constants/random_sampler'; + +export const RandomSamplerRangeSlider = ({ + samplingProbability, + setSamplingProbability, +}: { + samplingProbability?: number | null; + setSamplingProbability?: (value: number | null) => void; +}) => { + // Keep track of the input in sampling probability slider when mode is on - manual + // before 'Apply' is clicked + const [samplingProbabilityInput, setSamplingProbabilityInput] = useState(samplingProbability); + + const isInvalidSamplingProbabilityInput = + !isDefined(samplingProbabilityInput) || + isNaN(samplingProbabilityInput) || + samplingProbabilityInput < MIN_SAMPLER_PROBABILITY || + samplingProbabilityInput > 0.5; + + const inputValue = (samplingProbabilityInput ?? MIN_SAMPLER_PROBABILITY) * 100; + + return ( + + + + = 1 + ? roundToDecimalPlace(inputValue, 0) + : roundToDecimalPlace(inputValue, 3) + } + ticks={RANDOM_SAMPLER_PROBABILITIES.map((d) => ({ + value: d, + label: d === 0.001 || d >= 5 ? `${d}` : '', + }))} + isInvalid={isInvalidSamplingProbabilityInput} + onChange={(e) => { + const value = parseFloat((e.target as HTMLInputElement).value); + const prevValue = samplingProbabilityInput ? samplingProbabilityInput * 100 : value; + + if (value > 0 && value <= 1) { + setSamplingProbabilityInput(value / 100); + } else { + // Because the incremental step is very small (0.0001), + // everytime user clicks the ^/∨ in the numerical input + // we need to make sure it rounds up or down to the next whole number + const nearestInt = value > prevValue ? Math.ceil(value) : Math.floor(value); + setSamplingProbabilityInput(nearestInt / 100); + } + }} + step={RANDOM_SAMPLER_STEP} + data-test-subj="dvRandomSamplerProbabilityRange" + append={ + { + if (setSamplingProbability && isDefined(samplingProbabilityInput)) { + setSamplingProbability(samplingProbabilityInput); + } + }} + > + + + } + /> + + + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index 1508683fd1a433..18695703fb87d3 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -41,6 +41,7 @@ import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme' import { DV_FROZEN_TIER_PREFERENCE, DV_RANDOM_SAMPLER_PREFERENCE, + DV_RANDOM_SAMPLER_P_VALUE, type DVKey, type DVStorageMapped, } from '../../types/storage'; @@ -71,7 +72,11 @@ import { DataVisualizerDataViewManagement } from '../data_view_management'; import { GetAdditionalLinks } from '../../../common/components/results_links'; import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable'; -import { RANDOM_SAMPLER_OPTION } from '../../constants/random_sampler'; +import { + MIN_SAMPLER_PROBABILITY, + RANDOM_SAMPLER_OPTION, + RandomSamplerOption, +} from '../../constants/random_sampler'; interface DataVisualizerPageState { overallStats: OverallStats; @@ -143,6 +148,11 @@ export const IndexDataVisualizerView: FC = (dataVi DVStorageMapped >(DV_RANDOM_SAMPLER_PREFERENCE, RANDOM_SAMPLER_OPTION.ON_AUTOMATIC); + const [savedRandomSamplerProbability, saveRandomSamplerProbability] = useStorage< + DVKey, + DVStorageMapped + >(DV_RANDOM_SAMPLER_P_VALUE, MIN_SAMPLER_PROBABILITY); + const [frozenDataPreference, setFrozenDataPreference] = useStorage< DVKey, DVStorageMapped @@ -156,6 +166,7 @@ export const IndexDataVisualizerView: FC = (dataVi () => getDefaultDataVisualizerListState({ rndSamplerPref: savedRandomSamplerPreference, + probability: savedRandomSamplerProbability, }), // We just need to load the saved preference when the page is first loaded // eslint-disable-next-line react-hooks/exhaustive-deps @@ -316,9 +327,38 @@ export const IndexDataVisualizerView: FC = (dataVi ] ); - const setSamplingProbability = (value: number | null) => { - setDataVisualizerListState({ ...dataVisualizerListState, probability: value }); - }; + const setSamplingProbability = useCallback( + (value: number | null) => { + if (savedRandomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_MANUAL && value !== null) { + saveRandomSamplerProbability(value); + } + setDataVisualizerListState({ ...dataVisualizerListState, probability: value }); + }, + [ + dataVisualizerListState, + saveRandomSamplerProbability, + savedRandomSamplerPreference, + setDataVisualizerListState, + ] + ); + + const setRandomSamplerPreference = useCallback( + (nextPref: RandomSamplerOption) => { + if (nextPref === RANDOM_SAMPLER_OPTION.ON_MANUAL) { + // By default, when switching to manual, restore previously chosen probability + // else, default to 0.001% + setSamplingProbability( + savedRandomSamplerProbability && + savedRandomSamplerProbability > 0 && + savedRandomSamplerProbability <= 0.5 + ? savedRandomSamplerProbability + : MIN_SAMPLER_PROBABILITY + ); + } + saveRandomSamplerPreference(nextPref); + }, + [savedRandomSamplerProbability, setSamplingProbability, saveRandomSamplerPreference] + ); useEffect( function clearFiltersOnLeave() { @@ -565,7 +605,7 @@ export const IndexDataVisualizerView: FC = (dataVi } loading={overallStatsProgress.loaded < 100} randomSamplerPreference={savedRandomSamplerPreference} - setRandomSamplerPreference={saveRandomSamplerPreference} + setRandomSamplerPreference={setRandomSamplerPreference} /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/constants/random_sampler.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/constants/random_sampler.ts index 310ccde5f9e29f..f1cfdd3d2bca80 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/constants/random_sampler.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/constants/random_sampler.ts @@ -8,13 +8,11 @@ import { i18n } from '@kbn/i18n'; export const RANDOM_SAMPLER_PROBABILITIES = [ - 0.5, 0.25, 0.1, 0.05, 0.025, 0.01, 0.005, 0.0025, 0.001, 0.0005, 0.00025, 0.0001, 0.00005, - 0.00001, -] - .reverse() - .map((n) => n * 100); + 0.00001, 0.00005, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, +].map((n) => n * 100); -export const RANDOM_SAMPLER_STEP = 0.00001 * 100; +export const MIN_SAMPLER_PROBABILITY = 0.00001; +export const RANDOM_SAMPLER_STEP = MIN_SAMPLER_PROBABILITY * 100; export const RANDOM_SAMPLER_OPTION = { ON_AUTOMATIC: 'on_automatic', diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/storage.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/storage.ts index cbafcfd63c6335..372fa2a738a538 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/storage.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/storage.ts @@ -16,7 +16,7 @@ export const DV_RANDOM_SAMPLER_P_VALUE = 'dataVisualizer.randomSamplerPValue'; export type DV = Partial<{ [DV_FROZEN_TIER_PREFERENCE]: FrozenTierPreference; [DV_RANDOM_SAMPLER_PREFERENCE]: RandomSamplerOption; - [DV_RANDOM_SAMPLER_P_VALUE]: number; + [DV_RANDOM_SAMPLER_P_VALUE]: null | number; }> | null; export type DVKey = keyof Exclude; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx index ad9f0b3d0bb712..65954578db6ed1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx @@ -58,6 +58,10 @@ export const getStringBasedClassName = (v: string | boolean | undefined | number if (typeof v === 'boolean') { return v ? 'True' : 'False'; } + + if (v === 'true') return 'True'; + if (v === 'false') return 'False'; + if (typeof v === 'number') { return v.toString(); } diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_random_sampler.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_random_sampler.ts index 7df4e9c18eee7f..ce74bbfb8884ab 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_random_sampler.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_random_sampler.ts @@ -71,8 +71,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await goToSourceForIndexBasedDataVisualizer('ft_module_sample_logs'); await ml.dataVisualizerIndexBased.assertRandomSamplingOption( - 'dvRandomSamplerOptionOnManual', - 50 + 'dvRandomSamplerOptionOnManual' ); }); }); diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index e46108373d450d..c15569bb262ac7 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -258,7 +258,7 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ if (expectedOption === 'dvRandomSamplerOptionOff') { await testSubjects.existOrFail('dvRandomSamplerOptionOff', { timeout: 1000 }); await testSubjects.missingOrFail('dvRandomSamplerProbabilityRange', { timeout: 1000 }); - await testSubjects.missingOrFail('dvRandomSamplerAutomaticProbabilityMsg', { + await testSubjects.missingOrFail('dvRandomSamplerProbabilityUsedMsg', { timeout: 1000, }); } @@ -280,13 +280,13 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ if (expectedOption === 'dvRandomSamplerOptionOnAutomatic') { await testSubjects.existOrFail('dvRandomSamplerOptionOnAutomatic', { timeout: 1000 }); - await testSubjects.existOrFail('dvRandomSamplerAutomaticProbabilityMsg', { + await testSubjects.existOrFail('dvRandomSamplerProbabilityUsedMsg', { timeout: 1000, }); if (expectedProbability !== undefined) { const probabilityText = await testSubjects.getVisibleText( - 'dvRandomSamplerAutomaticProbabilityMsg' + 'dvRandomSamplerProbabilityUsedMsg' ); expect(probabilityText).to.contain( `${expectedProbability}`,