You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/07/03 05:19:43 UTC
(superset) branch legacy-viz-t2-time-table created (now ebdff9f840b)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a change to branch legacy-viz-t2-time-table
in repository https://gitbox.apache.org/repos/asf/superset.git
at ebdff9f840b feat(time-table): migrate time_table chart to v1 chart data API
This branch includes the following new commits:
new ebdff9f840b feat(time-table): migrate time_table chart to v1 chart data API
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(superset) 01/01: feat(time-table): migrate time_table chart to v1 chart data API
Posted by ru...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch legacy-viz-t2-time-table
in repository https://gitbox.apache.org/repos/asf/superset.git
commit ebdff9f840bff886f9e3ca07141440e370d227c3
Author: Claude Code <no...@anthropic.com>
AuthorDate: Thu Jul 2 22:19:12 2026 -0700
feat(time-table): migrate time_table chart to v1 chart data API
Adds a buildQuery mirroring the legacy TimeTableViz.query_obj (a
timeseries query ordered by the first metric, ascending unless
order_desc) and ports the pandas pivot from the backend get_data into a
transformData helper: records keyed by the stringified timestamp, one
column per metric, or per group value of the single metric when grouped
(multi-level names joined with the flat column separator, commas
escaped). Missing combinations are padded with null and legacy
pre-shaped payloads pass through untouched.
No DB migration needed: viz_type is unchanged and legacy
granularity_sqla/time_range form data flows through extractExtras.
Co-Authored-By: Claude Fable 5 <no...@anthropic.com>
---
.../TimeTable/config/buildQuery.test.ts | 47 ++++++++++
.../visualizations/TimeTable/config/buildQuery.ts | 42 +++++++++
.../config/transformProps/transformData.test.ts | 71 +++++++++++++++
.../config/transformProps/transformData.ts | 100 +++++++++++++++++++++
.../config/transformProps/transformProps.ts | 22 ++++-
.../src/visualizations/TimeTable/index.ts | 3 +-
6 files changed, 281 insertions(+), 4 deletions(-)
diff --git a/superset-frontend/src/visualizations/TimeTable/config/buildQuery.test.ts b/superset-frontend/src/visualizations/TimeTable/config/buildQuery.test.ts
new file mode 100644
index 00000000000..84869289129
--- /dev/null
+++ b/superset-frontend/src/visualizations/TimeTable/config/buildQuery.test.ts
@@ -0,0 +1,47 @@
+/**
+ * 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 { QueryFormData } from '@superset-ui/core';
+import buildQuery from './buildQuery';
+
+const formData: QueryFormData = {
+ datasource: '5__table',
+ granularity_sqla: 'ds',
+ time_grain_sqla: 'P1D',
+ time_range: 'No filter',
+ viz_type: 'time_table',
+ metrics: ['sum__num', 'count'],
+};
+
+test('builds a timeseries query ordered by the first metric ascending', () => {
+ const [query] = buildQuery(formData).queries;
+ expect(query.metrics).toEqual(['sum__num', 'count']);
+ expect(query.is_timeseries).toBe(true);
+ expect(query.orderby).toEqual([['sum__num', true]]);
+ expect(query.granularity).toEqual('ds');
+});
+
+test('orders descending when order_desc is set', () => {
+ const [query] = buildQuery({ ...formData, order_desc: true }).queries;
+ expect(query.orderby).toEqual([['sum__num', false]]);
+});
+
+test('keeps the groupby columns for grouped mode', () => {
+ const [query] = buildQuery({ ...formData, groupby: ['gender'] }).queries;
+ expect(query.columns).toEqual(['gender']);
+});
diff --git a/superset-frontend/src/visualizations/TimeTable/config/buildQuery.ts b/superset-frontend/src/visualizations/TimeTable/config/buildQuery.ts
new file mode 100644
index 00000000000..ef25a137481
--- /dev/null
+++ b/superset-frontend/src/visualizations/TimeTable/config/buildQuery.ts
@@ -0,0 +1,42 @@
+/**
+ * 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 {
+ buildQueryContext,
+ ensureIsArray,
+ QueryFormData,
+} from '@superset-ui/core';
+
+/**
+ * Mirrors the legacy TimeTableViz.query_obj: a timeseries query ordered
+ * by the first metric (ascending unless order_desc).
+ */
+export default function buildQuery(formData: QueryFormData) {
+ const { order_desc } = formData;
+ return buildQueryContext(formData, baseQueryObject => {
+ const metrics = ensureIsArray(baseQueryObject.metrics);
+ const firstMetric = metrics[0];
+ return [
+ {
+ ...baseQueryObject,
+ is_timeseries: true,
+ orderby: firstMetric ? [[firstMetric, !order_desc]] : undefined,
+ },
+ ];
+ });
+}
diff --git a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts
new file mode 100644
index 00000000000..3451da7a737
--- /dev/null
+++ b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts
@@ -0,0 +1,71 @@
+/**
+ * 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 transformData, { formatTimestamp } from './transformData';
+
+const jan1 = 1704067200000; // 2024-01-01T00:00:00Z
+const jan2 = 1704153600000; // 2024-01-02T00:00:00Z
+
+test('formats timestamps like str(pandas.Timestamp)', () => {
+ expect(formatTimestamp(jan1)).toEqual('2024-01-01 00:00:00');
+});
+
+test('pivots metrics into records keyed by time when ungrouped', () => {
+ const { records, columns, is_group_by } = transformData(
+ [
+ { __timestamp: jan1, sum__num: 1, count: 2 },
+ { __timestamp: jan2, sum__num: 3, count: 4 },
+ ],
+ [],
+ ['sum__num', 'count'],
+ );
+ expect(is_group_by).toBe(false);
+ expect(columns).toEqual(['count', 'sum__num']);
+ expect(records).toEqual({
+ '2024-01-01 00:00:00': { sum__num: 1, count: 2 },
+ '2024-01-02 00:00:00': { sum__num: 3, count: 4 },
+ });
+});
+
+test('pivots single metric per group value when grouped', () => {
+ const { records, columns, is_group_by } = transformData(
+ [
+ { __timestamp: jan1, gender: 'boy', sum__num: 1 },
+ { __timestamp: jan1, gender: 'girl', sum__num: 2 },
+ { __timestamp: jan2, gender: 'boy', sum__num: 3 },
+ // girl missing on jan2 -> padded with null
+ ],
+ ['gender'],
+ ['sum__num'],
+ );
+ expect(is_group_by).toBe(true);
+ expect(columns).toEqual(['boy', 'girl']);
+ expect(records).toEqual({
+ '2024-01-01 00:00:00': { boy: 1, girl: 2 },
+ '2024-01-02 00:00:00': { boy: 3, girl: null },
+ });
+});
+
+test('joins multi-column groups with the flat column separator', () => {
+ const { columns } = transformData(
+ [{ __timestamp: jan1, gender: 'boy', state: 'CA, USA', sum__num: 1 }],
+ ['gender', 'state'],
+ ['sum__num'],
+ );
+ expect(columns).toEqual(['boy, CA\\, USA']);
+});
diff --git a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts
new file mode 100644
index 00000000000..88689aa1d14
--- /dev/null
+++ b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts
@@ -0,0 +1,100 @@
+/**
+ * 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 { DTTM_ALIAS } from '@superset-ui/core';
+
+const FLAT_COLUMN_SEPARATOR = ', ';
+
+const escapeSeparator = (value: string) =>
+ value.replace(new RegExp(FLAT_COLUMN_SEPARATOR, 'g'), '\\, ');
+
+const pad = (part: number) => String(part).padStart(2, '0');
+
+/**
+ * Formats an epoch timestamp the way str(pandas.Timestamp) did on the
+ * legacy endpoint ("YYYY-MM-DD HH:MM:SS"), which the TimeTable component
+ * re-parses with new Date().
+ */
+export const formatTimestamp = (value: unknown): string => {
+ const date = new Date(value as number);
+ if (Number.isNaN(date.getTime())) {
+ return String(value);
+ }
+ return (
+ `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-` +
+ `${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:` +
+ `${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`
+ );
+};
+
+export interface TimeTablePayload {
+ records: Record<string, Record<string, unknown>>;
+ columns: string[];
+ is_group_by: boolean;
+}
+
+/**
+ * Ports the legacy TimeTableViz.get_data pandas pivot: rows keyed by the
+ * stringified timestamp, one column per metric, or per group value of the
+ * single metric when grouped (multi-level group names joined with ", ",
+ * commas escaped, matching FLAT_COLUMN_SEPARATOR semantics).
+ */
+export default function transformData(
+ records: Record<string, unknown>[],
+ groupbyLabels: string[],
+ metricLabels: string[],
+): TimeTablePayload {
+ const isGroupBy = groupbyLabels.length > 0;
+ const pivoted: Record<string, Record<string, unknown>> = {};
+ const columnSet = new Set<string>();
+
+ records.forEach(record => {
+ const timeKey = formatTimestamp(record[DTTM_ALIAS]);
+ if (!pivoted[timeKey]) {
+ pivoted[timeKey] = {};
+ }
+ if (isGroupBy) {
+ const column =
+ groupbyLabels.length === 1
+ ? String(record[groupbyLabels[0]])
+ : groupbyLabels
+ .map(label => escapeSeparator(String(record[label])))
+ .join(FLAT_COLUMN_SEPARATOR);
+ columnSet.add(column);
+ pivoted[timeKey][column] = record[metricLabels[0]];
+ } else {
+ metricLabels.forEach(metric => {
+ columnSet.add(metric);
+ pivoted[timeKey][metric] = record[metric];
+ });
+ }
+ });
+
+ // pandas pivot pads missing combinations with NaN -> null
+ const columns = Array.from(columnSet).sort();
+ Object.values(pivoted).forEach(row => {
+ columns.forEach(column => {
+ if (!(column in row)) {
+ // eslint-disable-next-line no-param-reassign
+ row[column] = null;
+ }
+ });
+ });
+
+ return { records: pivoted, columns, is_group_by: isGroupBy };
+}
diff --git a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformProps.ts b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformProps.ts
index d4d6d8e51f2..b433264384c 100644
--- a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformProps.ts
+++ b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformProps.ts
@@ -16,7 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ChartProps, DataRecord, Metric } from '@superset-ui/core';
+import {
+ ChartProps,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ Metric,
+ QueryFormMetric,
+} from '@superset-ui/core';
+import transformData from './transformData';
interface FormData {
groupby: string[];
@@ -44,7 +53,16 @@ interface ColumnData {
export function transformProps(chartProps: TableChartProps) {
const { height, datasource, formData, queriesData } = chartProps;
const { columnCollection = [], groupby, metrics, url } = formData;
- const { records, columns } = queriesData[0].data;
+ // v1 chart data responses arrive as flat records; the legacy explore_json
+ // endpoint delivered a pre-pivoted {records, columns} payload.
+ const rawData = queriesData[0].data;
+ const { records, columns } = Array.isArray(rawData)
+ ? transformData(
+ rawData as DataRecord[],
+ ensureIsArray(groupby).map(getColumnLabel),
+ ensureIsArray(metrics as QueryFormMetric[]).map(getMetricLabel),
+ )
+ : rawData;
const isGroupBy = groupby?.length > 0;
let rows;
diff --git a/superset-frontend/src/visualizations/TimeTable/index.ts b/superset-frontend/src/visualizations/TimeTable/index.ts
index 3dcf0f3cb4f..07a7023c8a9 100644
--- a/superset-frontend/src/visualizations/TimeTable/index.ts
+++ b/superset-frontend/src/visualizations/TimeTable/index.ts
@@ -34,7 +34,6 @@ const metadata = new ChartMetadata({
tags: [
t('Multi-Variables'),
t('Comparison'),
- t('Legacy'),
t('Percentages'),
t('Tabular'),
t('Text'),
@@ -42,7 +41,6 @@ const metadata = new ChartMetadata({
],
thumbnail,
thumbnailDark,
- useLegacyApi: true,
});
export default class TimeTableChartPlugin extends ChartPlugin {
@@ -50,6 +48,7 @@ export default class TimeTableChartPlugin extends ChartPlugin {
super({
metadata,
transformProps,
+ loadBuildQuery: () => import('./config/buildQuery'),
loadChart: () => import('./TimeTable'),
controlPanel,
});