You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by yo...@apache.org on 2022/06/05 13:05:22 UTC

[superset] branch master updated: feat: standardized form_data (#20010)

This is an automated email from the ASF dual-hosted git repository.

yongjiezhao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new dd4b581fb5 feat: standardized form_data (#20010)
dd4b581fb5 is described below

commit dd4b581fb55d920fc3b709fc044cea5339802ee2
Author: Yongjie Zhao <yo...@gmail.com>
AuthorDate: Sun Jun 5 21:05:01 2022 +0800

    feat: standardized form_data (#20010)
---
 .../superset-ui-chart-controls/src/types.ts        |  17 ++
 .../src/BigNumber/BigNumberTotal/controlPanel.ts   |   4 +
 .../BigNumberWithTrendline/controlPanel.tsx        |   4 +
 .../plugin-chart-echarts/src/Pie/controlPanel.tsx  |   5 +
 .../src/Timeseries/Regular/controlPanel.tsx        |   4 +
 .../src/plugin/controlPanel.tsx                    |  12 +
 .../plugin-chart-table/src/controlPanel.tsx        |   5 +
 .../src/explore/controlUtils/index.ts              |   1 +
 .../controlUtils/standardizedFormData.test.tsx     | 307 +++++++++++++++++++++
 .../explore/controlUtils/standardizedFormData.ts   | 182 ++++++++++++
 .../src/explore/reducers/exploreReducer.js         |  28 +-
 11 files changed, 554 insertions(+), 15 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
index 53e3a198bd..7926ffd759 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -25,6 +25,8 @@ import type {
   JsonValue,
   Metric,
   QueryFormData,
+  QueryFormMetric,
+  QueryFormColumn,
 } from '@superset-ui/core';
 import { sharedControls } from './shared-controls';
 import sharedControlComponents from './shared-controls/components';
@@ -340,11 +342,26 @@ export interface ControlPanelSectionConfig {
   controlSetRows: ControlSetRow[];
 }
 
+export interface StandardizedState {
+  metrics: QueryFormMetric[];
+  columns: QueryFormColumn[];
+}
+
+export interface StandardizedFormDataInterface {
+  standardizedState: StandardizedState;
+  memorizedFormData: Map<string, QueryFormData>;
+}
+
 export interface ControlPanelConfig {
   controlPanelSections: (ControlPanelSectionConfig | null)[];
   controlOverrides?: ControlOverrides;
   sectionOverrides?: SectionOverrides;
   onInit?: (state: ControlStateMapping) => void;
+  denormalizeFormData?: (
+    formData: QueryFormData & {
+      standardizedFormData: StandardizedFormDataInterface;
+    },
+  ) => QueryFormData;
 }
 
 export type ControlOverrides = {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts
index 8511c3ca56..c715c8f232 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts
@@ -96,4 +96,8 @@ export default {
       label: t('Number format'),
     },
   },
+  denormalizeFormData: formData => ({
+    ...formData,
+    metric: formData.standardizedFormData.standardizedState.metrics[0],
+  }),
 } as ControlPanelConfig;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx
index 3ba00f55ea..c61ec6d046 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx
@@ -270,6 +270,10 @@ const config: ControlPanelConfig = {
       label: t('Number format'),
     },
   },
+  denormalizeFormData: formData => ({
+    ...formData,
+    metric: formData.standardizedFormData.standardizedState.metrics[0],
+  }),
 };
 
 export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx
index 9056446f9f..6e3a1ba593 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx
@@ -253,6 +253,11 @@ const config: ControlPanelConfig = {
       default: 100,
     },
   },
+  denormalizeFormData: formData => ({
+    ...formData,
+    metric: formData.standardizedFormData.standardizedState.metrics[0],
+    groupby: formData.standardizedFormData.standardizedState.columns,
+  }),
 };
 
 export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx
index f234df0c82..eaf3cb2615 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx
@@ -240,6 +240,10 @@ const config: ControlPanelConfig = {
       default: rowLimit,
     },
   },
+  denormalizeFormData: formData => ({
+    ...formData,
+    metrics: formData.standardizedFormData.standardizedState.metrics,
+  }),
 };
 
 export default config;
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx
index 99efb8bbcc..acbdb04bc7 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx
@@ -18,6 +18,7 @@
  */
 import React from 'react';
 import {
+  ensureIsArray,
   QueryFormMetric,
   smartDateFormatter,
   t,
@@ -367,6 +368,17 @@ const config: ControlPanelConfig = {
       ],
     },
   ],
