Copilot commented on code in PR #33146:
URL: https://github.com/apache/superset/pull/33146#discussion_r2219863081


##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts:
##########
@@ -156,11 +156,45 @@ export default function transformProps(
     legendState,
     queriesData,
     hooks,
+    filterState,
     theme,
+    emitCrossFilters,
     inContextMenu,
   } = chartProps;
   const refs: Refs = {};
-  const { data = [] } = queriesData[0];
+  let { data = [] } = queriesData[0];
+
+  const { xAxisSortByColumn, xAxisSortByColumnOrder } = formData;
+  if (
+    xAxisSortByColumn &&
+    xAxisSortByColumnOrder &&
+    xAxisSortByColumnOrder !== 'NONE'
+  ) {
+    const sortColumn = xAxisSortByColumn;
+    const isAscending = xAxisSortByColumnOrder === 'ASC';
+
+    data = [...data].sort((a, b) => {
+      const aValue = a[sortColumn];
+      const bValue = b[sortColumn];
+
+      // Handle null/undefined values
+      if (aValue == null && bValue == null) return 0;
+      if (aValue == null) return isAscending ? -1 : 1;
+      if (bValue == null) return isAscending ? 1 : -1;

Review Comment:
   Use strict equality checks (=== null) instead of loose equality (== null) 
for better type safety and clearer intent.
   ```suggestion
         if ((aValue === null || aValue === undefined) && (bValue === null || 
bValue === undefined)) return 0;
         if (aValue === null || aValue === undefined) return isAscending ? -1 : 
1;
         if (bValue === null || bValue === undefined) return isAscending ? 1 : 
-1;
   ```



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts:
##########
@@ -156,11 +156,45 @@ export default function transformProps(
     legendState,
     queriesData,
     hooks,
+    filterState,
     theme,
+    emitCrossFilters,
     inContextMenu,
   } = chartProps;
   const refs: Refs = {};
-  const { data = [] } = queriesData[0];
+  let { data = [] } = queriesData[0];
+
+  const { xAxisSortByColumn, xAxisSortByColumnOrder } = formData;
+  if (
+    xAxisSortByColumn &&
+    xAxisSortByColumnOrder &&
+    xAxisSortByColumnOrder !== 'NONE'
+  ) {
+    const sortColumn = xAxisSortByColumn;
+    const isAscending = xAxisSortByColumnOrder === 'ASC';
+
+    data = [...data].sort((a, b) => {
+      const aValue = a[sortColumn];
+      const bValue = b[sortColumn];
+
+      // Handle null/undefined values
+      if (aValue == null && bValue == null) return 0;
+      if (aValue == null) return isAscending ? -1 : 1;
+      if (bValue == null) return isAscending ? 1 : -1;

Review Comment:
   Use strict equality checks (=== null) instead of loose equality (== null) 
for better type safety and clearer intent.
   ```suggestion
         if ((aValue === null || aValue === undefined) && (bValue === null || 
bValue === undefined)) return 0;
         if (aValue === null || aValue === undefined) return isAscending ? -1 : 
1;
         if (bValue === null || bValue === undefined) return isAscending ? 1 : 
-1;
   ```



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx:
##########
@@ -34,6 +34,51 @@ const config: ControlPanelConfig = {
       expanded: true,
       controlSetRows: [
         ['x_axis'],
+        [
+          {
+            name: 'xAxisSortByColumn',
+            config: {
+              type: 'SelectControl',
+              label: t('X axis sort by column'),
+              description: t(
+                'Column to use for ordering the waterfall based on a column',
+              ),
+              mapStateToProps: state => ({
+                choices: [
+                  ...(state.datasource?.columns || []).map(col => [
+                    col.column_name,
+                    col.column_name,
+                  ]),
+                ],
+                default: '',
+              }),
+              renderTrigger: false,
+              clearable: true,
+              visibility: ({ controls }) => Boolean(controls?.x_axis?.value),
+            },
+          },
+        ],
+        [
+          {
+            name: 'xAxisSortByColumnOrder',
+            config: {
+              type: 'SelectControl',
+              label: t('X axis sort by column order direction'),
+              choices: [
+                ['NONE', t('None')],
+                ['ASC', t('Ascending')],
+                ['DESC', t('Descending')],
+              ],
+              default: 'NONE',
+              clearable: false,
+              description: t(
+                'Ordering direction for the series, to be used with "Order 
series by column"',

Review Comment:
   The description text references 'Order series by column' but the actual 
control is labeled 'X axis sort by column'. The description should be updated 
to match the correct control name.
   ```suggestion
                   'Ordering direction for the series, to be used with "X axis 
sort by column"',
   ```



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts:
##########
@@ -452,14 +490,301 @@ export default function transformProps(
     series: barSeries,
   };
 
+  const processSeriesData = (options: EChartsOption) => {
+    let processedOptions = { ...options };
+
+    // Handle subtotal styling
+    if (useFirstValueAsSubtotal) {
+      const processedSeries = ((options.series as any[]) || []).map(series => {
+        const newData = series.data.map((dataPoint: any, index: number) => {
+          if (index !== 0) return dataPoint;
+          if (dataPoint?.itemStyle?.color === 'transparent') return dataPoint;
+          if (dataPoint.value === '-') return dataPoint;
+
+          const updatedColor = `rgba(${totalColor.r}, ${totalColor.g}, 
${totalColor.b})`;
+          return {
+            ...dataPoint,
+            itemStyle: {
+              ...dataPoint.itemStyle,
+              color: updatedColor,
+              borderColor: updatedColor,
+            },
+          };
+        });
+
+        return {
+          ...series,
+          data: newData,
+        };
+      });
+
+      processedOptions = {
+        ...processedOptions,
+        series: processedSeries,
+      };
+    }
+
+    // Handle total visibility
+    if (!showTotal) {
+      const totalsIndex =
+        ((processedOptions.series as any[]) || [])
+          .find(series => series.name === 'Total')
+          ?.data.map((dataPoint: any, index: number) =>
+            dataPoint.value !== '-' ? index : -1,
+          )
+          .filter((index: number) => index !== -1) || [];
+
+      const filteredData = [
+        ...((processedOptions.xAxis as { data: (string | number)[] }).data ||
+          []),
+      ].filter((_, index) => !totalsIndex.includes(index));
+
+      const filteredSeries = ((processedOptions.series as any[]) || []).map(
+        series => ({
+          ...series,
+          data: series.data.filter(
+            (_: any, index: number) => !totalsIndex.includes(index),
+          ),
+        }),
+      );
+
+      processedOptions = {
+        ...processedOptions,
+        xAxis: {
+          ...(processedOptions.xAxis as any),
+          data: filteredData,
+        },
+        series: filteredSeries,
+      };
+    }
+
+    // Handle orientation
+    if (orientation === 'horizontal') {
+      processedOptions = {
+        ...processedOptions,
+        xAxis: {
+          ...((processedOptions.yAxis as any) || {}),
+          type: 'value',
+        },
+        yAxis: {
+          ...((processedOptions.xAxis as any) || {}),
+          type: 'category',
+          data: [...(processedOptions.xAxis as any).data].reverse(),
+        },
+        series: Array.isArray(processedOptions.series)
+          ? processedOptions.series.map((series: any) => ({
+              ...series,
+              encode: {
+                x: series.encode?.y,
+                y: series.encode?.x,
+              },
+              data: [...series.data].reverse(),
+              label: {
+                ...(series.label || {}),
+                position: series.name === 'Decrease' ? 'left' : 'right',
+              },
+            }))
+          : [],
+      };
+    }
+
+    return processedOptions;
+  };
+
+  // adds more formatting to previous axisLabel.formatter
+  const getFormattedAxisOptions = (options: EChartsOption) => {
+    // If no formatting needed, return original options
+    if (boldLabels === 'none' && xTicksLayout !== 'flat') {
+      return options;
+    }
+
+    // Get total indices for bold formatting
+    const totalsIndex = ['total', 'both'].includes(boldLabels)
+      ? ((options.series as any[]) || [])
+          .find(series => series.name === 'Total')
+          ?.data.map((dataPoint: any, index: number) =>
+            dataPoint.value !== '-' ? index : -1,
+          )
+          .filter((index: number) => index !== -1) || []
+      : [];
+
+    const formatText = (value: string, index: number) => {
+      // Handle bold formatting first
+      let formattedValue = value;
+      if (value === TOTAL_MARK) {
+        formattedValue = TOTAL_MARK;
+      } else if (
+        coltypeMapping[xAxisColumns[index]] === GenericDataType.Temporal
+      ) {
+        if (typeof value === 'string') {
+          formattedValue = getTimeFormatter(xAxisTimeFormat)(
+            Number.parseInt(value, 10),
+          );
+        } else {
+          formattedValue = getTimeFormatter(xAxisTimeFormat)(value);
+        }
+      } else {
+        formattedValue = String(value);
+      }
+
+      if (orientation === 'vertical') {
+        if (index === 0 && ['subtotal', 'both'].includes(boldLabels)) {
+          formattedValue = `{subtotal|${formattedValue}}`;
+        } else if (
+          totalsIndex.includes(index) &&
+          ['total', 'both'].includes(boldLabels)
+        ) {
+          formattedValue = `{total|${formattedValue}}`;
+        }
+      } else {
+        const axisData = (options.yAxis as { data?: any[] })?.data || [];
+        const isLast = index === axisData.length - 1;
+        if (isLast && ['subtotal', 'both'].includes(boldLabels)) {
+          formattedValue = `{subtotal|${formattedValue}}`;
+        } else if (
+          totalsIndex.includes(index) &&
+          ['total', 'both'].includes(boldLabels)
+        ) {
+          formattedValue = `{total|${formattedValue}}`;
+        }
+      }
+
+      return formattedValue;
+    };
+
+    const richTextOptions = {
+      subtotal: ['subtotal', 'both'].includes(boldLabels)
+        ? { fontWeight: 'bold' }
+        : undefined,
+      total: ['total', 'both'].includes(boldLabels)
+        ? { fontWeight: 'bold' }
+        : undefined,
+    };
+
+    if (orientation === 'vertical') {
+      return {
+        ...options,
+        xAxis: {
+          ...(options.xAxis as any),
+          axisLabel: {
+            ...(options.xAxis as any)?.axisLabel,
+            formatter: formatText,
+            overflow: 'break',
+            interval: 0,
+            width: 70,
+            rich: {
+              ...(options.xAxis as any)?.axisLabel?.rich,
+              ...richTextOptions,
+            },
+          },
+        },
+      };
+    }
+
+    return {
+      ...options,
+      yAxis: {
+        ...(options.yAxis as any),
+        axisLabel: {
+          ...(options.yAxis as any)?.axisLabel,
+          formatter: formatText,
+          overflow: 'break',
+          rich: {
+            ...(options.yAxis as any)?.axisLabel?.rich,
+            ...richTextOptions,
+          },
+        },
+      },
+    };
+  };
+
+  const processedSeriesData = processSeriesData(baseEchartOptions);
+  const echartOptions = getFormattedAxisOptions(processedSeriesData);
+
+  const handleCrossFilter = (value: string, isCurrentValue: boolean) => {
+    if (value === 'Total') return null;
+
+    if (isCurrentValue) {
+      return {
+        extraFormData: {},
+        filterState: {
+          value: null,
+        },
+      };
+    }
+
+    return {
+      extraFormData: {
+        filters: [
+          {
+            col: xAxis,
+            op: '==',
+            val: value,
+          },
+        ],
+      },
+      filterState: {
+        value,
+      },
+    };
+  };
+
+  const getCrossFilteredSeries = (
+    series: any[],
+    filterValue: string | null,
+  ) => {
+    if (!filterValue) {
+      return series.map(s => ({
+        ...s,
+        itemStyle: {
+          ...s.itemStyle,
+          opacity: 1,
+        },
+      }));
+    }
+
+    const xAxisLabel = baseEchartOptions.xAxis as { data: (string | number)[] 
};
+    const valueIndex = (xAxisLabel?.data || []).indexOf(filterValue);
+
+    return series.map(s => ({
+      ...s,
+      data: s.data.map((d: any, idx: number) => ({
+        ...d,
+        itemStyle: {
+          ...d.itemStyle,
+          opacity: !Number.isNaN(d.value) && idx === valueIndex ? 1 : 0.3,

Review Comment:
   [nitpick] The magic number 0.3 for opacity should be extracted to a named 
constant for better maintainability. Consider defining it as DIMMED_OPACITY = 
0.3.
   ```suggestion
             opacity: !Number.isNaN(d.value) && idx === valueIndex ? 1 : 
DIMMED_OPACITY,
   ```



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts:
##########
@@ -156,11 +156,45 @@ export default function transformProps(
     legendState,
     queriesData,
     hooks,
+    filterState,
     theme,
+    emitCrossFilters,
     inContextMenu,
   } = chartProps;
   const refs: Refs = {};
-  const { data = [] } = queriesData[0];
+  let { data = [] } = queriesData[0];
+
+  const { xAxisSortByColumn, xAxisSortByColumnOrder } = formData;
+  if (
+    xAxisSortByColumn &&
+    xAxisSortByColumnOrder &&
+    xAxisSortByColumnOrder !== 'NONE'
+  ) {
+    const sortColumn = xAxisSortByColumn;
+    const isAscending = xAxisSortByColumnOrder === 'ASC';
+
+    data = [...data].sort((a, b) => {
+      const aValue = a[sortColumn];
+      const bValue = b[sortColumn];
+
+      // Handle null/undefined values
+      if (aValue == null && bValue == null) return 0;
+      if (aValue == null) return isAscending ? -1 : 1;
+      if (bValue == null) return isAscending ? 1 : -1;

Review Comment:
   Use strict equality checks (=== null) instead of loose equality (== null) 
for better type safety and clearer intent.
   ```suggestion
         if ((aValue === null || aValue === undefined) && (bValue === null || 
bValue === undefined)) return 0;
         if (aValue === null || aValue === undefined) return isAscending ? -1 : 
1;
         if (bValue === null || bValue === undefined) return isAscending ? 1 : 
-1;
   ```



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts:
##########
@@ -452,14 +490,301 @@ export default function transformProps(
     series: barSeries,
   };
 
+  const processSeriesData = (options: EChartsOption) => {
+    let processedOptions = { ...options };
+
+    // Handle subtotal styling
+    if (useFirstValueAsSubtotal) {
+      const processedSeries = ((options.series as any[]) || []).map(series => {
+        const newData = series.data.map((dataPoint: any, index: number) => {
+          if (index !== 0) return dataPoint;
+          if (dataPoint?.itemStyle?.color === 'transparent') return dataPoint;
+          if (dataPoint.value === '-') return dataPoint;
+
+          const updatedColor = `rgba(${totalColor.r}, ${totalColor.g}, 
${totalColor.b})`;
+          return {
+            ...dataPoint,
+            itemStyle: {
+              ...dataPoint.itemStyle,
+              color: updatedColor,
+              borderColor: updatedColor,
+            },
+          };
+        });
+
+        return {
+          ...series,
+          data: newData,
+        };
+      });
+
+      processedOptions = {
+        ...processedOptions,
+        series: processedSeries,
+      };
+    }
+
+    // Handle total visibility
+    if (!showTotal) {
+      const totalsIndex =
+        ((processedOptions.series as any[]) || [])
+          .find(series => series.name === 'Total')
+          ?.data.map((dataPoint: any, index: number) =>
+            dataPoint.value !== '-' ? index : -1,
+          )
+          .filter((index: number) => index !== -1) || [];
+
+      const filteredData = [
+        ...((processedOptions.xAxis as { data: (string | number)[] }).data ||
+          []),
+      ].filter((_, index) => !totalsIndex.includes(index));
+
+      const filteredSeries = ((processedOptions.series as any[]) || []).map(
+        series => ({
+          ...series,
+          data: series.data.filter(
+            (_: any, index: number) => !totalsIndex.includes(index),
+          ),
+        }),
+      );
+
+      processedOptions = {
+        ...processedOptions,
+        xAxis: {
+          ...(processedOptions.xAxis as any),
+          data: filteredData,
+        },
+        series: filteredSeries,
+      };
+    }
+
+    // Handle orientation
+    if (orientation === 'horizontal') {
+      processedOptions = {
+        ...processedOptions,
+        xAxis: {
+          ...((processedOptions.yAxis as any) || {}),
+          type: 'value',
+        },
+        yAxis: {
+          ...((processedOptions.xAxis as any) || {}),
+          type: 'category',
+          data: [...(processedOptions.xAxis as any).data].reverse(),
+        },
+        series: Array.isArray(processedOptions.series)
+          ? processedOptions.series.map((series: any) => ({
+              ...series,
+              encode: {
+                x: series.encode?.y,
+                y: series.encode?.x,
+              },
+              data: [...series.data].reverse(),
+              label: {
+                ...(series.label || {}),
+                position: series.name === 'Decrease' ? 'left' : 'right',
+              },
+            }))
+          : [],
+      };
+    }
+
+    return processedOptions;
+  };
+
+  // adds more formatting to previous axisLabel.formatter
+  const getFormattedAxisOptions = (options: EChartsOption) => {
+    // If no formatting needed, return original options
+    if (boldLabels === 'none' && xTicksLayout !== 'flat') {
+      return options;
+    }
+
+    // Get total indices for bold formatting
+    const totalsIndex = ['total', 'both'].includes(boldLabels)
+      ? ((options.series as any[]) || [])
+          .find(series => series.name === 'Total')
+          ?.data.map((dataPoint: any, index: number) =>
+            dataPoint.value !== '-' ? index : -1,
+          )
+          .filter((index: number) => index !== -1) || []
+      : [];
+
+    const formatText = (value: string, index: number) => {
+      // Handle bold formatting first
+      let formattedValue = value;
+      if (value === TOTAL_MARK) {
+        formattedValue = TOTAL_MARK;
+      } else if (
+        coltypeMapping[xAxisColumns[index]] === GenericDataType.Temporal
+      ) {
+        if (typeof value === 'string') {
+          formattedValue = getTimeFormatter(xAxisTimeFormat)(
+            Number.parseInt(value, 10),
+          );
+        } else {
+          formattedValue = getTimeFormatter(xAxisTimeFormat)(value);
+        }
+      } else {
+        formattedValue = String(value);
+      }
+
+      if (orientation === 'vertical') {
+        if (index === 0 && ['subtotal', 'both'].includes(boldLabels)) {
+          formattedValue = `{subtotal|${formattedValue}}`;
+        } else if (
+          totalsIndex.includes(index) &&
+          ['total', 'both'].includes(boldLabels)
+        ) {
+          formattedValue = `{total|${formattedValue}}`;
+        }
+      } else {
+        const axisData = (options.yAxis as { data?: any[] })?.data || [];
+        const isLast = index === axisData.length - 1;
+        if (isLast && ['subtotal', 'both'].includes(boldLabels)) {
+          formattedValue = `{subtotal|${formattedValue}}`;
+        } else if (
+          totalsIndex.includes(index) &&
+          ['total', 'both'].includes(boldLabels)
+        ) {
+          formattedValue = `{total|${formattedValue}}`;
+        }
+      }
+
+      return formattedValue;
+    };
+
+    const richTextOptions = {
+      subtotal: ['subtotal', 'both'].includes(boldLabels)
+        ? { fontWeight: 'bold' }
+        : undefined,
+      total: ['total', 'both'].includes(boldLabels)
+        ? { fontWeight: 'bold' }
+        : undefined,
+    };
+
+    if (orientation === 'vertical') {
+      return {
+        ...options,
+        xAxis: {
+          ...(options.xAxis as any),
+          axisLabel: {
+            ...(options.xAxis as any)?.axisLabel,
+            formatter: formatText,
+            overflow: 'break',
+            interval: 0,
+            width: 70,

Review Comment:
   [nitpick] The magic number 70 for axis label width should be extracted to a 
named constant or made configurable for better maintainability.
   ```suggestion
               width: AXIS_LABEL_WIDTH,
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org
For additional commands, e-mail: notifications-h...@superset.apache.org

Reply via email to