{tabs.map((tab) => (
setSelectedTabId(tab.id)}
key={tab.id}
@@ -146,6 +147,6 @@ export const DecisionPathPopover: FC = ({
{selectedTabId === DECISION_PATH_TABS.JSON && (
)}
- >
+
);
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx
index 64835e7ca4c6d..0fab1cf75259e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx
@@ -210,6 +210,7 @@ export const FeatureImportanceSummaryPanel: FC
-
+
)
}
/>
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js
index 8df186c5c3c6e..b2d054becbb1a 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js
@@ -51,6 +51,7 @@ import {
unhighlightFocusChartAnnotation,
ANNOTATION_MIN_WIDTH,
} from './timeseries_chart_annotations';
+import { distinctUntilChanged } from 'rxjs/operators';
const focusZoomPanelHeight = 25;
const focusChartHeight = 310;
@@ -570,6 +571,7 @@ class TimeseriesChartIntl extends Component {
}
renderFocusChart() {
+ console.log('renderFocusChart');
const {
focusAggregationInterval,
focusAnnotationData: focusAnnotationDataOriginalPropValue,
@@ -1798,7 +1800,15 @@ class TimeseriesChartIntl extends Component {
}
export const TimeseriesChart = (props) => {
- const annotationProp = useObservable(annotation$);
+ const annotationProp = useObservable(
+ annotation$.pipe(
+ distinctUntilChanged((prev, curr) => {
+ // prevent re-rendering
+ return prev !== null && curr !== null;
+ })
+ )
+ );
+
if (annotationProp === undefined) {
return null;
}
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
index e3b6e38f47bab..f14f11e5d6149 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
@@ -1014,6 +1014,7 @@ export class TimeSeriesExplorer extends React.Component {
this.previousShowForecast = showForecast;
this.previousShowModelBounds = showModelBounds;
+ console.log('Timeseriesexplorer rerendered');
return (
{fieldNamesWithEmptyValues.length > 0 && (
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts
new file mode 100644
index 0000000000000..ff2bbd077fa8f
--- /dev/null
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts
@@ -0,0 +1,211 @@
+/*
+ * 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.
+ */
+import { DeepPartial } from '../../../../../plugins/ml/common/types/common';
+import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const ml = getService('ml');
+
+ describe('total feature importance panel and decision path popover', function () {
+ const testDataList: Array<{
+ suiteTitle: string;
+ archive: string;
+ indexPattern: { name: string; timeField: string };
+ job: DeepPartial;
+ }> = (() => {
+ const timestamp = Date.now();
+
+ return [
+ {
+ suiteTitle: 'binary classification job',
+ archive: 'ml/ihp_outlier',
+ indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
+ job: {
+ id: `ihp_fi_binary_${timestamp}`,
+ description:
+ "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'",
+ source: {
+ index: ['ft_ihp_outlier'],
+ query: {
+ match_all: {},
+ },
+ },
+ dest: {
+ get index(): string {
+ return `user-ihp_fi_binary_${timestamp}`;
+ },
+ results_field: 'ml_central_air',
+ },
+ analyzed_fields: {
+ includes: [
+ 'CentralAir',
+ 'GarageArea',
+ 'GarageCars',
+ 'YearBuilt',
+ 'Electrical',
+ 'Neighborhood',
+ 'Heating',
+ '1stFlrSF',
+ ],
+ },
+ analysis: {
+ classification: {
+ dependent_variable: 'CentralAir',
+ num_top_feature_importance_values: 5,
+ training_percent: 35,
+ prediction_field_name: 'CentralAir_prediction',
+ num_top_classes: -1,
+ },
+ },
+ model_memory_limit: '60mb',
+ allow_lazy_start: false,
+ },
+ },
+ {
+ suiteTitle: 'multi class classification job',
+ archive: 'ml/ihp_outlier',
+ indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
+ job: {
+ id: `ihp_fi_multi_${timestamp}`,
+ description:
+ "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'",
+ source: {
+ index: ['ft_ihp_outlier'],
+ query: {
+ match_all: {},
+ },
+ },
+ dest: {
+ get index(): string {
+ return `user-ihp_fi_multi_${timestamp}`;
+ },
+ results_field: 'ml_heating_qc',
+ },
+ analyzed_fields: {
+ includes: [
+ 'CentralAir',
+ 'GarageArea',
+ 'GarageCars',
+ 'Electrical',
+ 'Neighborhood',
+ 'Heating',
+ '1stFlrSF',
+ 'HeatingQC',
+ ],
+ },
+ analysis: {
+ classification: {
+ dependent_variable: 'HeatingQC',
+ num_top_feature_importance_values: 5,
+ training_percent: 35,
+ prediction_field_name: 'heatingqc',
+ num_top_classes: -1,
+ },
+ },
+ model_memory_limit: '60mb',
+ allow_lazy_start: false,
+ },
+ },
+ {
+ suiteTitle: 'regression job',
+ archive: 'ml/egs_regression',
+ indexPattern: { name: 'ft_egs_regression', timeField: '@timestamp' },
+ job: {
+ id: `egs_fi_reg_${timestamp}`,
+ description: 'This is the job description',
+ source: {
+ index: ['ft_egs_regression'],
+ query: {
+ match_all: {},
+ },
+ },
+ dest: {
+ get index(): string {
+ return `user-egs_fi_reg_${timestamp}`;
+ },
+ results_field: 'ml',
+ },
+ analysis: {
+ regression: {
+ prediction_field_name: 'test',
+ dependent_variable: 'stab',
+ num_top_feature_importance_values: 5,
+ training_percent: 35,
+ },
+ },
+ analyzed_fields: {
+ includes: [
+ 'g1',
+ 'g2',
+ 'g3',
+ 'g4',
+ 'p1',
+ 'p2',
+ 'p3',
+ 'p4',
+ 'stab',
+ 'tau1',
+ 'tau2',
+ 'tau3',
+ 'tau4',
+ ],
+ excludes: [],
+ },
+ model_memory_limit: '20mb',
+ },
+ },
+ ];
+ })();
+
+ before(async () => {
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ await ml.securityUI.loginAsMlPowerUser();
+ for (const testData of testDataList) {
+ await esArchiver.loadIfNeeded(testData.archive);
+ await ml.testResources.createIndexPatternIfNeeded(
+ testData.indexPattern.name,
+ testData.indexPattern.timeField
+ );
+ await ml.api.createAndRunDFAJob(testData.job as DataFrameAnalyticsConfig);
+ }
+ });
+
+ after(async () => {
+ await ml.api.cleanMlIndices();
+ });
+
+ for (const testData of testDataList) {
+ describe(`${testData.suiteTitle}`, function () {
+ before(async () => {
+ await ml.navigation.navigateToMl();
+ await ml.navigation.navigateToDataFrameAnalytics();
+ await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad();
+ await ml.dataFrameAnalyticsTable.openResultsView(testData.job.id as string);
+ });
+
+ after(async () => {
+ await ml.api.deleteIndices(testData.job.dest!.index as string);
+ await ml.testResources.deleteIndexPatternByTitle(testData.job.dest!.index as string);
+ });
+
+ it('should display the total feature importance in the results view', async () => {
+ await ml.dataFrameAnalyticsResults.assertTotalFeatureImportanceEvaluatePanelExists();
+ });
+
+ it('should display the feature importance decision path in the data grid', async () => {
+ await ml.dataFrameAnalyticsResults.assertResultsTableExists();
+ await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty();
+ await ml.dataFrameAnalyticsResults.openFeatureImportanceDecisionPathPopover();
+ await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathElementsExists();
+ await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathChartElementsExists();
+ });
+ });
+ }
+ });
+}
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts
index 0202c8431ce34..a57d26b536b4f 100644
--- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts
@@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./regression_creation'));
loadTestFile(require.resolve('./classification_creation'));
loadTestFile(require.resolve('./cloning'));
+ loadTestFile(require.resolve('./feature_importance'));
});
}
diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts
index 8781a2cd216f2..1ac11a0149897 100644
--- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts
+++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts
@@ -5,12 +5,14 @@
*/
import expect from '@kbn/expect';
+import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper';
import { FtrProviderContext } from '../../ftr_provider_context';
export function MachineLearningDataFrameAnalyticsResultsProvider({
getService,
}: FtrProviderContext) {
+ const retry = getService('retry');
const testSubjects = getService('testSubjects');
return {
@@ -60,5 +62,59 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({
`DFA results table should have at least one row (got '${resultTableRows.length}')`
);
},
+
+ async assertTotalFeatureImportanceEvaluatePanelExists() {
+ await testSubjects.existOrFail('mlDFExpandableSection-FeatureImportanceSummary');
+ await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 5000 });
+ },
+
+ async assertFeatureImportanceDecisionPathElementsExists() {
+ await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_chart', {
+ timeout: 5000,
+ });
+ await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_json', {
+ timeout: 5000,
+ });
+ },
+
+ async assertFeatureImportanceDecisionPathChartElementsExists() {
+ await testSubjects.existOrFail('mlDFADecisionPathChart', {
+ timeout: 5000,
+ });
+ },
+
+ async openFeatureImportanceDecisionPathPopover() {
+ this.assertResultsTableNotEmpty();
+
+ const featureImportanceCell = await this.getFirstFeatureImportanceCell();
+ const interactionButton = await featureImportanceCell.findByTagName('button');
+
+ // simulate hover and wait for button to appear
+ await featureImportanceCell.moveMouseTo();
+ await this.waitForInteractionButtonToDisplay(interactionButton);
+
+ // open popover
+ await interactionButton.click();
+ await testSubjects.existOrFail('mlDFADecisionPathPopover');
+ },
+
+ async getFirstFeatureImportanceCell(): Promise {
+ // get first row of the data grid
+ const firstDataGridRow = await testSubjects.find(
+ 'mlExplorationDataGrid loaded > dataGridRow'
+ );
+ // find the feature importance cell in that row
+ const featureImportanceCell = await firstDataGridRow.findByCssSelector(
+ '[data-test-subj="dataGridRowCell"][class*="featureImportance"]'
+ );
+ return featureImportanceCell;
+ },
+
+ async waitForInteractionButtonToDisplay(interactionButton: WebElementWrapper) {
+ await retry.tryForTime(5000, async () => {
+ const buttonVisible = await interactionButton.isDisplayed();
+ expect(buttonVisible).to.equal(true, 'Expected data grid cell button to be visible');
+ });
+ },
};
}