+  denormalizeFormData: formData => {
+    const groupbyColumns =
+      formData.standardizedFormData.standardizedState.columns.filter(
+        col => !ensureIsArray(formData.groupbyRows).includes(col),
+      );
+    return {
+      ...formData,
+      metrics: formData.standardizedFormData.standardizedState.metrics,
+      groupbyColumns,
+    };
+  },
 };
 
 export default config;
diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
index bb855bd7cc..4f5530f6b9 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
@@ -531,6 +531,11 @@ const config: ControlPanelConfig = {
       ],
     },
   ],
+  denormalizeFormData: formData => ({
+    ...formData,
+    metrics: formData.standardizedFormData.standardizedState.metrics,
+    groupby: formData.standardizedFormData.standardizedState.columns,
+  }),
 };
 
 export default config;
diff --git a/superset-frontend/src/explore/controlUtils/index.ts b/superset-frontend/src/explore/controlUtils/index.ts
index d9cb1132f4..b39233422b 100644
--- a/superset-frontend/src/explore/controlUtils/index.ts
+++ b/superset-frontend/src/explore/controlUtils/index.ts
@@ -21,3 +21,4 @@ export * from './getControlConfig';
 export * from './getControlState';
 export * from './getFormDataFromControls';
 export * from './getControlValuesCompatibleWithDatasource';
