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/09/28 07:33:00 UTC

[superset] branch master updated: feat: explicit distribute columns on BoxPlot and apply time grain (#21593)

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 93f08e778b feat: explicit distribute columns on BoxPlot and apply time grain (#21593)
93f08e778b is described below

commit 93f08e778bfd48be150749f22d0b184467da73ac
Author: Yongjie Zhao <yo...@apache.org>
AuthorDate: Wed Sep 28 15:32:35 2022 +0800

    feat: explicit distribute columns on BoxPlot and apply time grain (#21593)
---
 .../superset-ui-chart-controls/src/index.ts        |   2 +-
 .../src/shared-controls/dndControls.tsx            |   2 +-
 .../shared-controls/{constants.tsx => mixins.tsx}  |  30 +----
 .../superset-ui-chart-controls/src/types.ts        |  11 +-
 .../src/utils/getTemporalColumns.ts                |  64 +++++++++
 .../superset-ui-chart-controls/src/utils/index.ts  |   1 +
 .../superset-ui-chart-controls/test/fixtures.ts    | 149 +++++++++++++++++++++
 .../test/utils/columnChoices.test.tsx              |   4 +-
 .../test/utils/getTemporalColumns.test.ts          |  95 +++++++++++++
 .../superset-ui-core/src/query/types/Query.ts      |  54 +++++++-
 .../packages/superset-ui-core/src/types/index.ts   |   2 +
 .../plugin-chart-echarts/src/BoxPlot/buildQuery.ts |  54 +++++---
 .../src/BoxPlot/controlPanel.ts                    |  61 ++++++++-
 13 files changed, 466 insertions(+), 63 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/index.ts b/superset-frontend/packages/superset-ui-chart-controls/src/index.ts
index 66ef14917a..6d22c2a7ce 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/index.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/index.ts
@@ -37,4 +37,4 @@ export { legacySortBy } from './shared-controls/legacySortBy';
 export * from './shared-controls/emitFilterControl';
 export * from './shared-controls/components';
 export * from './types';
-export { xAxisMixin, temporalColumnMixin } from './shared-controls/constants';
+export { xAxisMixin, temporalColumnMixin } from './shared-controls/mixins';
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx
index 691dbca7c0..35a7f1979e 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx
@@ -40,7 +40,7 @@ import {
   FilterOption,
   temporalColumnMixin,
 } from '..';
-import { xAxisMixin } from './constants';
+import { xAxisMixin } from './mixins';
 
 type Control = {
   savedMetrics?: Metric[] | null;
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/constants.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/mixins.tsx
similarity index 73%
rename from superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/constants.tsx
rename to superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/mixins.tsx
index 8de12bfcf6..4963be0122 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/constants.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/mixins.tsx
@@ -20,16 +20,11 @@ import {
   FeatureFlag,
   isFeatureEnabled,
   QueryFormData,
-  QueryResponse,
   t,
   validateNonEmpty,
 } from '@superset-ui/core';
-import {
-  BaseControlConfig,
-  ControlPanelState,
-  ControlState,
-  Dataset,
-} from '../types';
+import { BaseControlConfig, ControlPanelState, ControlState } from '../types';
+import { getTemporalColumns } from '../utils';
 
 const getAxisLabel = (
   formData: QueryFormData,
@@ -60,24 +55,11 @@ export const xAxisMixin = {
 
 export const temporalColumnMixin: Pick<BaseControlConfig, 'mapStateToProps'> = {
   mapStateToProps: ({ datasource }) => {
-    if (datasource?.columns[0]?.hasOwnProperty('column_name')) {
-      const temporalColumns =
-        (datasource as Dataset)?.columns?.filter(c => c.is_dttm) ?? [];
-      return {
-        options: temporalColumns,
-        default:
-          (datasource as Dataset)?.main_dttm_col ||
-          temporalColumns[0]?.column_name ||
-          null,
-        isTemporal: true,
-      };
-    }
-    const sortedQueryColumns = (datasource as QueryResponse)?.columns?.sort(
-      query => (query?.is_dttm ? -1 : 1),
-    );
+    const payload = getTemporalColumns(datasource);
+
     return {
-      options: sortedQueryColumns,
-      default: sortedQueryColumns[0]?.name || null,
+      options: payload.temporalColumns,
+      default: payload.defaultTemporalColumn,
       isTemporal: true,
     };
   },
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 99ea6a5325..8c9ce71874 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -24,6 +24,7 @@ import type {
   DatasourceType,
   JsonValue,
   Metric,
+  QueryColumn,
   QueryFormColumn,
   QueryFormData,
   QueryFormMetric,
@@ -449,9 +450,9 @@ export type ColorFormatters = {
 export default {};
 
 export function isColumnMeta(
-  column: AdhocColumn | ColumnMeta,
+  column: AdhocColumn | ColumnMeta | QueryColumn,
 ): column is ColumnMeta {
-  return 'column_name' in column;
+  return !!column && 'column_name' in column;
 }
 
 export function isSavedExpression(
@@ -477,9 +478,5 @@ export function isDataset(
 export function isQueryResponse(
   datasource: Dataset | QueryResponse | null | undefined,
 ): datasource is QueryResponse {
-  return (
-    !!datasource &&
-    ('results' in datasource ||
-      datasource?.type === ('query' as DatasourceType.Query))
-  );
+  return !!datasource && 'results' in datasource && 'sql' in datasource;
 }
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getTemporalColumns.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getTemporalColumns.ts
new file mode 100644
index 0000000000..caae8dfd52
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getTemporalColumns.ts
@@ -0,0 +1,64 @@
+/**
+ * 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,
+  isDefined,
+  QueryColumn,
+  ValueOf,
+} from '@superset-ui/core';
+import {
+  ColumnMeta,
+  ControlPanelState,
+  isDataset,
+  isQueryResponse,
+} from '@superset-ui/chart-controls';
+
+export const getTemporalColumns = (
+  datasource: ValueOf<Pick<ControlPanelState, 'datasource'>>,
+) => {
+  const rv: {
+    temporalColumns: ColumnMeta[] | QueryColumn[];
+    defaultTemporalColumn: string | null | undefined;
+  } = {
+    temporalColumns: [],
+    defaultTemporalColumn: undefined,
+  };
+
+  if (isDataset(datasource)) {
+    rv.temporalColumns = ensureIsArray(datasource.columns).filter(
+      c => c.is_dttm,
+    );
+  }
+  if (isQueryResponse(datasource)) {
+    rv.temporalColumns = ensureIsArray(datasource.columns).filter(
+      c => c.is_dttm,
+    );
+  }
+
+  if (isDataset(datasource)) {
+    rv.defaultTemporalColumn = datasource.main_dttm_col;
+  }
+  if (!isDefined(rv.defaultTemporalColumn)) {
+    rv.defaultTemporalColumn =
+      (rv.temporalColumns[0] as ColumnMeta)?.column_name ??
+      (rv.temporalColumns[0] as QueryColumn)?.name;
+  }
+
+  return rv;
+};
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
index bd5fae8030..ff0b8db638 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
@@ -24,3 +24,4 @@ export { default as mainMetric } from './mainMetric';
 export { default as columnChoices } from './columnChoices';
 export * from './defineSavedMetrics';
 export * from './getStandardizedControls';
+export { getTemporalColumns } from './getTemporalColumns';
diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/fixtures.ts b/superset-frontend/packages/superset-ui-chart-controls/test/fixtures.ts
new file mode 100644
index 0000000000..d694bde883
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-chart-controls/test/fixtures.ts
@@ -0,0 +1,149 @@
+/**
+ * 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 { Dataset } from '@superset-ui/chart-controls';
+import { DatasourceType } from '@superset-ui/core';
+
+export const TestDataset: Dataset = {
+  column_format: {},
+  columns: [
+    {
+      advanced_data_type: null,
+      certification_details: null,
+      certified_by: null,
+      column_name: 'num',
+      description: null,
+      expression: '',
+      filterable: true,
+      groupby: true,
+      id: 332,
+      is_certified: false,
+      is_dttm: false,
+      python_date_format: null,
+      type: 'BIGINT',
+      type_generic: 0,
+      verbose_name: null,
+      warning_markdown: null,
+    },
+    {
+      advanced_data_type: null,
+      certification_details: null,
+      certified_by: null,
+      column_name: 'gender',
+      description: null,
+      expression: '',
+      filterable: true,
+      groupby: true,
+      id: 330,
+      is_certified: false,
+      is_dttm: false,
+      python_date_format: null,
+      type: 'VARCHAR(16)',
+      type_generic: 1,
+      verbose_name: '',
+      warning_markdown: null,
+    },
+    {
+      advanced_data_type: null,
+      certification_details: null,
+      certified_by: null,
+      column_name: 'state',
+      description: null,
+      expression: '',
+      filterable: true,
+      groupby: true,
+      id: 333,
+      is_certified: false,
+      is_dttm: false,
+      python_date_format: null,
+      type: 'VARCHAR(10)',
+      type_generic: 1,
+      verbose_name: null,
+      warning_markdown: null,
+    },
+    {
+      advanced_data_type: null,
+      certification_details: null,
+      certified_by: null,
+      column_name: 'ds',
+      description: null,
+      expression: '',
+      filterable: true,
+      groupby: true,
+      id: 329,
+      is_certified: false,
+      is_dttm: true,
+      python_date_format: null,
+      type: 'TIMESTAMP WITHOUT TIME ZONE',
+      type_generic: 2,
+      verbose_name: null,
+      warning_markdown: null,
+    },
+    {
+      advanced_data_type: null,
+      certification_details: null,
+      certified_by: null,
+      column_name: 'name',
+      description: null,
+      expression: '',
+      filterable: true,
+      groupby: true,
+      id: 331,
+      is_certified: false,
+      is_dttm: false,
+      python_date_format: null,
+      type: 'VARCHAR(255)',
+      type_generic: 1,
+      verbose_name: null,
+      warning_markdown: null,
+    },
+  ],
+  datasource_name: 'birth_names',
+  description: null,
+  granularity_sqla: 'ds',
+  id: 2,
+  main_dttm_col: 'ds',
+  metrics: [
+    {
+      certification_details: null,
+      certified_by: null,
+      d3format: null,
+      description: null,
+      expression: 'COUNT(*)',
+      id: 7,
+      is_certified: false,
+      metric_name: 'count',
+      verbose_name: 'COUNT(*)',
+      warning_markdown: '',
+      warning_text: null,
+    },
+  ],
+  name: 'public.birth_names',
+  order_by_choices: [],
+  owners: [
+    {
+      first_name: 'admin',
+      id: 1,
+      last_name: 'user',
+      username: 'admin',
+    },
+  ],
+  type: DatasourceType.Dataset,
+  uid: '2__table',
+  verbose_map: {},
+};
diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
index 3224bbcc26..59f4796a44 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DatasourceType, QueryResponse, testQuery } from '@superset-ui/core';
+import { DatasourceType, testQueryResponse } from '@superset-ui/core';
 import { columnChoices } from '../../src';
 
 describe('columnChoices()', () => {
@@ -58,7 +58,7 @@ describe('columnChoices()', () => {
   });
 
   it('should convert columns to choices when source is a Query', () => {
-    expect(columnChoices(testQuery as QueryResponse)).toEqual([
+    expect(columnChoices(testQueryResponse)).toEqual([
       ['Column 1', 'Column 1'],
       ['Column 2', 'Column 2'],
       ['Column 3', 'Column 3'],
diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getTemporalColumns.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getTemporalColumns.test.ts
new file mode 100644
index 0000000000..79dee957d1
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getTemporalColumns.test.ts
@@ -0,0 +1,95 @@
+/**
+ * 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 { testQueryResponse, testQueryResults } from '@superset-ui/core';
+import { Dataset, getTemporalColumns } from '../../src';
+import { TestDataset } from '../fixtures';
+
+test('get temporal columns from a Dataset', () => {
+  expect(getTemporalColumns(TestDataset)).toEqual({
+    temporalColumns: [
+      {
+        advanced_data_type: null,
+        certification_details: null,
+        certified_by: null,
+        column_name: 'ds',
+        description: null,
+        expression: '',
+        filterable: true,
+        groupby: true,
+        id: 329,
+        is_certified: false,
+        is_dttm: true,
+        python_date_format: null,
+        type: 'TIMESTAMP WITHOUT TIME ZONE',
+        type_generic: 2,
+        verbose_name: null,
+        warning_markdown: null,
+      },
+    ],
+    defaultTemporalColumn: 'ds',
+  });
+});
+
+test('get temporal columns from a QueryResponse', () => {
+  expect(getTemporalColumns(testQueryResponse)).toEqual({
+    temporalColumns: [
+      {
+        name: 'Column 2',
+        type: 'TIMESTAMP',
+        is_dttm: true,
+      },
+    ],
+    defaultTemporalColumn: 'Column 2',
+  });
+});
+
+test('get temporal columns from null', () => {
+  expect(getTemporalColumns(null)).toEqual({
+    temporalColumns: [],
+    defaultTemporalColumn: undefined,
+  });
+});
+
+test('should accept empty Dataset or queryResponse', () => {
+  expect(
+    getTemporalColumns({
+      ...TestDataset,
+      ...{
+        columns: [],
+        main_dttm_col: undefined,
+      },
+    } as any as Dataset),
+  ).toEqual({
+    temporalColumns: [],
+    defaultTemporalColumn: undefined,
+  });
+
+  expect(
+    getTemporalColumns({
+      ...testQueryResponse,
+      ...{
+        columns: [],
+        results: { ...testQueryResults.results, ...{ columns: [] } },
+      },
+    }),
+  ).toEqual({
+    temporalColumns: [],
+    defaultTemporalColumn: undefined,
+  });
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
index 6c86b397fd..a2d6df39b8 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
@@ -350,6 +350,7 @@ export type QueryResults = {
 
 export type QueryResponse = Query & QueryResults;
 
+// todo: move out from typing
 export const testQuery: Query = {
   id: 'clientId2353',
   dbId: 1,
@@ -388,22 +389,69 @@ export const testQuery: Query = {
   columns: [
     {
       name: 'Column 1',
-      type: DatasourceType.Query,
+      type: 'STRING',
       is_dttm: false,
     },
     {
       name: 'Column 3',
-      type: DatasourceType.Query,
+      type: 'STRING',
       is_dttm: false,
     },
     {
       name: 'Column 2',
-      type: DatasourceType.Query,
+      type: 'TIMESTAMP',
       is_dttm: true,
     },
   ],
 };
 
+export const testQueryResults = {
+  results: {
+    displayLimitReached: false,
+    columns: [
+      {
+        name: 'Column 1',
+        type: 'STRING',
+        is_dttm: false,
+      },
+      {
+        name: 'Column 3',
+        type: 'STRING',
+        is_dttm: false,
+      },
+      {
+        name: 'Column 2',
+        type: 'TIMESTAMP',
+        is_dttm: true,
+      },
+    ],
+    data: [
+      { 'Column 1': 'a', 'Column 2': 'b', 'Column 3': '2014-11-11T00:00:00' },
+    ],
+    expanded_columns: [],
+    selected_columns: [
+      {
+        name: 'Column 1',
+        type: 'STRING',
+        is_dttm: false,
+      },
+      {
+        name: 'Column 3',
+        type: 'STRING',
+        is_dttm: false,
+      },
+      {
+        name: 'Column 2',
+        type: 'TIMESTAMP',
+        is_dttm: true,
+      },
+    ],
+    query: { limit: 6 },
+  },
+};
+
+export const testQueryResponse = { ...testQuery, ...testQueryResults };
+
 export enum ContributionType {
   Row = 'row',
   Column = 'column',
diff --git a/superset-frontend/packages/superset-ui-core/src/types/index.ts b/superset-frontend/packages/superset-ui-core/src/types/index.ts
index eaab5c0b49..7c75ad42cc 100644
--- a/superset-frontend/packages/superset-ui-core/src/types/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/types/index.ts
@@ -19,3 +19,5 @@
 export * from '../query/types';
 
 export type Maybe<T> = T | null;
+
+export type ValueOf<T> = T[keyof T];
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/buildQuery.ts
index 14ce144d61..a897eea156 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/buildQuery.ts
@@ -16,26 +16,44 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { buildQueryContext } from '@superset-ui/core';
+import {
+  AdhocColumn,
+  buildQueryContext,
+  ensureIsArray,
+  isPhysicalColumn,
+} from '@superset-ui/core';
 import { boxplotOperator } from '@superset-ui/chart-controls';
 import { BoxPlotQueryFormData } from './types';
 
 export default function buildQuery(formData: BoxPlotQueryFormData) {
-  const { columns = [], granularity_sqla, groupby = [] } = formData;
-  return buildQueryContext(formData, baseQueryObject => {
-    const distributionColumns: string[] = [];
-    // For now default to using the temporal column as distribution column.
-    // In the future this control should be made mandatory.
-    if (!columns.length && granularity_sqla) {
-      distributionColumns.push(granularity_sqla);
-    }
-    return [
-      {
-        ...baseQueryObject,
-        columns: [...distributionColumns, ...columns, ...groupby],
-        series_columns: groupby,
-        post_processing: [boxplotOperator(formData, baseQueryObject)],
-      },
-    ];
-  });
+  return buildQueryContext(formData, baseQueryObject => [
+    {
+      ...baseQueryObject,
+      columns: [
+        ...(ensureIsArray(formData.columns).length === 0 &&
+        formData.granularity_sqla
+          ? [formData.granularity_sqla] // for backwards compatible: if columns control is empty and granularity_sqla was set, the time columns is default distributed column.
+          : ensureIsArray(formData.columns)
+        ).map(col => {
+          if (
+            isPhysicalColumn(col) &&
+            formData.time_grain_sqla &&
+            formData?.datetime_columns_lookup?.[col]
+          ) {
+            return {
+              timeGrain: formData.time_grain_sqla,
+              columnType: 'BASE_AXIS',
+              sqlExpression: col,
+              label: col,
+              expressionType: 'SQL',
+            } as AdhocColumn;
+          }
+          return col;
+        }),
+        ...ensureIsArray(formData.groupby),
+      ],
+      series_columns: formData.groupby,
+      post_processing: [boxplotOperator(formData, baseQueryObject)],
+    },
+  ]);
 }
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts
index 8a85c42bfa..b439a9888a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts
@@ -16,7 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ensureIsArray, t } from '@superset-ui/core';
+import {
+  ensureIsArray,
+  isAdhocColumn,
+  isPhysicalColumn,
+  t,
+  validateNonEmpty,
+} from '@superset-ui/core';
 import {
   D3_FORMAT_DOCS,
   D3_FORMAT_OPTIONS,
@@ -26,20 +32,53 @@ import {
   emitFilterControl,
   ControlPanelConfig,
   getStandardizedControls,
+  ControlState,
+  ControlPanelState,
+  getTemporalColumns,
+  sharedControls,
 } from '@superset-ui/chart-controls';
 
 const config: ControlPanelConfig = {
   controlPanelSections: [
-    sections.legacyTimeseriesTime,
+    sections.legacyRegularTime,
     {
       label: t('Query'),
       expanded: true,
       controlSetRows: [
+        ['columns'],
+        [
+          {
+            name: 'time_grain_sqla',
+            config: {
+              ...sharedControls.time_grain_sqla,
+              visibility: ({ controls }) => {
+                const dttmLookup = Object.fromEntries(
+                  ensureIsArray(controls?.columns?.options).map(option => [
+                    option.column_name,
+                    option.is_dttm,
+                  ]),
+                );
+
+                return ensureIsArray(controls?.columns.value)
+                  .map(selection => {
+                    if (isAdhocColumn(selection)) {
+                      return true;
+                    }
+                    if (isPhysicalColumn(selection)) {
+                      return !!dttmLookup[selection];
+                    }
+                    return false;
+                  })
+                  .some(Boolean);
+              },
+            },
+          },
+          'datetime_columns_lookup',
+        ],
+        ['groupby'],
         ['metrics'],
         ['adhoc_filters'],
         emitFilterControl,
-        ['groupby'],
-        ['columns'], // TODO: this should be migrated to `series_columns`
         ['series_limit'],
         ['series_limit_metric'],
         [
@@ -132,9 +171,17 @@ const config: ControlPanelConfig = {
     columns: {
       label: t('Distribute across'),
       multi: true,
-      description: t(
-        'Columns to calculate distribution across. Defaults to temporal column if left empty.',
-      ),
+      description: t('Columns to calculate distribution across.'),
+      initialValue: (control: ControlState, state: ControlPanelState) => {
+        if (
+          (state && !control?.value) ||
+          (Array.isArray(control?.value) && control.value.length === 0)
+        ) {
+          return [getTemporalColumns(state.datasource).defaultTemporalColumn];
+        }
+        return control.value;
+      },
+      validators: [validateNonEmpty],
     },
   },
   formDataOverrides: formData => {