This is an automated email from the ASF dual-hosted git repository. kgabryje pushed a commit to branch what-if in repository https://gitbox.apache.org/repos/asf/superset.git
commit 263e20e439ca1b0fc309c807f4752aa18a83a801 Author: Kamil Gabryjelski <[email protected]> AuthorDate: Tue Dec 16 15:22:51 2025 +0100 initial frontend logic --- .../src/dashboard/actions/dashboardState.js | 11 ++ superset-frontend/src/dashboard/actions/hydrate.js | 1 + .../components/gridComponents/Chart/Chart.jsx | 5 + .../src/dashboard/reducers/dashboardState.js | 14 ++ superset-frontend/src/dashboard/types.ts | 15 ++ .../util/charts/getFormDataWithExtraFilters.ts | 32 +++- superset-frontend/src/dashboard/util/whatIf.ts | 171 +++++++++++++++++++++ 7 files changed, 248 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 18b567e20f..9327b612df 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -774,6 +774,17 @@ export function clearAllChartStates() { return { type: CLEAR_ALL_CHART_STATES }; } +// What-If Analysis actions +export const SET_WHAT_IF_MODIFICATIONS = 'SET_WHAT_IF_MODIFICATIONS'; +export function setWhatIfModifications(modifications) { + return { type: SET_WHAT_IF_MODIFICATIONS, modifications }; +} + +export const CLEAR_WHAT_IF_MODIFICATIONS = 'CLEAR_WHAT_IF_MODIFICATIONS'; +export function clearWhatIfModifications() { + return { type: CLEAR_WHAT_IF_MODIFICATIONS }; +} + // Undo history --------------------------------------------------------------- export const SET_MAX_UNDO_HISTORY_EXCEEDED = 'SET_MAX_UNDO_HISTORY_EXCEEDED'; export function setMaxUndoHistoryExceeded(maxUndoHistoryExceeded = true) { diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index b6f11988e1..a0e578ba45 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -312,6 +312,7 @@ export const hydrateDashboard = dashboardState?.datasetsStatus || ResourceStatus.Loading, chartStates: chartStates || dashboardState?.chartStates || {}, chartCustomizationItems, + whatIfModifications: [], }, dashboardLayout, }, diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx index 37e24b99f6..341f27b0de 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx @@ -367,6 +367,9 @@ const Chart = props => { state.dashboardInfo?.metadata?.shared_label_colors, ), ); + const whatIfModifications = useSelector( + state => state.dashboardState.whatIfModifications || EMPTY_ARRAY, + ); const formData = useMemo( () => @@ -386,6 +389,7 @@ const Chart = props => { labelsColorMap, sharedLabelsColors, ownColorScheme, + whatIfModifications, }), [ chart.id, @@ -403,6 +407,7 @@ const Chart = props => { labelsColorMap, sharedLabelsColors, ownColorScheme, + whatIfModifications, ], ); diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 70b5b6ab19..abdb6ccbee 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -54,6 +54,8 @@ import { REMOVE_CHART_STATE, RESTORE_CHART_STATES, CLEAR_ALL_CHART_STATES, + SET_WHAT_IF_MODIFICATIONS, + CLEAR_WHAT_IF_MODIFICATIONS, } from '../actions/dashboardState'; import { HYDRATE_DASHBOARD } from '../actions/hydrate'; @@ -333,6 +335,18 @@ export default function dashboardStateReducer(state = {}, action) { chartStates: {}, }; }, + [SET_WHAT_IF_MODIFICATIONS]() { + return { + ...state, + whatIfModifications: action.modifications, + }; + }, + [CLEAR_WHAT_IF_MODIFICATIONS]() { + return { + ...state, + whatIfModifications: [], + }; + }, }; if (action.type in actionHandlers) { diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index e1a589acdb..243f81b61a 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -129,6 +129,7 @@ export type DashboardState = { data: JsonObject; }; chartStates?: Record<string, any>; + whatIfModifications: WhatIfModification[]; }; export type DashboardInfo = { id: number; @@ -280,6 +281,20 @@ export type Slice = { created_by: { id: number }; }; +/** + * What-If Analysis types + */ +export interface WhatIfModification { + column: string; + multiplier: number; +} + +export interface WhatIfColumn { + columnName: string; + datasourceId: number; + usedByChartIds: number[]; +} + export enum MenuKeys { DownloadAsImage = 'download_as_image', ExploreChart = 'explore_chart', diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts index e29ec9a7b8..cd12d0ceee 100644 --- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts +++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts @@ -30,6 +30,7 @@ import { ChartConfiguration, ChartQueryPayload, ActiveFilters, + WhatIfModification, } from 'src/dashboard/types'; import { ChartCustomizationItem } from 'src/dashboard/components/nativeFilters/ChartCustomization/types'; import { getExtraFormData } from 'src/dashboard/components/nativeFilters/utils'; @@ -76,6 +77,7 @@ const cachedFormdataByChart: Record< CachedFormData & { dataMask: DataMask; extraControls: Record<string, string | boolean | null>; + whatIfModifications?: WhatIfModification[]; } > = {}; @@ -97,6 +99,7 @@ export interface GetFormDataWithExtraFiltersArguments { allSliceIds: number[]; chartCustomization?: JsonObject; activeFilters?: ActiveFilters; + whatIfModifications?: WhatIfModification[]; } const createFilterDataMapping = ( @@ -450,6 +453,7 @@ export default function getFormDataWithExtraFilters({ allSliceIds, chartCustomization, activeFilters: passedActiveFilters, + whatIfModifications, }: GetFormDataWithExtraFiltersArguments) { const cachedFormData = cachedFormdataByChart[sliceId]; const dataMaskEqual = areObjectsEqual(cachedFormData?.dataMask, dataMask, { @@ -476,7 +480,8 @@ export default function getFormDataWithExtraFilters({ }) && areObjectsEqual(cachedFormData?.chart_customization, chartCustomization, { ignoreUndefined: true, - }) + }) && + isEqual(cachedFormData?.whatIfModifications, whatIfModifications) ) { return cachedFormData; } @@ -518,6 +523,23 @@ export default function getFormDataWithExtraFilters({ } } + // Apply what-if modifications to charts that use the modified columns + // Note: what_if goes in 'extras', not 'extra_form_data', because the backend + // reads it from extras in get_sqla_query (superset/models/helpers.py) + let whatIfExtras: { what_if?: { modifications: WhatIfModification[] } } = {}; + if (whatIfModifications && whatIfModifications.length > 0) { + const chartColumns = buildExistingColumnsSet(chart as ChartQueryPayload); + const applicableModifications = whatIfModifications.filter(mod => + chartColumns.has(mod.column), + ); + + if (applicableModifications.length > 0) { + whatIfExtras = { + what_if: { modifications: applicableModifications }, + }; + } + } + let layerFilterScope: { [filterId: string]: number[] } | undefined; const isDeckMultiChart = chart.form_data?.viz_type === 'deck_multi'; @@ -571,6 +593,13 @@ export default function getFormDataWithExtraFilters({ ...groupByFormData, ...(chartCustomization && { chart_customization: chartCustomization }), ...(layerFilterScope && { layer_filter_scope: layerFilterScope }), + // Merge what-if into extras (backend reads from extras, not extra_form_data) + ...(whatIfExtras.what_if && { + extras: { + ...chart.form_data?.extras, + ...whatIfExtras, + }, + }), }; cachedFiltersByChart[sliceId] = filters; @@ -579,6 +608,7 @@ export default function getFormDataWithExtraFilters({ dataMask, extraControls, ...(chartCustomization && { chart_customization: chartCustomization }), + whatIfModifications, }; return formData; diff --git a/superset-frontend/src/dashboard/util/whatIf.ts b/superset-frontend/src/dashboard/util/whatIf.ts new file mode 100644 index 0000000000..53b83da462 --- /dev/null +++ b/superset-frontend/src/dashboard/util/whatIf.ts @@ -0,0 +1,171 @@ +/** + * 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, getColumnLabel } from '@superset-ui/core'; +import { GenericDataType } from '@apache-superset/core/api/core'; +import { ColumnMeta } from '@superset-ui/chart-controls'; +import { + ChartsState, + DatasourcesState, + ChartQueryPayload, + WhatIfColumn, +} from '../types'; + +/** + * Check if a column is numeric based on its type_generic field + */ +export function isNumericColumn(column: ColumnMeta): boolean { + return column.type_generic === GenericDataType.Numeric; +} + +/** + * Extract column names from a chart's form_data + * This includes columns from groupby, metrics, x_axis, series, filters, etc. + */ +export function extractColumnsFromChart(chart: ChartQueryPayload): Set<string> { + const columns = new Set<string>(); + const formData = chart.form_data; + if (!formData) return columns; + + // Helper to add column - handles both physical columns (strings) and adhoc columns + const addColumn = (col: any) => { + if (col) { + const label = getColumnLabel(col); + if (label) columns.add(label); + } + }; + + // Extract groupby columns (can be physical or adhoc) + ensureIsArray(formData.groupby).forEach(addColumn); + + // Extract x_axis column (can be physical or adhoc) + if (formData.x_axis) { + addColumn(formData.x_axis); + } + + // Extract metrics - get column names from metric definitions + ensureIsArray(formData.metrics).forEach((metric: any) => { + if (typeof metric === 'string') { + // Saved metric name - we can't extract columns from it + return; + } + if (metric && typeof metric === 'object' && 'column' in metric) { + const metricColumn = metric.column; + if (typeof metricColumn === 'string') { + columns.add(metricColumn); + } else if (metricColumn?.column_name) { + columns.add(metricColumn.column_name); + } + } + }); + + // Extract series column (can be physical or adhoc) + if (formData.series) { + addColumn(formData.series); + } + + // Extract entity column + if (formData.entity) { + addColumn(formData.entity); + } + + // Extract columns from filters + ensureIsArray(formData.adhoc_filters).forEach((filter: any) => { + if (filter?.subject && typeof filter.subject === 'string') { + columns.add(filter.subject); + } + }); + + // Extract columns array (used by some chart types like box_plot) + ensureIsArray(formData.columns).forEach(addColumn); + + return columns; +} + +/** + * Get the datasource key from a chart's form_data + * Format: "datasourceId__datasourceType" e.g., "2__table" + */ +export function getDatasourceKey(chart: ChartQueryPayload): string | null { + const datasource = chart.form_data?.datasource; + if (!datasource || typeof datasource !== 'string') return null; + return datasource; +} + +/** + * Get numeric columns used by charts on a dashboard + * Returns columns grouped by their usage across charts + */ +export function getNumericColumnsForDashboard( + charts: ChartsState, + datasources: DatasourcesState, +): WhatIfColumn[] { + const columnMap = new Map<string, WhatIfColumn>(); + + Object.values(charts).forEach(chart => { + const chartId = chart.id; + // Chart and ChartQueryPayload both have id and form_data, so we can safely access them + const chartPayload = { id: chart.id, form_data: chart.form_data }; + const datasourceKey = getDatasourceKey(chartPayload); + if (!datasourceKey) return; + + const datasource = datasources[datasourceKey]; + if (!datasource?.columns) return; + + // Extract columns referenced by this chart + const referencedColumns = extractColumnsFromChart(chartPayload); + + // For each referenced column, check if it's numeric + referencedColumns.forEach(colName => { + const colMetadata = datasource.columns.find( + (c: ColumnMeta) => c.column_name === colName, + ); + + if (colMetadata && isNumericColumn(colMetadata)) { + // Create a unique key for this column (datasource + column name) + const key = `${datasource.id}:${colName}`; + + if (!columnMap.has(key)) { + columnMap.set(key, { + columnName: colName, + datasourceId: datasource.id, + usedByChartIds: [chartId], + }); + } else { + const existing = columnMap.get(key)!; + if (!existing.usedByChartIds.includes(chartId)) { + existing.usedByChartIds.push(chartId); + } + } + } + }); + }); + + return Array.from(columnMap.values()); +} + +/** + * Check if a chart uses a specific column + */ +export function chartUsesColumn( + chart: ChartQueryPayload, + columnName: string, +): boolean { + const columns = extractColumnsFromChart(chart); + return columns.has(columnName); +}