+export * from './standardizedFormData';
diff --git a/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx
new file mode 100644
index 0000000000..e048c04498
--- /dev/null
+++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx
@@ -0,0 +1,307 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { getChartControlPanelRegistry, QueryFormData } from '@superset-ui/core';
+import TableChartPlugin from '@superset-ui/plugin-chart-table';
+import { BigNumberTotalChartPlugin } from '@superset-ui/plugin-chart-echarts';
+import { sections } from '@superset-ui/chart-controls';
+import {
+  StandardizedFormData,
+  sharedControls,
+  publicControls,
+} from './standardizedFormData';
+import { xAxisControl } from '../../../plugins/plugin-chart-echarts/src/controls';
+
+describe('should collect control values and create SFD', () => {
+  const sharedControlsFormData = {};
+  Object.entries(sharedControls).forEach(([, names]) => {
+    names.forEach(name => {
+      sharedControlsFormData[name] = name;
+    });
+  });
+  const publicControlsFormData = Object.fromEntries(
+    publicControls.map((name, idx) => [[name], idx]),
+  );
+  const sourceMockFormData: QueryFormData = {
+    ...sharedControlsFormData,
+    ...publicControlsFormData,
+    datasource: '100__table',
+    viz_type: 'source_viz',
+  };
+  const sourceMockStore = {
+    form_data: sourceMockFormData,
+    controls: Object.fromEntries(
+      Object.entries(sourceMockFormData).map(([key, value]) => [
+        key,
+        { value },
+      ]),
+    ),
+    datasource: {
+      type: 'table',
+      columns: [],
+    },
+  };
+  beforeAll(() => {
+    getChartControlPanelRegistry().registerValue('source_viz', {
+      controlPanelSections: [
+        sections.advancedAnalyticsControls,
+        {
+          label: 'transform controls',
+          controlSetRows: publicControls.map(control => [control]),
+        },
+        {
+          label: 'axis column',
+          controlSetRows: [[xAxisControl]],
+        },
+      ],
+    });
+    getChartControlPanelRegistry().registerValue('target_viz', {
+      controlPanelSections: [
+        sections.advancedAnalyticsControls,
+        {
+          label: 'transform controls',
+          controlSetRows: publicControls.map(control => [control]),
+        },
+        {
+          label: 'axis column',
+          controlSetRows: [[xAxisControl]],
+        },
+      ],
+      denormalizeFormData: (formData: QueryFormData) => ({
+        ...formData,
+        columns: formData.standardizedFormData.standardizedState.columns,
+        metrics: formData.standardizedFormData.standardizedState.metrics,
+      }),
+    });
+  });
+
+  test('collect sharedControls', () => {
+    const sfd = new StandardizedFormData(sourceMockFormData);
+
+    expect(sfd.dumpSFD().standardizedState.metrics).toEqual(
+      sharedControls.metrics.map(controlName => controlName),
+    );
+    expect(sfd.dumpSFD().standardizedState.columns).toEqual(
+      sharedControls.columns.map(controlName => controlName),
+    );
+  });
+
+  test('should transform all publicControls', () => {
+    const sfd = new StandardizedFormData(sourceMockFormData);
+    const { formData } = sfd.transform('target_viz', sourceMockStore);
+    Object.entries(publicControlsFormData).forEach(([key]) => {
+      expect(formData).toHaveProperty(key);
+    });
+    Object.entries(sharedControls).forEach(([key, value]) => {
+      expect(formData[key]).toEqual(value);
+    });
+  });
+
+  test('should inherit standardizedFormData and memorizedFormData is LIFO', () => {
+    // from source_viz to target_viz
+    const sfd = new StandardizedFormData(sourceMockFormData);
+    const { formData, controlsState } = sfd.transform(
+      'target_viz',
+      sourceMockStore,
+    );
+    expect(
+      formData.standardizedFormData.memorizedFormData.map(
+        (fd: [string, QueryFormData]) => fd[0],
+      ),
+    ).toEqual(['source_viz']);
+
+    // from target_viz to source_viz
+    const sfd2 = new StandardizedFormData(formData);
+    const { formData: fd2, controlsState: cs2 } = sfd2.transform('source_viz', {
+      ...sourceMockStore,
+      form_data: formData,
+      controls: controlsState,
+    });
+    expect(
+      fd2.standardizedFormData.memorizedFormData.map(
+        (fd: [string, QueryFormData]) => fd[0],
+      ),
+    ).toEqual(['source_viz', 'target_viz']);
+
+    // from source_viz to target_viz
+    const sfd3 = new StandardizedFormData(fd2);
+    const { formData: fd3 } = sfd3.transform('target_viz', {
+      ...sourceMockStore,
+      form_data: fd2,
+      controls: cs2,
+    });
+    expect(
+      fd3.standardizedFormData.memorizedFormData.map(
+        (fd: [string, QueryFormData]) => fd[0],
+      ),
+    ).toEqual(['target_viz', 'source_viz']);
+  });
+});
+
+describe('should transform form_data between table and bigNumberTotal', () => {
+  const tableVizFormData = {
+    datasource: '30__table',
+    viz_type: 'table',
+    time_grain_sqla: 'P1D',
+    time_range: 'No filter',
+    query_mode: 'aggregate',
+    groupby: ['name'],
+    metrics: ['count'],
+    all_columns: [],
+    percent_metrics: [],
+    adhoc_filters: [],
+    order_by_cols: [],
+    row_limit: 10000,
+    server_page_length: 10,
+    order_desc: true,
+    table_timestamp_format: 'smart_date',
+    show_cell_bars: true,
+    color_pn: true,
+    applied_time_extras: {},
+    url_params: {
+      form_data_key:
+        'p3No_sqDW7k-kMTzlBPAPd9vwp1IXTf6stbyzjlrPPa0ninvdYUUiMC6F1iKit3Y',
+      dataset_id: '30',
+    },
+  };
+  const tableVizStore = {
+    form_data: tableVizFormData,
+    controls: {
+      datasource: {
+        value: '30__table',
+      },
+      viz_type: {
+        value: 'table',
+      },
+      slice_id: {},
+      cache_timeout: {},
+      url_params: {
+        value: {
+          form_data_key:
+            'p3No_sqDW7k-kMTzlBPAPd9vwp1IXTf6stbyzjlrPPa0ninvdYUUiMC6F1iKit3Y',
+          dataset_id: '30',
+        },
+      },
+      granularity_sqla: {},
+      time_grain_sqla: {
+        value: 'P1D',
+      },
+      time_range: {
+        value: 'No filter',
+      },
+      query_mode: {
+        value: 'aggregate',
+      },
+      groupby: {
+        value: ['name'],
+      },
+      metrics: {
+        value: ['count'],
+      },
+      all_columns: {
+        value: [],
+      },
+      percent_metrics: {
+        value: [],
+      },
+      adhoc_filters: {
+        value: [],
+      },
+      timeseries_limit_metric: {},
+      order_by_cols: {
+        value: [],
+      },
+      server_pagination: {},
+      row_limit: {
+        value: 10000,
+      },
+      server_page_length: {
+        value: 10,
+      },
+      include_time: {},
+      order_desc: {
+        value: true,
+      },
+      show_totals: {},
+      emit_filter: {},
+      table_timestamp_format: {
+        value: 'smart_date',
+      },
+      page_length: {},
+      include_search: {},
+      show_cell_bars: {
+        value: true,
+      },
+      align_pn: {},
+      color_pn: {
+        value: true,
+      },
+      column_config: {},
+      conditional_formatting: {},
+    },
+    datasource: {
+      type: 'table',
+      columns: [],
+    },
+  };
+
+  beforeAll(() => {
+    getChartControlPanelRegistry().registerValue(
+      'big_number_total',
+      new BigNumberTotalChartPlugin().controlPanel,
+    );
+    getChartControlPanelRegistry().registerValue(
+      'table',
+      new TableChartPlugin().controlPanel,
+    );
+  });
+
+  test('transform', () => {
+    // table -> bigNumberTotal
+    const sfd = new StandardizedFormData(tableVizFormData);
+    const { formData: bntFormData, controlsState: bntControlsState } =
+      sfd.transform('big_number_total', tableVizStore);
+    expect(Object.keys(bntFormData).sort()).toEqual(
+      [...Object.keys(bntControlsState), 'standardizedFormData'].sort(),
+    );
+    expect(bntFormData.viz_type).toBe('big_number_total');
+    expect(bntFormData.metric).toBe('count');
+
+    // change control values
+    bntFormData.metric = 'sum(sales)';
+    bntFormData.time_range = '2021 : 2022';
+    bntControlsState.metric.value = 'sum(sales)';
+    bntControlsState.time_range.value = '2021 : 2022';
+
+    // bigNumberTotal -> table
+    const sfd2 = new StandardizedFormData(bntFormData);
+    const { formData: tblFormData, controlsState: tblControlsState } =
+      sfd2.transform('table', {
+        ...tableVizStore,
+        form_data: bntFormData,
+        controls: bntControlsState,
+      });
+    expect(Object.keys(tblFormData).sort()).toEqual(
+      [...Object.keys(tblControlsState), 'standardizedFormData'].sort(),
+    );
+    expect(tblFormData.viz_type).toBe('table');
+    expect(tblFormData.metrics).toEqual(['sum(sales)']);
+    expect(tblFormData.groupby).toEqual([]);
+    expect(tblFormData.time_range).toBe('2021 : 2022');
+  });
+});
diff --git a/superset-frontend/src/explore/controlUtils/standardizedFormData.ts b/superset-frontend/src/explore/controlUtils/standardizedFormData.ts
new file mode 100644
index 0000000000..c42ef95085
--- /dev/null
+++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.ts
@@ -0,0 +1,182 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  ensureIsArray,
+  getChartControlPanelRegistry,
+  QueryFormData,
+} from '@superset-ui/core';
+import {
+  ControlStateMapping,
+  StandardizedState,
+  StandardizedFormDataInterface,
+} from '@superset-ui/chart-controls';
+import { getControlsState } from 'src/explore/store';
+import { getFormDataFromControls } from './getFormDataFromControls';
+
+export const sharedControls: Record<keyof StandardizedState, string[]> = {
+  metrics: ['metric', 'metrics', 'metric_2'],
+  columns: ['groupby', 'columns', 'groupbyColumns', 'groupbyRows'],
+};
+export const publicControls = [
+  // time section
+  'granularity_sqla',
+  'time_grain_sqla',
+  'time_range',
+  // filters
+  'adhoc_filters',
+  // subquery limit(series limit)
+  'limit',
+  // order by clause
+  'timeseries_limit_metric',
+  'series_limit_metric',
+  // desc or asc in order by clause
+  'order_desc',
+  // outer query limit
+  'row_limit',
+  // x asxs column
+  'x_axis',
+  // advanced analytics - rolling window
+  'rolling_type',
+  'rolling_periods',
+  'min_periods',
+  // advanced analytics - time comparison
+  'time_compare',
+  'comparison_type',
+  // advanced analytics - resample
+  'resample_rule',
+  'resample_method',
+];
+
+export class StandardizedFormData {
+  private sfd: StandardizedFormDataInterface;
+
+  constructor(sourceFormData: QueryFormData) {
+    /*
+     * Support form_data for smooth switching between different viz
+     * */
+    const standardizedState = {
+      metrics: [],
+      columns: [],
+    };
+    const formData = Object.freeze(sourceFormData);
+    const reversedMap = StandardizedFormData.getReversedMap();
+
+    Object.entries(formData).forEach(([key, value]) => {
+      if (reversedMap.has(key)) {
+        standardizedState[reversedMap.get(key)].push(...ensureIsArray(value));
+      }
+    });
+
+    const memorizedFormData = Array.isArray(
+      formData?.standardizedFormData?.memorizedFormData,
+    )
+      ? new Map(formData.standardizedFormData.memorizedFormData)
+      : new Map();
+    const vizType = formData.viz_type;
+    if (memorizedFormData.has(vizType)) {
+      memorizedFormData.delete(vizType);
+    }
+    memorizedFormData.set(vizType, formData);
+    this.sfd = {
+      standardizedState,
+      memorizedFormData,
+    };
+  }
+
+  static getReversedMap() {
+    const reversedMap = new Map();
+    Object.entries(sharedControls).forEach(([key, names]) => {
+      names.forEach(name => {
+        reversedMap.set(name, key);
+      });
+    });
+    return reversedMap;
+  }
+
+  private getLatestFormData(vizType: string): QueryFormData {
+    if (this.sfd.memorizedFormData.has(vizType)) {
+      return this.sfd.memorizedFormData.get(vizType) as QueryFormData;
+    }
+
+    return this.memorizedFormData.slice(-1)[0][1];
+  }
+
+  private get standardizedState() {
+    return this.sfd.standardizedState;
+  }
+
+  private get memorizedFormData() {
+    return Array.from(this.sfd.memorizedFormData.entries());
+  }
+
+  dumpSFD() {
+    return {
+      standardizedState: this.standardizedState,
+      memorizedFormData: this.memorizedFormData,
+    };
+  }
+
+  transform(
+    targetVizType: string,
+    exploreState: Record<string, any>,
+  ): {
+    formData: QueryFormData;
+    controlsState: ControlStateMapping;
+  } {
+    /*
+     * Transfrom form_data between different viz. Return new form_data and controlsState.
+     * 1. get memorized form_data by viz type or get previous form_data
+     * 2. collect public control values
+     * 3. generate initial targetControlsState
+     * 4. attach `standardizedFormData` to the initial form_data
+     * 5. call denormalizeFormData to transform initial form_data if the plugin was defined
+     * 6. use final form_data to generate controlsState
+     * */
+    const latestFormData = this.getLatestFormData(targetVizType);
+    const publicFormData = {};
+    publicControls.forEach(key => {
+      if (key in exploreState.form_data) {
+        publicFormData[key] = exploreState.form_data[key];
+      }
+    });
+    const targetControlsState = getControlsState(exploreState, {
+      ...latestFormData,
+      ...publicFormData,
+      viz_type: targetVizType,
+    });
+    const targetFormData = {
+      ...getFormDataFromControls(targetControlsState),
+      standardizedFormData: this.dumpSFD(),
+    };
+
+    const controlPanel = getChartControlPanelRegistry().get(targetVizType);
+    if (controlPanel?.denormalizeFormData) {
+      const transformed = controlPanel.denormalizeFormData(targetFormData);
+      return {
+        formData: transformed,
+        controlsState: getControlsState(exploreState, transformed),
+      };
+    }
+
+    return {
+      formData: targetFormData,
+      controlsState: targetControlsState,
+    };
+  }
+}
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js b/superset-frontend/src/explore/reducers/exploreReducer.js
index 2897832ff0..818bb31c8c 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.js
+++ b/superset-frontend/src/explore/reducers/exploreReducer.js
@@ -23,9 +23,9 @@ import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
 import { getControlsState } from 'src/explore/store';
 import {
   getControlConfig,
-  getFormDataFromControls,
   getControlStateFromControlConfig,
   getControlValuesCompatibleWithDatasource,
+  StandardizedFormData,
 } from 'src/explore/controlUtils';
 import * as actions from 'src/explore/actions/exploreActions';
 
