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);
+}

Reply via email to