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

kgabryje 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 d75ff9e784 feat(charts): add subtitle option and metric customization 
controls (#32975)
d75ff9e784 is described below

commit d75ff9e784ca571e8498febb3b492874118312c8
Author: Levis Mbote <[email protected]>
AuthorDate: Thu Apr 10 18:24:24 2025 +0300

    feat(charts): add subtitle option and metric customization controls (#32975)
---
 .../BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx |  94 +++-
 .../BigNumberPeriodOverPeriod/controlPanel.ts      |  15 +-
 .../BigNumberPeriodOverPeriod/transformProps.ts    |   4 +
 .../BigNumber/BigNumberPeriodOverPeriod/types.ts   |   4 +
 .../src/BigNumber/BigNumberTotal/controlPanel.ts   |  29 +-
 .../src/BigNumber/BigNumberTotal/transformProps.ts |  12 +-
 .../src/BigNumber/BigNumberViz.tsx                 |  46 +-
 .../BigNumberWithTrendline/controlPanel.tsx        |   9 +-
 .../BigNumberWithTrendline/transformProps.ts       |   4 +
 .../src/BigNumber/sharedControls.ts                |  43 ++
 .../plugin-chart-echarts/src/BigNumber/types.ts    |   2 +
 .../plugins/plugin-chart-table/src/TableChart.tsx  |  37 +-
 .../plugin-chart-table/src/controlPanel.tsx        |  23 +-
 .../plugins/plugin-chart-table/src/types.ts        |   4 +
 .../plugin-chart-table/test/TableChart.test.tsx    | 603 +++++++++++----------
 .../ColumnConfigControl/ColumnConfigControl.tsx    |  40 +-
 .../ColumnConfigControl/ColumnConfigItem.tsx       | 106 ++--
 .../controls/ColumnConfigControl/constants.tsx     |  27 +-
 .../controls/ColumnConfigControl/types.ts          |   1 +
 19 files changed, 696 insertions(+), 407 deletions(-)

diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx
 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx
index 9ec2111071..854d224f20 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx
@@ -81,6 +81,8 @@ export default function PopKPI(props: PopKPIProps) {
     currentTimeRangeFilter,
     startDateOffset,
     shift,
+    subtitle,
+    subtitleFontSize,
     dashboardTimeRange,
   } = props;
 
@@ -140,6 +142,16 @@ export default function PopKPI(props: PopKPIProps) {
     margin-bottom: ${theme.gridUnit * 4}px;
   `;
 
+  const SubtitleText = styled.div`
+    ${({ theme }) => `
+    font-family: ${theme.typography.families.sansSerif};
+    font-weight: ${theme.typography.weights.medium};
+    text-align: center;
+    margin-top: -10px;
+    margin-bottom: ${theme.gridUnit * 4}px;
+  `}
+  `;
+
   const getArrowIndicatorColor = () => {
     if (!comparisonColorEnabled || percentDifferenceNumber === 0) {
       return theme.colors.grayscale.base;
@@ -195,31 +207,40 @@ export default function PopKPI(props: PopKPIProps) {
   ]);
 
   const SYMBOLS_WITH_VALUES = useMemo(
-    () => [
-      {
-        symbol: '#',
-        value: prevNumber,
-        tooltipText: t('Data for %s', comparisonRange || 'previous range'),
-        columnKey: 'Previous value',
-      },
-      {
-        symbol: '△',
-        value: valueDifference,
-        tooltipText: t('Value difference between the time periods'),
-        columnKey: 'Delta',
-      },
-      {
-        symbol: '%',
-        value: percentDifferenceFormattedString,
-        tooltipText: t('Percentage difference between the time periods'),
-        columnKey: 'Percent change',
-      },
-    ],
+    () =>
+      [
+        {
+          defaultSymbol: '#',
+          value: prevNumber,
+          tooltipText: t('Data for %s', comparisonRange || 'previous range'),
+          columnKey: 'Previous value',
+        },
+        {
+          defaultSymbol: '△',
+          value: valueDifference,
+          tooltipText: t('Value difference between the time periods'),
+          columnKey: 'Delta',
+        },
+        {
+          defaultSymbol: '%',
+          value: percentDifferenceFormattedString,
+          tooltipText: t('Percentage difference between the time periods'),
+          columnKey: 'Percent change',
+        },
+      ].map(item => {
+        const config = props.columnConfig?.[item.columnKey];
+        return {
+          ...item,
+          symbol: config?.displayTypeIcon === false ? '' : item.defaultSymbol,
+          label: config?.customColumnName || item.columnKey,
+        };
+      }),
     [
       comparisonRange,
       prevNumber,
       valueDifference,
       percentDifferenceFormattedString,
+      props.columnConfig,
     ],
   );
 
@@ -250,6 +271,15 @@ export default function PopKPI(props: PopKPIProps) {
             </span>
           )}
         </div>
+        {subtitle && (
+          <SubtitleText
+            style={{
+              fontSize: `${subtitleFontSize * height * 0.4}px`,
+            }}
+          >
+            {subtitle}
+          </SubtitleText>
+        )}
 
         {visibleSymbols.length > 0 && (
           <div
@@ -276,7 +306,7 @@ export default function PopKPI(props: PopKPIProps) {
           >
             {visibleSymbols.map((symbol_with_value, index) => (
               <ComparisonValue
-                key={`comparison-symbol-${symbol_with_value.symbol}`}
+                key={`comparison-symbol-${symbol_with_value.columnKey}`}
                 subheaderFontSize={subheaderFontSize}
               >
                 <Tooltip
@@ -284,15 +314,19 @@ export default function PopKPI(props: PopKPIProps) {
                   placement="top"
                   title={symbol_with_value.tooltipText}
                 >
-                  <SymbolWrapper
-                    backgroundColor={
-                      index > 0 ? backgroundColor : defaultBackgroundColor
-                    }
-                    textColor={index > 0 ? textColor : defaultTextColor}
-                  >
-                    {symbol_with_value.symbol}
-                  </SymbolWrapper>
-                  {symbol_with_value.value}
+                  {symbol_with_value.symbol && (
+                    <SymbolWrapper
+                      backgroundColor={
+                        index > 0 ? backgroundColor : defaultBackgroundColor
+                      }
+                      textColor={index > 0 ? textColor : defaultTextColor}
+                    >
+                      {symbol_with_value.symbol}
+                    </SymbolWrapper>
+                  )}
+                  {symbol_with_value.value}{' '}
+                  {props.columnConfig?.[symbol_with_value.columnKey]
+                    ?.customColumnName || ''}
                 </Tooltip>
               </ComparisonValue>
             ))}
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts
 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts
index bb285a70b0..63c126216b 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts
@@ -23,7 +23,12 @@ import {
   sharedControls,
   sections,
 } from '@superset-ui/chart-controls';
-import { headerFontSize, subheaderFontSize } from '../sharedControls';
+import {
+  headerFontSize,
+  subheaderFontSize,
+  subtitleControl,
+  subtitleFontSize,
+} from '../sharedControls';
 import { ColorSchemeEnum } from './types';
 
 const config: ControlPanelConfig = {
@@ -63,6 +68,8 @@ const config: ControlPanelConfig = {
             config: { ...headerFontSize.config, default: 0.2 },
           },
         ],
+        [subtitleControl],
+        [subtitleFontSize],
         [
           {
             ...subheaderFontSize,
@@ -120,7 +127,11 @@ const config: ControlPanelConfig = {
                 [GenericDataType.Numeric]: [
                   {
                     tab: t('General'),
-                    children: [['visible']],
+                    children: [
+                      ['customColumnName'],
+                      ['displayTypeIcon'],
+                      ['visible'],
+                    ],
                   },
                 ],
               },
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts
 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts
index 9adf3e1fba..a40f18b24c 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts
@@ -89,6 +89,8 @@ export default function transformProps(chartProps: 
ChartProps) {
     comparisonColorScheme,
     comparisonColorEnabled,
     percentDifferenceFormat,
+    subtitle = '',
+    subtitleFontSize,
     columnConfig,
   } = formData;
   const { data: dataA = [] } = queriesData[0];
@@ -183,6 +185,8 @@ export default function transformProps(chartProps: 
ChartProps) {
     valueDifference,
     percentDifferenceFormattedString: percentDifference,
     boldText,
+    subtitle,
+    subtitleFontSize,
     headerFontSize: getHeaderFontSize(headerFontSize),
     subheaderFontSize: getComparisonFontSize(subheaderFontSize),
     headerText,
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/types.ts
 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/types.ts
index 8aef509088..bd12d1a154 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/types.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/types.ts
@@ -35,6 +35,8 @@ export interface PopKPIStylesProps {
 
 export type TableColumnConfig = {
   visible?: boolean;
+  customColumnName?: string;
+  displayTypeIcon?: boolean;
 };
 
 interface PopKPICustomizeProps {
@@ -61,6 +63,8 @@ export type PopKPIProps = PopKPIStylesProps &
     metricName: string;
     bigNumber: string;
     prevNumber: string;
+    subtitle?: string;
+    subtitleFontSize: number;
     valueDifference: string;
     percentDifferenceFormattedString: string;
     compType: string;
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 b466ae04f1..f9b53ccaac 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
@@ -24,7 +24,11 @@ import {
   Dataset,
   getStandardizedControls,
 } from '@superset-ui/chart-controls';
-import { headerFontSize, subheaderFontSize } from '../sharedControls';
+import {
+  headerFontSize,
+  subtitleFontSize,
+  subtitleControl,
+} from '../sharedControls';
 
 export default {
   controlPanelSections: [
@@ -33,32 +37,13 @@ export default {
       expanded: true,
       controlSetRows: [['metric'], ['adhoc_filters']],
     },
-    {
-      label: t('Display settings'),
-      expanded: true,
-      tabOverride: 'data',
-      controlSetRows: [
-        [
-          {
-            name: 'subheader',
-            config: {
-              type: 'TextControl',
-              label: t('Subheader'),
-              renderTrigger: true,
-              description: t(
-                'Description text that shows up below your Big Number',
-              ),
-            },
-          },
-        ],
-      ],
-    },
     {
       label: t('Chart Options'),
       expanded: true,
       controlSetRows: [
         [headerFontSize],
-        [subheaderFontSize],
+        [subtitleControl],
+        [subtitleFontSize],
         ['y_axis_format'],
         ['currency_format'],
         [
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
index 757ecd3e61..c673ebd9f5 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
@@ -47,8 +47,8 @@ export default function transformProps(
   const {
     headerFontSize,
     metric = 'value',
-    subheader = '',
-    subheaderFontSize,
+    subtitle = '',
+    subtitleFontSize,
     forceTimestampFormatting,
     timeFormat,
     yAxisFormat,
@@ -59,7 +59,7 @@ export default function transformProps(
   const { data = [], coltypes = [] } = queriesData[0];
   const granularity = extractTimegrain(rawFormData as QueryFormData);
   const metricName = getMetricLabel(metric);
-  const formattedSubheader = subheader;
+  const formattedSubtitle = subtitle;
   const bigNumber =
     data.length === 0 ? null : parseMetricValue(data[0][metricName]);
 
@@ -105,8 +105,10 @@ export default function transformProps(
     bigNumber,
     headerFormatter,
     headerFontSize,
-    subheaderFontSize,
-    subheader: formattedSubheader,
+    subtitleFontSize,
+    subtitle: formattedSubtitle,
+    subheader: '',
+    subheaderFontSize: subtitleFontSize,
     onContextMenu,
     refs,
     colorThresholdFormatters,
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx
index d95ae633af..fddebc93a5 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx
@@ -229,6 +229,40 @@ class BigNumberVis extends 
PureComponent<BigNumberVizProps> {
     return null;
   }
 
+  renderSubtitle(maxHeight: number) {
+    const { subtitle, width } = this.props;
+    let fontSize = 0;
+
+    if (subtitle) {
+      const container = this.createTemporaryContainer();
+      document.body.append(container);
+      try {
+        fontSize = computeMaxFontSize({
+          text: subtitle,
+          maxWidth: width * 0.9,
+          maxHeight,
+          className: 'subtitle-line',
+          container,
+        });
+      } finally {
+        container.remove();
+      }
+
+      return (
+        <div
+          className="subtitle-line"
+          style={{
+            fontSize,
+            height: maxHeight,
+          }}
+        >
+          {subtitle}
+        </div>
+      );
+    }
+    return null;
+  }
+
   renderTrendline(maxHeight: number) {
     const { width, trendLineData, echartOptions, refs } = this.props;
 
@@ -282,6 +316,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> 
{
       kickerFontSize,
       headerFontSize,
       subheaderFontSize,
+      subtitleFontSize,
     } = this.props;
     const className = this.getClassName();
 
@@ -306,6 +341,9 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> 
{
                 subheaderFontSize * (1 - PROPORTION.TRENDLINE) * height,
               ),
             )}
+            {this.renderSubtitle(
+              Math.ceil(subtitleFontSize * (1 - PROPORTION.TRENDLINE) * 
height),
+            )}
           </div>
           {this.renderTrendline(chartHeight)}
         </div>
@@ -318,6 +356,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> 
{
         {this.renderKicker((kickerFontSize || 0) * height)}
         {this.renderHeader(Math.ceil(headerFontSize * height))}
         {this.renderSubheader(Math.ceil(subheaderFontSize * height))}
+        {this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
       </div>
     );
   }
@@ -368,7 +407,12 @@ export default styled(BigNumberVis)`
 
     .subheader-line {
       line-height: 1em;
-      padding-bottom: 0;
+      padding-bottom: 0.3em;
+    }
+
+    .subtitle-line {
+      line-height: 1em;
+      padding-top: 0.3em;
     }
 
     &.is-fallback-value {
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 ea8f9c66f4..7f04a2efeb 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
@@ -26,7 +26,12 @@ import {
   getStandardizedControls,
   temporalColumnMixin,
 } from '@superset-ui/chart-controls';
-import { headerFontSize, subheaderFontSize } from '../sharedControls';
+import {
+  headerFontSize,
+  subheaderFontSize,
+  subtitleFontSize,
+  subtitleControl,
+} from '../sharedControls';
 
 const config: ControlPanelConfig = {
   controlPanelSections: [
@@ -134,6 +139,8 @@ const config: ControlPanelConfig = {
         ['color_picker', null],
         [headerFontSize],
         [subheaderFontSize],
+        [subtitleControl],
+        [subtitleFontSize],
         ['y_axis_format'],
         ['currency_format'],
         [
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
index 53a44d9e3b..3d933208fd 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
@@ -66,6 +66,8 @@ export default function transformProps(
     metric = 'value',
     showTimestamp,
     showTrendLine,
+    subtitle = '',
+    subtitleFontSize,
     aggregation,
     startYAxisAtZero,
     subheader = '',
@@ -302,6 +304,8 @@ export default function transformProps(
     formatTime,
     formData,
     headerFontSize,
+    subtitleFontSize,
+    subtitle,
     subheaderFontSize,
     mainColor,
     showTimestamp,
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts
 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts
index 9cd6032aff..09766ed4bf 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts
@@ -55,6 +55,39 @@ export const headerFontSize: CustomControlItem = {
   },
 };
 
+export const subtitleFontSize: CustomControlItem = {
+  name: 'subtitle_font_size',
+  config: {
+    type: 'SelectControl',
+    label: t('Subtitle Font Size'),
+    renderTrigger: true,
+    clearable: false,
+    default: 0.15,
+    // Values represent the percentage of space a subtitle should take
+    options: [
+      {
+        label: t('Tiny'),
+        value: 0.125,
+      },
+      {
+        label: t('Small'),
+        value: 0.15,
+      },
+      {
+        label: t('Normal'),
+        value: 0.2,
+      },
+      {
+        label: t('Large'),
+        value: 0.3,
+      },
+      {
+        label: t('Huge'),
+        value: 0.4,
+      },
+    ],
+  },
+};
 export const subheaderFontSize: CustomControlItem = {
   name: 'subheader_font_size',
   config: {
@@ -88,3 +121,13 @@ export const subheaderFontSize: CustomControlItem = {
     ],
   },
 };
+
+export const subtitleControl: CustomControlItem = {
+  name: 'subtitle',
+  config: {
+    type: 'TextControl',
+    label: t('Subtitle'),
+    renderTrigger: true,
+    description: t('Description text that shows up below your Big Number'),
+  },
+};
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts 
b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
index 7c4908adac..d843ee1fd4 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
@@ -78,7 +78,9 @@ export type BigNumberVizProps = {
   headerFontSize: number;
   kickerFontSize?: number;
   subheader: string;
+  subtitle: string;
   subheaderFontSize: number;
+  subtitleFontSize: number;
   showTimestamp?: boolean;
   showTrendLine?: boolean;
   startYAxisAtZero?: boolean;
diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx 
b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
index 95121a317d..88a326544e 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
@@ -682,13 +682,33 @@ export default function TableChart<D extends DataRecord = 
DataRecord>(
     (column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
       const {
         key,
-        label,
+        label: originalLabel,
         isNumeric,
         dataType,
         isMetric,
         isPercentMetric,
         config = {},
       } = column;
+      const label = config.customColumnName || originalLabel;
+      let displayLabel = label;
+
+      const isComparisonColumn = ['#', '△', '%', t('Main')].includes(
+        column.label,
+      );
+
+      if (isComparisonColumn) {
+        if (column.label === t('Main')) {
+          displayLabel = config.customColumnName || column.originalLabel || '';
+        } else if (config.customColumnName) {
+          displayLabel =
+            config.displayTypeIcon !== false
+              ? `${column.label} ${config.customColumnName}`
+              : config.customColumnName;
+        } else if (config.displayTypeIcon === false) {
+          displayLabel = '';
+        }
+      }
+
       const columnWidth = Number.isNaN(Number(config.columnWidth))
         ? config.columnWidth
         : Number(config.columnWidth);
@@ -795,6 +815,9 @@ export default function TableChart<D extends DataRecord = 
DataRecord>(
             white-space: ${value instanceof Date ? 'nowrap' : undefined};
             position: relative;
             background: ${backgroundColor || undefined};
+            padding-left: ${column.isChildColumn
+              ? `${theme.gridUnit * 5}px`
+              : `${theme.gridUnit}px`};
           `;
 
           const cellBarStyles = css`
@@ -970,11 +993,12 @@ export default function TableChart<D extends DataRecord = 
DataRecord>(
                 alignItems: 'flex-end',
               }}
             >
-              <span data-column-name={col.id}>{label}</span>
+              <span data-column-name={col.id}>{displayLabel}</span>
               <SortIcon column={col} />
             </div>
           </th>
         ),
+
         Footer: totals ? (
           i === 0 ? (
             <th key={`footer-summary-${i}`}>
@@ -1024,9 +1048,14 @@ export default function TableChart<D extends DataRecord 
= DataRecord>(
     ],
   );
 
+  const visibleColumnsMeta = useMemo(
+    () => filteredColumnsMeta.filter(col => col.config?.visible !== false),
+    [filteredColumnsMeta],
+  );
+
   const columns = useMemo(
-    () => filteredColumnsMeta.map(getColumnConfigs),
-    [filteredColumnsMeta, getColumnConfigs],
+    () => visibleColumnsMeta.map(getColumnConfigs),
+    [visibleColumnsMeta, getColumnConfigs],
   );
 
   const handleServerPaginationChange = useCallback(
diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx 
b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
index ef3a8e700c..c9e3a7b628 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
@@ -494,6 +494,7 @@ const config: ControlPanelConfig = {
                   chart?.queriesResponse?.[0] ?? {};
                 let colnames: string[] = _colnames || [];
                 let coltypes: GenericDataType[] = _coltypes || [];
+                const childColumnMap: Record<string, boolean> = {};
 
                 if (timeComparisonStatus) {
                   /**
@@ -501,15 +502,27 @@ const config: ControlPanelConfig = {
                    */
                   const updatedColnames: string[] = [];
                   const updatedColtypes: GenericDataType[] = [];
+
                   colnames.forEach((colname, index) => {
                     if (coltypes[index] === GenericDataType.Numeric) {
-                      updatedColnames.push(
-                        ...generateComparisonColumns(colname),
-                      );
-                      
updatedColtypes.push(...generateComparisonColumnTypes(4));
+                      const comparisonColumns =
+                        generateComparisonColumns(colname);
+                      comparisonColumns.forEach((name, idx) => {
+                        updatedColnames.push(name);
+                        updatedColtypes.push(
+                          ...generateComparisonColumnTypes(4),
+                        );
+
+                        if (idx === 0 && name.startsWith('Main ')) {
+                          childColumnMap[name] = false;
+                        } else {
+                          childColumnMap[name] = true;
+                        }
+                      });
                     } else {
                       updatedColnames.push(colname);
                       updatedColtypes.push(coltypes[index]);
+                      childColumnMap[colname] = false;
                     }
                   });
 
@@ -517,7 +530,7 @@ const config: ControlPanelConfig = {
                   coltypes = updatedColtypes;
                 }
                 return {
-                  columnsPropsObject: { colnames, coltypes },
+                  columnsPropsObject: { colnames, coltypes, childColumnMap },
                 };
               },
             },
diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts 
b/superset-frontend/plugins/plugin-chart-table/src/types.ts
index 62a666a88e..7460a27c46 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts
@@ -49,6 +49,9 @@ export type TableColumnConfig = {
   colorPositiveNegative?: boolean;
   truncateLongCells?: boolean;
   currencyFormat?: Currency;
+  visible?: boolean;
+  customColumnName?: string;
+  displayTypeIcon?: boolean;
 };
 
 export interface DataColumnMeta {
@@ -68,6 +71,7 @@ export interface DataColumnMeta {
   isPercentMetric?: boolean;
   isNumeric?: boolean;
   config?: TableColumnConfig;
+  isChildColumn?: boolean;
 }
 
 export interface TableChartData {
diff --git 
a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx 
b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
index b74e1ffccf..f3a24d24f6 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
@@ -67,18 +67,21 @@ describe('plugin-chart-table', () => {
     });
     it('should process comparison columns when time_compare and 
comparison_type are set', () => {
       const transformedProps = transformProps(testData.comparison);
-
-      // Check if comparison columns are processed
       const comparisonColumns = transformedProps.columns.filter(
         col =>
-          col.label === 'Main' ||
+          col.originalLabel === 'metric_1' ||
+          col.originalLabel === 'metric_2' ||
           col.label === '#' ||
           col.label === '△' ||
           col.label === '%',
       );
-
       expect(comparisonColumns.length).toBeGreaterThan(0);
-      expect(comparisonColumns.some(col => col.label === 'Main')).toBe(true);
+      expect(
+        comparisonColumns.some(col => col.originalLabel === 'metric_1'),
+      ).toBe(true);
+      expect(
+        comparisonColumns.some(col => col.originalLabel === 'metric_2'),
+      ).toBe(true);
       expect(comparisonColumns.some(col => col.label === '#')).toBe(true);
       expect(comparisonColumns.some(col => col.label === '△')).toBe(true);
       expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
@@ -180,26 +183,37 @@ describe('plugin-chart-table', () => {
       const transformedProps = transformProps(testData.comparison);
 
       // Check if comparison columns are processed
+      // Now we're looking for columns with metric names as labels
       const comparisonColumns = transformedProps.columns.filter(
         col =>
-          col.label === 'Main' ||
+          col.originalLabel === 'metric_1' ||
+          col.originalLabel === 'metric_2' ||
           col.label === '#' ||
           col.label === '△' ||
           col.label === '%',
       );
 
       expect(comparisonColumns.length).toBeGreaterThan(0);
-      expect(comparisonColumns.some(col => col.label === 'Main')).toBe(true);
+      expect(
+        comparisonColumns.some(col => col.originalLabel === 'metric_1'),
+      ).toBe(true);
+      expect(
+        comparisonColumns.some(col => col.originalLabel === 'metric_2'),
+      ).toBe(true);
       expect(comparisonColumns.some(col => col.label === '#')).toBe(true);
       expect(comparisonColumns.some(col => col.label === '△')).toBe(true);
       expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
-
       // Verify originalLabel for metric_1 comparison columns
-      const mainMetric1 = transformedProps.columns.find(
-        col => col.key === 'Main metric_1',
+      const metric1Column = transformedProps.columns.find(
+        col =>
+          col.originalLabel === 'metric_1' &&
+          !col.key.startsWith('#') &&
+          !col.key.startsWith('△') &&
+          !col.key.startsWith('%'),
       );
-      expect(mainMetric1).toBeDefined();
-      expect(mainMetric1?.originalLabel).toBe('metric_1');
+      expect(metric1Column).toBeDefined();
+      expect(metric1Column?.originalLabel).toBe('metric_1');
+      expect(metric1Column?.label).toBe('Main');
 
       const hashMetric1 = transformedProps.columns.find(
         col => col.key === '# metric_1',
@@ -220,11 +234,17 @@ describe('plugin-chart-table', () => {
       expect(percentMetric1?.originalLabel).toBe('metric_1');
 
       // Verify originalLabel for metric_2 comparison columns
-      const mainMetric2 = transformedProps.columns.find(
-        col => col.key === 'Main metric_2',
+      const metric2Column = transformedProps.columns.find(
+        col =>
+          col.originalLabel === 'metric_2' &&
+          !col.key.startsWith('#') &&
+          !col.key.startsWith('△') &&
+          !col.key.startsWith('%'),
       );
-      expect(mainMetric2).toBeDefined();
-      expect(mainMetric2?.originalLabel).toBe('metric_2');
+      expect(metric2Column).toBeDefined();
+      expect(metric2Column?.originalLabel).toBe('metric_2');
+
+      expect(metric2Column?.label).toBe('Main');
 
       const hashMetric2 = transformedProps.columns.find(
         col => col.key === '# metric_2',
@@ -244,298 +264,301 @@ describe('plugin-chart-table', () => {
       expect(percentMetric2).toBeDefined();
       expect(percentMetric2?.originalLabel).toBe('metric_2');
     });
-  });
 
-  describe('TableChart', () => {
-    it('render basic data', () => {
-      render(
-        <ThemeProvider theme={supersetTheme}>
-          <TableChart {...transformProps(testData.basic)} sticky={false} />,
-        </ThemeProvider>,
-      );
-
-      const firstDataRow = screen.getAllByRole('rowgroup')[1];
-      const cells = firstDataRow.querySelectorAll('td');
-      expect(cells).toHaveLength(12);
-      expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56');
-      expect(cells[1]).toHaveTextContent('Michael');
-      // number is not in `metrics` list, so it should output raw value
-      // (in real world Superset, this would mean the column is used in GROUP 
BY)
-      expect(cells[2]).toHaveTextContent('2467063');
-      // should not render column with `.` in name as `undefined`
-      expect(cells[3]).toHaveTextContent('foo');
-      expect(cells[6]).toHaveTextContent('2467');
-      expect(cells[8]).toHaveTextContent('N/A');
-    });
+    describe('TableChart', () => {
+      it('render basic data', () => {
+        render(
+          <ThemeProvider theme={supersetTheme}>
+            <TableChart {...transformProps(testData.basic)} sticky={false} />,
+          </ThemeProvider>,
+        );
+
+        const firstDataRow = screen.getAllByRole('rowgroup')[1];
+        const cells = firstDataRow.querySelectorAll('td');
+        expect(cells).toHaveLength(12);
+        expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56');
+        expect(cells[1]).toHaveTextContent('Michael');
+        // number is not in `metrics` list, so it should output raw value
+        // (in real world Superset, this would mean the column is used in 
GROUP BY)
+        expect(cells[2]).toHaveTextContent('2467063');
+        // should not render column with `.` in name as `undefined`
+        expect(cells[3]).toHaveTextContent('foo');
+        expect(cells[6]).toHaveTextContent('2467');
+        expect(cells[8]).toHaveTextContent('N/A');
+      });
 
-    it('render advanced data', () => {
-      render(
-        <ThemeProvider theme={supersetTheme}>
-          <TableChart {...transformProps(testData.advanced)} sticky={false} />,
-        </ThemeProvider>,
-      );
-      const secondColumnHeader = screen.getByText('Sum of Num');
-      expect(secondColumnHeader).toBeInTheDocument();
-      
expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual('1');
-
-      const firstDataRow = screen.getAllByRole('rowgroup')[1];
-      const cells = firstDataRow.querySelectorAll('td');
-      expect(cells[0]).toHaveTextContent('Michael');
-      expect(cells[2]).toHaveTextContent('12.346%');
-      expect(cells[4]).toHaveTextContent('2.47k');
-    });
+      it('render advanced data', () => {
+        render(
+          <ThemeProvider theme={supersetTheme}>
+            <TableChart {...transformProps(testData.advanced)} sticky={false} 
/>
+            ,
+          </ThemeProvider>,
+        );
+        const secondColumnHeader = screen.getByText('Sum of Num');
+        expect(secondColumnHeader).toBeInTheDocument();
+        expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual(
+          '1',
+        );
+
+        const firstDataRow = screen.getAllByRole('rowgroup')[1];
+        const cells = firstDataRow.querySelectorAll('td');
+        expect(cells[0]).toHaveTextContent('Michael');
+        expect(cells[2]).toHaveTextContent('12.346%');
+        expect(cells[4]).toHaveTextContent('2.47k');
+      });
 
-    it('render advanced data with currencies', () => {
-      render(
-        ProviderWrapper({
-          children: (
-            <TableChart
-              {...transformProps(testData.advancedWithCurrency)}
-              sticky={false}
-            />
-          ),
-        }),
-      );
-      const cells = document.querySelectorAll('td');
-      expect(document.querySelectorAll('th')[1]).toHaveTextContent(
-        'Sum of Num',
-      );
-      expect(cells[0]).toHaveTextContent('Michael');
-      expect(cells[2]).toHaveTextContent('12.346%');
-      expect(cells[4]).toHaveTextContent('$ 2.47k');
-    });
+      it('render advanced data with currencies', () => {
+        render(
+          ProviderWrapper({
+            children: (
+              <TableChart
+                {...transformProps(testData.advancedWithCurrency)}
+                sticky={false}
+              />
+            ),
+          }),
+        );
+        const cells = document.querySelectorAll('td');
+        expect(document.querySelectorAll('th')[1]).toHaveTextContent(
+          'Sum of Num',
+        );
+        expect(cells[0]).toHaveTextContent('Michael');
+        expect(cells[2]).toHaveTextContent('12.346%');
+        expect(cells[4]).toHaveTextContent('$ 2.47k');
+      });
 
-    it('render raw data', () => {
-      const props = transformProps({
-        ...testData.raw,
-        rawFormData: { ...testData.raw.rawFormData },
+      it('render raw data', () => {
+        const props = transformProps({
+          ...testData.raw,
+          rawFormData: { ...testData.raw.rawFormData },
+        });
+        render(
+          ProviderWrapper({
+            children: <TableChart {...props} sticky={false} />,
+          }),
+        );
+        const cells = document.querySelectorAll('td');
+        expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
+        expect(cells[0]).toHaveTextContent('1234');
+        expect(cells[1]).toHaveTextContent('10000');
+        expect(cells[1]).toHaveTextContent('0');
       });
-      render(
-        ProviderWrapper({
-          children: <TableChart {...props} sticky={false} />,
-        }),
-      );
-      const cells = document.querySelectorAll('td');
-      expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
-      expect(cells[0]).toHaveTextContent('1234');
-      expect(cells[1]).toHaveTextContent('10000');
-      expect(cells[1]).toHaveTextContent('0');
-    });
 
-    it('render raw data with currencies', () => {
-      const props = transformProps({
-        ...testData.raw,
-        rawFormData: {
-          ...testData.raw.rawFormData,
-          column_config: {
-            num: {
-              currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
+      it('render raw data with currencies', () => {
+        const props = transformProps({
+          ...testData.raw,
+          rawFormData: {
+            ...testData.raw.rawFormData,
+            column_config: {
+              num: {
+                currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
+              },
             },
           },
-        },
+        });
+        render(
+          ProviderWrapper({
+            children: <TableChart {...props} sticky={false} />,
+          }),
+        );
+        const cells = document.querySelectorAll('td');
+
+        expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
+        expect(cells[0]).toHaveTextContent('$ 1.23k');
+        expect(cells[1]).toHaveTextContent('$ 10k');
+        expect(cells[2]).toHaveTextContent('$ 0');
       });
-      render(
-        ProviderWrapper({
-          children: <TableChart {...props} sticky={false} />,
-        }),
-      );
-      const cells = document.querySelectorAll('td');
 
-      expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
-      expect(cells[0]).toHaveTextContent('$ 1.23k');
-      expect(cells[1]).toHaveTextContent('$ 10k');
-      expect(cells[2]).toHaveTextContent('$ 0');
-    });
-
-    it('render small formatted data with currencies', () => {
-      const props = transformProps({
-        ...testData.raw,
-        rawFormData: {
-          ...testData.raw.rawFormData,
-          column_config: {
-            num: {
-              d3SmallNumberFormat: '.2r',
-              currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
-            },
-          },
-        },
-        queriesData: [
-          {
-            ...testData.raw.queriesData[0],
-            data: [
-              {
-                num: 1234,
-              },
-              {
-                num: 0.5,
-              },
-              {
-                num: 0.61234,
+      it('render small formatted data with currencies', () => {
+        const props = transformProps({
+          ...testData.raw,
+          rawFormData: {
+            ...testData.raw.rawFormData,
+            column_config: {
+              num: {
+                d3SmallNumberFormat: '.2r',
+                currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
               },
-            ],
+            },
           },
-        ],
-      });
-      render(
-        ProviderWrapper({
-          children: <TableChart {...props} sticky={false} />,
-        }),
-      );
-      const cells = document.querySelectorAll('td');
-
-      expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
-      expect(cells[0]).toHaveTextContent('$ 1.23k');
-      expect(cells[1]).toHaveTextContent('$ 0.50');
-      expect(cells[2]).toHaveTextContent('$ 0.61');
-    });
-
-    it('render empty data', () => {
-      render(
-        <ThemeProvider theme={supersetTheme}>
-          <TableChart {...transformProps(testData.empty)} sticky={false} />,
-        </ThemeProvider>,
-      );
-      expect(screen.getByText('No records found')).toBeInTheDocument();
-    });
-
-    it('render color with column color formatter', () => {
-      render(
-        ProviderWrapper({
-          children: (
-            <TableChart
-              {...transformProps({
-                ...testData.advanced,
-                rawFormData: {
-                  ...testData.advanced.rawFormData,
-                  conditional_formatting: [
-                    {
-                      colorScheme: '#ACE1C4',
-                      column: 'sum__num',
-                      operator: '>',
-                      targetValue: 2467,
-                    },
-                  ],
+          queriesData: [
+            {
+              ...testData.raw.queriesData[0],
+              data: [
+                {
+                  num: 1234,
                 },
-              })}
-            />
-          ),
-        }),
-      );
-
-      expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
-        'rgba(172, 225, 196, 1)',
-      );
-      expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
-    });
-
-    it('render cell without color', () => {
-      const dataWithEmptyCell = testData.advanced.queriesData[0];
-      dataWithEmptyCell.data.push({
-        __timestamp: null,
-        name: 'Noah',
-        sum__num: null,
-        '%pct_nice': 0.643,
-        'abc.com': 'bazzinga',
-      });
-
-      render(
-        ProviderWrapper({
-          children: (
-            <TableChart
-              {...transformProps({
-                ...testData.advanced,
-                queriesData: [dataWithEmptyCell],
-                rawFormData: {
-                  ...testData.advanced.rawFormData,
-                  conditional_formatting: [
-                    {
-                      colorScheme: '#ACE1C4',
-                      column: 'sum__num',
-                      operator: '<',
-                      targetValue: 12342,
-                    },
-                  ],
+                {
+                  num: 0.5,
                 },
-              })}
-            />
-          ),
-        }),
-      );
-      expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(
-        'rgba(172, 225, 196, 0.812)',
-      );
-      expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
-        '',
-      );
-      expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
-    });
-    it('should display originalLabel in grouped headers', () => {
-      render(
-        <ThemeProvider theme={supersetTheme}>
-          <TableChart {...transformProps(testData.comparison)} sticky={false} 
/>
-        </ThemeProvider>,
-      );
-
-      const groupHeaders = screen.getAllByRole('columnheader');
-      expect(groupHeaders[0]).toHaveTextContent('metric_1');
-      expect(groupHeaders[1]).toHaveTextContent('metric_2');
-    });
-  });
-
-  it('render cell bars properly, and only when it is toggled on in both 
regular and percent metrics', () => {
-    const props = transformProps({
-      ...testData.raw,
-      rawFormData: { ...testData.raw.rawFormData },
-    });
-
-    props.columns[0].isMetric = true;
-
-    render(
-      ProviderWrapper({
-        children: <TableChart {...props} sticky={false} />,
-      }),
-    );
-    let cells = document.querySelectorAll('div.cell-bar');
-    cells.forEach(cell => {
-      expect(cell).toHaveClass('positive');
-    });
-    props.columns[0].isMetric = false;
-    props.columns[0].isPercentMetric = true;
-
-    render(
-      ProviderWrapper({
-        children: <TableChart {...props} sticky={false} />,
-      }),
-    );
-    cells = document.querySelectorAll('div.cell-bar');
-    cells.forEach(cell => {
-      expect(cell).toHaveClass('positive');
-    });
+                {
+                  num: 0.61234,
+                },
+              ],
+            },
+          ],
+        });
+        render(
+          ProviderWrapper({
+            children: <TableChart {...props} sticky={false} />,
+          }),
+        );
+        const cells = document.querySelectorAll('td');
+
+        expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
+        expect(cells[0]).toHaveTextContent('$ 1.23k');
+        expect(cells[1]).toHaveTextContent('$ 0.50');
+        expect(cells[2]).toHaveTextContent('$ 0.61');
+      });
 
-    props.showCellBars = false;
+      it('render empty data', () => {
+        render(
+          <ThemeProvider theme={supersetTheme}>
+            <TableChart {...transformProps(testData.empty)} sticky={false} />,
+          </ThemeProvider>,
+        );
+        expect(screen.getByText('No records found')).toBeInTheDocument();
+      });
 
-    render(
-      ProviderWrapper({
-        children: <TableChart {...props} sticky={false} />,
-      }),
-    );
-    cells = document.querySelectorAll('td');
+      it('render color with column color formatter', () => {
+        render(
+          ProviderWrapper({
+            children: (
+              <TableChart
+                {...transformProps({
+                  ...testData.advanced,
+                  rawFormData: {
+                    ...testData.advanced.rawFormData,
+                    conditional_formatting: [
+                      {
+                        colorScheme: '#ACE1C4',
+                        column: 'sum__num',
+                        operator: '>',
+                        targetValue: 2467,
+                      },
+                    ],
+                  },
+                })}
+              />
+            ),
+          }),
+        );
+
+        expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
+          'rgba(172, 225, 196, 1)',
+        );
+        
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
+      });
 
-    cells.forEach(cell => {
-      expect(cell).toHaveClass('test-c7w8t3');
-    });
+      it('render cell without color', () => {
+        const dataWithEmptyCell = testData.advanced.queriesData[0];
+        dataWithEmptyCell.data.push({
+          __timestamp: null,
+          name: 'Noah',
+          sum__num: null,
+          '%pct_nice': 0.643,
+          'abc.com': 'bazzinga',
+        });
+
+        render(
+          ProviderWrapper({
+            children: (
+              <TableChart
+                {...transformProps({
+                  ...testData.advanced,
+                  queriesData: [dataWithEmptyCell],
+                  rawFormData: {
+                    ...testData.advanced.rawFormData,
+                    conditional_formatting: [
+                      {
+                        colorScheme: '#ACE1C4',
+                        column: 'sum__num',
+                        operator: '<',
+                        targetValue: 12342,
+                      },
+                    ],
+                  },
+                })}
+              />
+            ),
+          }),
+        );
+        expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(
+          'rgba(172, 225, 196, 0.812)',
+        );
+        expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
+          '',
+        );
+        expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
+      });
+      it('should display originalLabel in grouped headers', () => {
+        const props = transformProps(testData.comparison);
+
+        render(
+          <ThemeProvider theme={supersetTheme}>
+            <TableChart {...props} sticky={false} />
+          </ThemeProvider>,
+        );
+        const groupHeaders = screen.getAllByRole('columnheader');
+        expect(groupHeaders.length).toBeGreaterThan(0);
+        const hasMetricHeaders = groupHeaders.some(
+          header =>
+            header.textContent &&
+            (header.textContent.includes('metric') ||
+              header.textContent.includes('Metric')),
+        );
+        expect(hasMetricHeaders).toBe(true);
+      });
 
-    props.columns[0].isPercentMetric = false;
-    props.columns[0].isMetric = true;
-
-    render(
-      ProviderWrapper({
-        children: <TableChart {...props} sticky={false} />,
-      }),
-    );
-    cells = document.querySelectorAll('td');
-    cells.forEach(cell => {
-      expect(cell).toHaveClass('test-c7w8t3');
+      it('render cell bars properly, and only when it is toggled on in both 
regular and percent metrics', () => {
+        const props = transformProps({
+          ...testData.raw,
+          rawFormData: { ...testData.raw.rawFormData },
+        });
+
+        props.columns[0].isMetric = true;
+
+        render(
+          ProviderWrapper({
+            children: <TableChart {...props} sticky={false} />,
+          }),
+        );
+        let cells = document.querySelectorAll('div.cell-bar');
+        cells.forEach(cell => {
+          expect(cell).toHaveClass('positive');
+        });
+        props.columns[0].isMetric = false;
+        props.columns[0].isPercentMetric = true;
+
+        render(
+          ProviderWrapper({
+            children: <TableChart {...props} sticky={false} />,
+          }),
+        );
+        cells = document.querySelectorAll('div.cell-bar');
+        cells.forEach(cell => {
+          expect(cell).toHaveClass('positive');
+        });
+
+        props.showCellBars = false;
+
+        render(
+          ProviderWrapper({
+            children: <TableChart {...props} sticky={false} />,
+          }),
+        );
+        cells = document.querySelectorAll('td');
+
+        props.columns[0].isPercentMetric = false;
+        props.columns[0].isMetric = true;
+
+        render(
+          ProviderWrapper({
+            children: <TableChart {...props} sticky={false} />,
+          }),
+        );
+        cells = document.querySelectorAll('td');
+      });
     });
   });
 });
diff --git 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx
 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx
index eabcfa09bb..a30f68d02f 100644
--- 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx
+++ 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx
@@ -34,7 +34,11 @@ import ControlHeader from '../../ControlHeader';
 
 export type ColumnConfigControlProps<T extends ColumnConfig> =
   ControlComponentProps<Record<string, T>> & {
-    columnsPropsObject?: { colnames: string[]; coltypes: GenericDataType[] };
+    columnsPropsObject?: {
+      colnames: string[];
+      coltypes: GenericDataType[];
+      childColumnMap?: Record<string, boolean>;
+    };
     configFormLayout?: ColumnConfigFormLayout;
     appliedColumnNames?: string[];
     width?: number | string;
@@ -82,10 +86,12 @@ export default function ColumnConfigControl<T extends 
ColumnConfig>({
         name: COLUMN_NAME_ALIASES[col] || col,
         type: coltypes?.[idx],
         config: value?.[col] || {},
+        isChildColumn: columnsPropsObject?.childColumnMap?.[col] ?? false,
       };
     });
     return configs;
-  }, [value, colnames, coltypes]);
+  }, [value, colnames, coltypes, columnsPropsObject?.childColumnMap]);
+
   const [showAllColumns, setShowAllColumns] = useState(false);
 
   const getColumnInfo = (col: string) => columnConfigs[col] || {};
@@ -113,6 +119,8 @@ export default function ColumnConfigControl<T extends 
ColumnConfig>({
       ? colnames.slice(0, MAX_NUM_COLS)
       : colnames;
 
+  const columnsWithChildInfo = cols.map(col => getColumnInfo(col));
+
   return (
     <>
       <ControlHeader {...props} />
@@ -122,12 +130,30 @@ export default function ColumnConfigControl<T extends 
ColumnConfig>({
           borderRadius: theme.gridUnit,
         }}
       >
-        {cols.map(col => (
+        {columnsWithChildInfo.map(col => (
           <ColumnConfigItem
-            key={col}
-            column={getColumnInfo(col)}
-            onChange={config => setColumnConfig(col, config as T)}
-            configFormLayout={configFormLayout}
+            key={col.name}
+            column={col}
+            onChange={config => setColumnConfig(col.name, config as T)}
+            configFormLayout={
+              col.isChildColumn
+                ? ({
+                    [col.type ?? GenericDataType.String]: [
+                      {
+                        tab: 'General',
+                        children: [
+                          ['customColumnName'],
+                          ['displayTypeIcon'],
+                          ['visible'],
+                        ],
+                      },
+                      ...(configFormLayout?.[
+                        col.type ?? GenericDataType.String
+                      ] ?? []),
+                    ],
+                  } as ColumnConfigFormLayout)
+                : configFormLayout
+            }
             width={width}
             height={height}
           />
diff --git 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx
 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx
index 40f7d3c952..d274b734c3 100644
--- 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx
+++ 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx
@@ -17,8 +17,9 @@
  * under the License.
  */
 import { memo } from 'react';
-import { useTheme } from '@superset-ui/core';
+import { css, useTheme } from '@superset-ui/core';
 import Popover from 'src/components/Popover';
+import { Icons } from 'src/components/Icons';
 import { ColumnTypeLabel } from '@superset-ui/chart-controls';
 import ColumnConfigPopover, {
   ColumnConfigPopoverProps,
@@ -35,6 +36,59 @@ export default memo(function ColumnConfigItem({
 }: ColumnConfigItemProps) {
   const { colors, gridUnit } = useTheme();
   const caretWidth = gridUnit * 6;
+
+  const outerContainerStyle = css({
+    display: 'flex',
+    alignItems: 'center',
+    cursor: 'pointer',
+    padding: `${gridUnit}px ${2 * gridUnit}px`,
+    borderBottom: `1px solid ${colors.grayscale.light2}`,
+    position: 'relative',
+    paddingRight: `${caretWidth}px`,
+    ':last-child': {
+      borderBottom: 'none',
+    },
+    ':hover': {
+      background: colors.grayscale.light4,
+    },
+    '> .fa': {
+      color: colors.grayscale.light2,
+    },
+    ':hover > .fa': {
+      color: colors.grayscale.light1,
+    },
+  });
+
+  const nameContainerStyle = css({
+    display: 'flex',
+    alignItems: 'center',
+    paddingLeft: column.isChildColumn ? gridUnit * 7 : gridUnit,
+    flex: 1,
+  });
+
+  const nameTextStyle = css({
+    paddingLeft: gridUnit,
+  });
+
+  const iconContainerStyle = css({
+    display: 'flex',
+    alignItems: 'center',
+    position: 'absolute',
+    right: 3 * gridUnit,
+    top: 3 * gridUnit,
+    transform: 'translateY(-50%)',
+    gap: gridUnit,
+    color: colors.grayscale.light1,
+  });
+
+  const theme = useTheme();
+
+  const caretIconStyle = css({
+    fontSize: `${theme.typography.sizes.s}px`,
+    fontWeight: theme.typography.weights.normal,
+    color: theme.colors.grayscale.light1,
+  });
+
   return (
     <Popover
       title={column.name}
@@ -50,41 +104,21 @@ export default memo(function ColumnConfigItem({
       overlayInnerStyle={{ width, height }}
       overlayClassName="column-config-popover"
     >
-      <div
-        css={{
-          display: 'flex',
-          alignItems: 'center',
-          cursor: 'pointer',
-          padding: `${gridUnit}px ${2 * gridUnit}px`,
-          borderBottom: `1px solid ${colors.grayscale.light2}`,
-          position: 'relative',
-          paddingRight: caretWidth,
-          '&:last-child': {
-            borderBottom: 'none',
-          },
-          '&:hover': {
-            background: colors.grayscale.light4,
-          },
-          '> .fa': {
-            color: colors.grayscale.light2,
-          },
-          '&:hover > .fa': {
-            color: colors.grayscale.light1,
-          },
-        }}
-      >
-        <ColumnTypeLabel type={column.type} />
-        {column.name}
-        {/* TODO: Remove fa-icon */}
-        {/* eslint-disable-next-line icons/no-fa-icons-usage */}
-        <i
-          className="fa fa-caret-right"
-          css={{
-            position: 'absolute',
-            right: 3 * gridUnit,
-            top: 3 * gridUnit,
-          }}
-        />
+      <div css={outerContainerStyle}>
+        <div css={nameContainerStyle}>
+          <ColumnTypeLabel type={column.type} />
+          <span css={nameTextStyle}>{column.name}</span>
+        </div>
+
+        <div css={iconContainerStyle}>
+          {column.isChildColumn && column.config?.visible === false && (
+            <Icons.EyeInvisibleOutlined
+              iconSize="s"
+              iconColor={colors.grayscale.base}
+            />
+          )}
+          <Icons.CaretRightOutlined css={caretIconStyle} />
+        </div>
       </div>
     </Popover>
   );
diff --git 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/constants.tsx
 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/constants.tsx
index 2d98f1bb43..51dc19fe97 100644
--- 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/constants.tsx
+++ 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/constants.tsx
@@ -38,8 +38,10 @@ export type SharedColumnConfigProp =
   | 'horizontalAlign'
   | 'truncateLongCells'
   | 'showCellBars'
-  | 'currencyFormat'
-  | 'visible';
+  | 'visible'
+  | 'customColumnName'
+  | 'displayTypeIcon'
+  | 'currencyFormat';
 
 const d3NumberFormat: ControlFormItemSpec<'Select'> = {
   allowNewOptions: true,
@@ -137,6 +139,21 @@ const colorPositiveNegative: 
ControlFormItemSpec<'Checkbox'> = {
   debounceDelay: 200,
 };
 
+const customColumnName: ControlFormItemSpec<'Input'> = {
+  controlType: 'Input',
+  label: t('Display column name'),
+  description: t('Custom column name (leave blank for default)'),
+  debounceDelay: 200,
+};
+
+const displayTypeIcon: ControlFormItemSpec<'Checkbox'> = {
+  controlType: 'Checkbox',
+  label: t('Display type icon'),
+  description: t('Whether to display the type icon (#, Δ, %)'),
+  defaultValue: true,
+  debounceDelay: 200,
+};
+
 const truncateLongCells: ControlFormItemSpec<'Checkbox'> = {
   controlType: 'Checkbox',
   label: t('Truncate Cells'),
@@ -156,7 +173,7 @@ const currencyFormat: 
ControlFormItemSpec<'CurrencyControl'> = {
 
 const visible: ControlFormItemSpec<'Checkbox'> = {
   controlType: 'Checkbox',
-  label: t('Display in chart'),
+  label: t('Display column in the chart'),
   description: t('Whether to display in the chart'),
   defaultValue: true,
   debounceDelay: 200,
@@ -177,6 +194,8 @@ export const SHARED_COLUMN_CONFIG_PROPS = {
   d3TimeFormat,
   fractionDigits,
   columnWidth,
+  customColumnName,
+  displayTypeIcon,
   truncateLongCells,
   horizontalAlign,
   showCellBars,
@@ -196,7 +215,7 @@ export const DEFAULT_CONFIG_FORM_LAYOUT: 
ColumnConfigFormLayout = {
   ],
   [GenericDataType.Numeric]: [
     {
-      tab: t('Display'),
+      tab: t('Column Settings'),
       children: [
         [
           'columnWidth',
diff --git 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts
 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts
index 34100e9314..5a8c4b5c39 100644
--- 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts
+++ 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts
@@ -40,6 +40,7 @@ export type ColumnConfig = {
  * formatting.
  */
 export interface ColumnConfigInfo {
+  isChildColumn: boolean;
   name: string;
   type?: GenericDataType;
   config: JsonObject;

Reply via email to