@@ -129,11 +129,10 @@ export default function exploreReducer(state = {}, action) {
       };
     },
     [actions.SET_FIELD_VALUE]() {
-      const new_form_data = state.form_data;
+      const { controlName, value, validationErrors } = action;
+      let new_form_data = { ...state.form_data, [controlName]: value };
       const old_metrics_data = state.form_data.metrics;
       const new_column_config = state.form_data.column_config;
-      const { controlName, value, validationErrors } = action;
-      new_form_data[controlName] = value;
 
       const vizType = new_form_data.viz_type;
 
@@ -204,18 +203,17 @@ export default function exploreReducer(state = {}, action) {
       });
       const hasErrors = errors && errors.length > 0;
 
-      const currentControlsState =
+      const isVizSwitch =
         action.controlName === 'viz_type' &&
-        action.value !== state.controls.viz_type.value
-          ? // rebuild the full control state if switching viz type
-            getControlsState(
-              state,
-              getFormDataFromControls({
-                ...state.controls,
-                viz_type: control,
-              }),
-            )
-          : state.controls;
+        action.value !== state.controls.viz_type.value;
+      let currentControlsState = state.controls;
+      if (isVizSwitch) {
+        // get StandardizedFormData from source form_data
+        const sfd = new StandardizedFormData(state.form_data);
+        const transformed = sfd.transform(action.value, state);
+        new_form_data = transformed.formData;
+        currentControlsState = transformed.controlsState;
+      }
 
       return {
         ...state,