korbit-ai[bot] commented on code in PR #33146:
URL: https://github.com/apache/superset/pull/33146#discussion_r2046346517


##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx:
##########
@@ -104,6 +199,20 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [
+          {
+            name: 'x_axis_label_distance',
+            config: {
+              type: 'TextControl',
+              label: t('X Axis Label Distance'),
+              description: t('Distance of the label from X axis (in pixels)'),
+              renderTrigger: true,
+              default: '25',
+              isInt: true,
+              // validators: [v => !Number.isNaN(v) && v >= 0],

Review Comment:
   ### Remove or implement commented validators <sub>![category 
Readability](https://img.shields.io/badge/Readability-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   There are commented-out validators for both x_axis_label_distance and 
y_axis_label_distance controls that should either be implemented or removed.
   
   ###### Why this matters
   Commented-out code creates confusion about whether the validation is 
intended to be used and adds unnecessary noise to the codebase.
   
   ###### Suggested change ∙ *Feature Preview*
   Either remove the commented validators or implement them properly:
   ```typescript
   validators: [v => !Number.isNaN(v) && v >= 0],
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/c11909f6-e26b-40c8-bdf7-2dae3ea6b119/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/c11909f6-e26b-40c8-bdf7-2dae3ea6b119?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/c11909f6-e26b-40c8-bdf7-2dae3ea6b119?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/c11909f6-e26b-40c8-bdf7-2dae3ea6b119?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/c11909f6-e26b-40c8-bdf7-2dae3ea6b119)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:efdd2929-3f92-4bc3-9205-c45b0700ee66 -->
   
   
   [](efdd2929-3f92-4bc3-9205-c45b0700ee66)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx:
##########
@@ -39,6 +40,48 @@ const config: ControlPanelConfig = {
         ['metric'],
         ['adhoc_filters'],
         ['row_limit'],
+        [
+          {
+            name: 'seriesOrderByColumn',
+            config: {
+              type: 'SelectControl',
+              label: t('Order Series By Column'),
+              description: t(
+                'Column to use for ordering the waterfall series with columns 
not in the chart',
+              ),
+              mapStateToProps: state => ({
+                choices: [
+                  [null, t('None')],
+                  ...(state.datasource?.columns || []).map(col => [
+                    col.column_name,
+                    col.column_name,
+                  ]),
+                ],
+              }),
+              default: null,
+              renderTrigger: true,
+            },
+          },
+        ],
+        [
+          {
+            name: 'seriesOrderDirection',
+            config: {
+              type: 'SelectControl',
+              label: t('Order Direction'),
+              choices: [
+                [null, t('None')],
+                ['ASC', t('Ascending')],
+                ['DESC', t('Descending')],
+              ],
+              default: null,
+              renderTrigger: true,

Review Comment:
   ### Order Direction Control Not Dependent on Column Selection 
<sub>![category 
Functionality](https://img.shields.io/badge/Functionality-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The seriesOrderDirection control allows selecting an order direction even 
when no seriesOrderByColumn is selected, which can lead to confusing behavior.
   
   ###### Why this matters
   Users might set an order direction without a column to order by, resulting 
in no effect and potentially confusing the user about why their sorting 
configuration isn't working.
   
   ###### Suggested change ∙ *Feature Preview*
   Add visibility condition to only show the order direction when a column is 
selected:
   
   ```typescript
               name: 'seriesOrderDirection',
               config: {
                 type: 'SelectControl',
                 label: t('Order Direction'),
                 choices: [
                   [null, t('None')],
                   ['ASC', t('Ascending')],
                   ['DESC', t('Descending')],
                 ],
                 default: null,
                 renderTrigger: true,
                 visibility: ({ controls }) => 
!!controls.seriesOrderByColumn.value,
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/69eefb0a-8dcb-418d-8cc7-fbe56fce6153/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/69eefb0a-8dcb-418d-8cc7-fbe56fce6153?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/69eefb0a-8dcb-418d-8cc7-fbe56fce6153?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/69eefb0a-8dcb-418d-8cc7-fbe56fce6153?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/69eefb0a-8dcb-418d-8cc7-fbe56fce6153)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:4bba72dd-fa1a-4e07-8dbe-c78ba6bff083 -->
   
   
   [](4bba72dd-fa1a-4e07-8dbe-c78ba6bff083)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx:
##########
@@ -104,6 +199,20 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [
+          {
+            name: 'x_axis_label_distance',
+            config: {
+              type: 'TextControl',
+              label: t('X Axis Label Distance'),
+              description: t('Distance of the label from X axis (in pixels)'),
+              renderTrigger: true,
+              default: '25',
+              isInt: true,

Review Comment:
   ### Type inconsistency in default value <sub>![category 
Readability](https://img.shields.io/badge/Readability-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The default value for axis label distances is specified as a string '25' 
while isInt: true indicates it should be a number.
   
   ###### Why this matters
   This type inconsistency between the default value and the expected type can 
lead to confusion and potential type coercion issues.
   
   ###### Suggested change ∙ *Feature Preview*
   Change the default value to be a number:
   ```typescript
   default: 25,
   isInt: true,
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b0957cfe-f4e8-45ae-8b37-2fe00fa28fe4/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b0957cfe-f4e8-45ae-8b37-2fe00fa28fe4?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b0957cfe-f4e8-45ae-8b37-2fe00fa28fe4?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b0957cfe-f4e8-45ae-8b37-2fe00fa28fe4?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b0957cfe-f4e8-45ae-8b37-2fe00fa28fe4)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:09c5d9bf-ffdf-4218-bac3-cba410f1d9bb -->
   
   
   [](09c5d9bf-ffdf-4218-bac3-cba410f1d9bb)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx:
##########
@@ -37,13 +161,351 @@ export default function EchartsWaterfall(
     },
   };
 
+  const getSubtotalOptions = (options: EChartsCoreOption) => {
+    if (!useFirstValueAsSubtotal) return options;
+
+    const xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ];
+
+    const processedSeries = ((options.series as any[]) || []).map(series => {
+      const newData = series.data.map((dataPoint: any, index: number) => {
+        if (index !== 0) return dataPoint;
+
+        const isTransparent =
+          dataPoint?.itemStyle?.color &&
+          dataPoint.itemStyle.color === 'transparent';
+
+        if (isTransparent) return dataPoint;
+
+        if (dataPoint.value === '-') return dataPoint;
+
+        const updatedColor = `rgba(${totalColor.r}, ${totalColor.g}, 
${totalColor.b}, ${totalColor.a})`;
+        return {
+          ...dataPoint,
+          itemStyle: {
+            ...dataPoint.itemStyle,
+            color: updatedColor,
+            borderColor: updatedColor,
+          },
+        };
+      });
+
+      return {
+        ...series,
+        data: newData,
+      };
+    });
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: xAxisData,
+      },
+      series: processedSeries,
+    };
+  };
+
+  const getShowTotalOptions = (options: EChartsCoreOption) => {
+    if (showTotal) return options;
+
+    const totalsIndex =
+      ((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 xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ].filter((_, index) => !totalsIndex.includes(index));
+
+    const filteredSeries = ((options.series as any[]) || []).map(series => ({
+      ...series,
+      data: series.data.filter(
+        (_: any, index: number) => !totalsIndex.includes(index),
+      ),
+    }));
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: xAxisData,
+      },
+      series: filteredSeries,
+    };
+  };
+
+  const getSortedOptions = (options: EChartsCoreOption) => {
+    if (sortXAxis === 'none') return options;
+    const xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ];
+
+    const sortedData = [...xAxisData];
+
+    sortedData.sort((a, b) => {
+      if (typeof a === 'number' && typeof b === 'number') {
+        return sortXAxis === 'asc' ? a - b : b - a;
+      }
+      const aStr = String(a);
+      const bStr = String(b);
+      return sortXAxis === 'asc'
+        ? aStr.localeCompare(bStr)
+        : bStr.localeCompare(aStr);
+    });
+
+    const indexMap = new Map(xAxisData.map((val, index) => [val, index]));
+
+    const sortedSeries = ((options.series as any[]) || []).map(series => ({
+      ...series,
+      data: sortedData.map(value => {
+        const index = indexMap.get(value);
+        return index !== undefined ? (series as any).data[index] : null;
+      }),
+    }));
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: sortedData,
+      },
+      series: sortedSeries,
+    };
+  };
+
+  const getFlippedOptions = (options: EChartsCoreOption) => {
+    if (orientation === 'vertical') return options;
+
+    return {
+      ...options,
+      xAxis: {
+        ...((options.yAxis as any) || {}),
+        type: 'value',
+        axisLine: {
+          show: true,
+          lineStyle: {
+            color: theme.colors.grayscale.light3,
+            width: 1,
+          },
+        },
+        splitLine: {
+          show: true,
+          lineStyle: {
+            color: theme.colors.grayscale.light2,
+            width: 1,
+            type: 'solid',
+          },
+        },
+        name: (options.yAxis as any)?.name || '',
+        nameLocation: 'middle',
+      },
+      yAxis: {
+        ...((options.xAxis as any) || {}),
+        type: 'category',
+        axisLine: { show: true },
+        data: [...(options.xAxis as any).data].reverse(),
+        name: (options.xAxis as any)?.name || '',
+        nameLocation: 'middle',
+      },
+      series: Array.isArray(options.series)
+        ? options.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',
+            },
+          }))
+        : [],
+    };
+  };
+
+  const getFormattedAxisOptions = (options: EChartsCoreOption) => {
+    const { xTicksLayout, xTicksWrapLength } = props.formData;
+
+    // If no formatting needed, return original options
+    if (!boldTotal && !boldSubTotal && xTicksLayout !== 'flat') {
+      return options;
+    }
+
+    // Get total indices for bold formatting
+    const totalsIndex = boldTotal
+      ? ((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 (orientation === 'vertical') {
+        if (index === 0 && boldSubTotal) {
+          formattedValue = `{subtotal|${value}}`;
+        } else if (totalsIndex.includes(index) && boldTotal) {
+          formattedValue = `{total|${value}}`;
+        }
+      } else {
+        const axisData = (options.yAxis as { data?: any[] })?.data || [];
+        const isLast = index === axisData.length - 1;
+        if (isLast && boldSubTotal) {
+          formattedValue = `{subtotal|${value}}`;
+        } else if (totalsIndex.includes(index) && boldTotal) {
+          formattedValue = `{total|${value}}`;
+        }
+      }
+
+      // get the width of xAxis to calculate the maxCharsPerLine
+      const getAxisRange = (options: EChartsCoreOption) => {
+        if (orientation === 'vertical') {
+          const xAxis = options.xAxis as any;
+          const grid = options.grid as any;
+
+          // Get actual chart area width accounting for grid margins
+          const availableWidth = width - (grid?.left || 0) - (grid?.right || 
0);
+
+          if (xAxis?.type === 'value') {
+            const range = xAxis.max - xAxis.min;
+            return Math.min(range, availableWidth);
+          }
+          if (xAxis?.type === 'category') {
+            const categories = xAxis.data?.length || 1;
+            // Calculate space per category
+            return availableWidth / categories;
+          }
+        } else {
+          const yAxis = options.yAxis as any;
+          const grid = options.grid as any;
+
+          // Get actual chart area height accounting for grid margins
+          const availableHeight =
+            height - (grid?.top || 0) - (grid?.bottom || 0);
+
+          if (yAxis?.type === 'value') {
+            const range = yAxis.max - yAxis.min;
+            return Math.min(range, availableHeight);
+          }
+          if (yAxis?.type === 'category') {
+            const categories = yAxis.data?.length || 1;
+            // Calculate space per category
+            return availableHeight / categories;
+          }
+        }
+
+        // Fallback to a reasonable default
+        return orientation === 'vertical' ? width * 0.8 : height * 0.8;
+      };
+
+      // Handle text wrapping if needed
+      const maxWidth = getAxisRange(options); // chart width
+      const maxCharsPerLine = Math.floor(maxWidth / xTicksWrapLength); // 
Approx chars per line

Review Comment:
   ### Inaccurate Text Wrapping Calculation <sub>![category 
Functionality](https://img.shields.io/badge/Functionality-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The text wrapping calculation assumes a fixed character width without 
accounting for variable-width fonts or special characters, which could lead to 
improper wrapping of axis labels.
   
   ###### Why this matters
   This simplistic approach to calculating text width can result in 
inconsistent or incorrect line breaks, potentially causing text overflow or 
poor layout in the chart's axis labels.
   
   ###### Suggested change ∙ *Feature Preview*
   Use a more robust approach to calculate text width:
   ```typescript
   const getTextWidth = (text: string) => {
     const canvas = document.createElement('canvas');
     const context = canvas.getContext('2d');
     if (!context) return 0;
     context.font = 
chartRef.current?.getEchartInstance().getOption()?.textStyle?.fontFamily || 
'sans-serif';
     return context.measureText(text).width;
   };
   
   const calculateWrappedText = (text: string, maxWidth: number) => {
     const words = text.split(' ');
     let line = '';
     let wrappedText = '';
     
     words.forEach(word => {
       const testLine = line + word + ' ';
       if (getTextWidth(testLine) > maxWidth) {
         wrappedText += line.trim() + '\n';
         line = word + ' ';
       } else {
         line = testLine;
       }
     });
     
     return wrappedText + line.trim();
   };
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0babf288-44d6-45e7-87ea-804ea1f002bb/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0babf288-44d6-45e7-87ea-804ea1f002bb?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0babf288-44d6-45e7-87ea-804ea1f002bb?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0babf288-44d6-45e7-87ea-804ea1f002bb?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0babf288-44d6-45e7-87ea-804ea1f002bb)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:8c391d30-23c8-4391-a78d-628c1d576cce -->
   
   
   [](8c391d30-23c8-4391-a78d-628c1d576cce)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts:
##########
@@ -17,17 +17,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
+import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
 import buildQuery from './buildQuery';
 import controlPanel from './controlPanel';
-import transformProps from './transformProps';
+import transformProps from './transformProps-copy';
 import thumbnail from './images/thumbnail.png';
 import example1 from './images/example1.png';
 import example2 from './images/example2.png';
 import example3 from './images/example3.png';
-import { EchartsWaterfallChartProps, EchartsWaterfallFormData } from './types';
+import { EchartsWaterfallChartProps, EchartsWaterfallFormData } from 
'./types-copy';

Review Comment:
   ### Suspicious file suffix in types import <sub>![category 
Readability](https://img.shields.io/badge/Readability-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   Similar to above, the types import path includes '-copy' suffix suggesting 
temporary file usage.
   
   ###### Why this matters
   Temporary file naming in production code can lead to maintenance confusion 
and versioning issues.
   
   ###### Suggested change ∙ *Feature Preview*
   ```typescript
   import { EchartsWaterfallChartProps, EchartsWaterfallFormData } from 
'./types';
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/79a9a675-581f-40ec-b83c-a0213a2bac7c/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/79a9a675-581f-40ec-b83c-a0213a2bac7c?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/79a9a675-581f-40ec-b83c-a0213a2bac7c?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/79a9a675-581f-40ec-b83c-a0213a2bac7c?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/79a9a675-581f-40ec-b83c-a0213a2bac7c)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:f385883e-ad09-4ed4-b493-4f49ab6a22ba -->
   
   
   [](f385883e-ad09-4ed4-b493-4f49ab6a22ba)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx:
##########
@@ -16,16 +16,140 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { EChartsCoreOption } from 'echarts/core';
+import { useTheme } from '@superset-ui/core';
+import React, { useRef } from 'react';
 import Echart from '../components/Echart';
-import { WaterfallChartTransformedProps } from './types';
+import { WaterfallChartTransformedProps } from './types-copy';
 import { EventHandlers } from '../types';
 
 export default function EchartsWaterfall(
   props: WaterfallChartTransformedProps,
 ) {
-  const { height, width, echartOptions, refs, onLegendStateChanged } = props;
+  const {
+    height,
+    width,
+    echartOptions,
+    setDataMask,
+    onContextMenu,
+    refs,
+    onLegendStateChanged,
+    formData: {
+      sortXAxis,
+      orientation,
+      showTotal,
+      useFirstValueAsSubtotal,
+      totalColor,
+      xAxisLabelDistance,
+      yAxisLabelDistance,
+      boldTotal,
+      boldSubTotal,
+    },
+    emitCrossFilters,
+  } = props;
+
+  const theme = useTheme();
+  const chartRef = useRef<any>(null);
 
   const eventHandlers: EventHandlers = {
+    click: params => {

Review Comment:
   ### Missing documentation for complex click handler <sub>![category 
Documentation](https://img.shields.io/badge/Documentation-7c3aed)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The click event handler has complex business logic related to filtering and 
visual state management, but lacks documentation explaining its purpose and 
behavior.
   
   ###### Why this matters
   Without clear documentation of this handler's behavior, future maintainers 
will have difficulty understanding the filtering logic and its interaction with 
the chart's visual state.
   
   ###### Suggested change ∙ *Feature Preview*
   const eventHandlers: EventHandlers = {
       /** 
        * Handles click events on chart elements to manage cross-filtering.
        * When an element is clicked:
        * - Applies filter if element isn't currently filtered
        * - Clears filter if element is currently filtered
        * - Updates visual state to highlight/unhighlight elements
        * - Ignores clicks on 'Total' column
        */
       click: params => {
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/a10bc2ef-52ff-4f34-892e-3bba1ac60bf2/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/a10bc2ef-52ff-4f34-892e-3bba1ac60bf2?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/a10bc2ef-52ff-4f34-892e-3bba1ac60bf2?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/a10bc2ef-52ff-4f34-892e-3bba1ac60bf2?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/a10bc2ef-52ff-4f34-892e-3bba1ac60bf2)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:5ad15ae9-42d4-4b1f-9b11-681a4364fba6 -->
   
   
   [](5ad15ae9-42d4-4b1f-9b11-681a4364fba6)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx:
##########
@@ -37,13 +161,351 @@ export default function EchartsWaterfall(
     },
   };
 
+  const getSubtotalOptions = (options: EChartsCoreOption) => {
+    if (!useFirstValueAsSubtotal) return options;
+
+    const xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ];
+
+    const processedSeries = ((options.series as any[]) || []).map(series => {
+      const newData = series.data.map((dataPoint: any, index: number) => {
+        if (index !== 0) return dataPoint;
+
+        const isTransparent =
+          dataPoint?.itemStyle?.color &&
+          dataPoint.itemStyle.color === 'transparent';
+
+        if (isTransparent) return dataPoint;
+
+        if (dataPoint.value === '-') return dataPoint;
+
+        const updatedColor = `rgba(${totalColor.r}, ${totalColor.g}, 
${totalColor.b}, ${totalColor.a})`;
+        return {
+          ...dataPoint,
+          itemStyle: {
+            ...dataPoint.itemStyle,
+            color: updatedColor,
+            borderColor: updatedColor,
+          },
+        };
+      });
+
+      return {
+        ...series,
+        data: newData,
+      };
+    });
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: xAxisData,
+      },
+      series: processedSeries,
+    };
+  };
+
+  const getShowTotalOptions = (options: EChartsCoreOption) => {
+    if (showTotal) return options;
+
+    const totalsIndex =
+      ((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 xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ].filter((_, index) => !totalsIndex.includes(index));
+
+    const filteredSeries = ((options.series as any[]) || []).map(series => ({
+      ...series,
+      data: series.data.filter(
+        (_: any, index: number) => !totalsIndex.includes(index),
+      ),
+    }));
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: xAxisData,
+      },
+      series: filteredSeries,
+    };
+  };
+
+  const getSortedOptions = (options: EChartsCoreOption) => {
+    if (sortXAxis === 'none') return options;
+    const xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ];
+
+    const sortedData = [...xAxisData];
+
+    sortedData.sort((a, b) => {
+      if (typeof a === 'number' && typeof b === 'number') {
+        return sortXAxis === 'asc' ? a - b : b - a;
+      }
+      const aStr = String(a);
+      const bStr = String(b);
+      return sortXAxis === 'asc'
+        ? aStr.localeCompare(bStr)
+        : bStr.localeCompare(aStr);
+    });

Review Comment:
   ### Inconsistent Mixed-Type Data Sorting <sub>![category 
Functionality](https://img.shields.io/badge/Functionality-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The sorting logic mixes numeric and string comparisons without handling 
mixed-type data correctly, which could lead to unexpected sorting results when 
the data contains both numbers and strings.
   
   ###### Why this matters
   When the dataset contains both numbers and strings, the current 
implementation might produce inconsistent sorting orders, affecting the 
visualization's clarity and user understanding.
   
   ###### Suggested change ∙ *Feature Preview*
   Implement a consistent sorting strategy that handles mixed types uniformly:
   ```typescript
   sortedData.sort((a, b) => {
     const aNum = Number(a);
     const bNum = Number(b);
     const areNumbers = !Number.isNaN(aNum) && !Number.isNaN(bNum);
     
     if (areNumbers) {
       return sortXAxis === 'asc' ? aNum - bNum : bNum - aNum;
     }
     // If not both numbers, treat as strings
     const aStr = String(a);
     const bStr = String(b);
     return sortXAxis === 'asc'
       ? aStr.localeCompare(bStr)
       : bStr.localeCompare(aStr);
   });
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f6e896b7-835b-413c-9ce8-c27fc392e2f1/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f6e896b7-835b-413c-9ce8-c27fc392e2f1?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f6e896b7-835b-413c-9ce8-c27fc392e2f1?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f6e896b7-835b-413c-9ce8-c27fc392e2f1?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f6e896b7-835b-413c-9ce8-c27fc392e2f1)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:0a294b1d-b4fe-4d27-ac47-e65eeacdf45a -->
   
   
   [](0a294b1d-b4fe-4d27-ac47-e65eeacdf45a)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx:
##########
@@ -16,16 +16,140 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { EChartsCoreOption } from 'echarts/core';
+import { useTheme } from '@superset-ui/core';
+import React, { useRef } from 'react';
 import Echart from '../components/Echart';
-import { WaterfallChartTransformedProps } from './types';
+import { WaterfallChartTransformedProps } from './types-copy';
 import { EventHandlers } from '../types';
 
 export default function EchartsWaterfall(
   props: WaterfallChartTransformedProps,
 ) {
-  const { height, width, echartOptions, refs, onLegendStateChanged } = props;
+  const {
+    height,
+    width,
+    echartOptions,
+    setDataMask,
+    onContextMenu,
+    refs,
+    onLegendStateChanged,
+    formData: {
+      sortXAxis,
+      orientation,
+      showTotal,
+      useFirstValueAsSubtotal,
+      totalColor,
+      xAxisLabelDistance,
+      yAxisLabelDistance,
+      boldTotal,
+      boldSubTotal,
+    },
+    emitCrossFilters,
+  } = props;
+
+  const theme = useTheme();
+  const chartRef = useRef<any>(null);
 
   const eventHandlers: EventHandlers = {
+    click: params => {
+      if (!setDataMask || !emitCrossFilters) return;
+
+      const { name: value } = params;
+      const xAxisColumn = props.formData.xAxis;
+
+      // Don't filter on Total column
+      if (value === 'Total') return;
+
+      const isCurrentValue = props.filterState?.value === value;
+
+      if (isCurrentValue) {
+        // Clear the filter and visual state
+        setDataMask({
+          extraFormData: {},
+          filterState: {
+            value: null,
+          },
+        });
+
+        if (chartRef.current) {
+          const series = echartOptions.series as any[];
+          const updatedSeries = series.map(s => ({
+            ...s,
+            itemStyle: {
+              ...s.itemStyle,
+              opacity: 1,
+            },
+          }));
+
+          if (orientation === 'vertical') {
+            chartRef.current.getEchartInstance().setOption({
+              series: updatedSeries,
+            });
+          } else {
+            chartRef.current.getEchartInstance().setOption({
+              series: updatedSeries.map(s => ({
+                ...s,
+                data: [...s.data].reverse(),
+              })),
+            });
+          }
+        }
+        return;
+      }
+
+      // Set new filter
+      setDataMask({
+        extraFormData: {
+          filters: [
+            {
+              col: xAxisColumn,
+              op: '==',
+              val: value,
+            },
+          ],
+        },
+        filterState: {
+          value,
+        },
+      });
+
+      if (chartRef.current) {
+        const series = echartOptions.series as any[];
+        const xAxisLabel = echartOptions.xAxis as { data: (string | number)[] 
};
+        // get index of value in xAxisLabel
+        const valueIndex = (xAxisLabel?.data || []).indexOf(value);
+
+        const updatedSeries = 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:
   ### Scattered series transformation logic <sub>![category 
Readability](https://img.shields.io/badge/Readability-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The series transformation logic is repeated in multiple places with similar 
patterns, making the code harder to follow.
   
   ###### Why this matters
   Similar transformation patterns scattered throughout the code increase 
cognitive load and make maintenance more difficult.
   
   ###### Suggested change ∙ *Feature Preview*
   Extract common series transformation logic into a utility function:
   ```typescript
   const updateSeriesOpacity = (series: any[], valueIndex: number | null) => 
series.map(s => ({
     ...s,
     data: s.data.map((d: any, idx: number) => ({
       ...d,
       itemStyle: {
         ...d.itemStyle,
         opacity: valueIndex === null ? 1 : (!Number.isNaN(d.value) && idx === 
valueIndex ? 1 : 0.3),
       },
     })),
   }));
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/56a0579f-6bc7-4dc4-a0ab-8b307240bb82/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/56a0579f-6bc7-4dc4-a0ab-8b307240bb82?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/56a0579f-6bc7-4dc4-a0ab-8b307240bb82?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/56a0579f-6bc7-4dc4-a0ab-8b307240bb82?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/56a0579f-6bc7-4dc4-a0ab-8b307240bb82)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:d314ddb5-9ccd-4ac8-8337-b43cd6f53f46 -->
   
   
   [](d314ddb5-9ccd-4ac8-8337-b43cd6f53f46)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx:
##########
@@ -37,13 +161,351 @@ export default function EchartsWaterfall(
     },
   };
 
+  const getSubtotalOptions = (options: EChartsCoreOption) => {
+    if (!useFirstValueAsSubtotal) return options;
+
+    const xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ];
+
+    const processedSeries = ((options.series as any[]) || []).map(series => {
+      const newData = series.data.map((dataPoint: any, index: number) => {
+        if (index !== 0) return dataPoint;
+
+        const isTransparent =
+          dataPoint?.itemStyle?.color &&
+          dataPoint.itemStyle.color === 'transparent';
+
+        if (isTransparent) return dataPoint;
+
+        if (dataPoint.value === '-') return dataPoint;
+
+        const updatedColor = `rgba(${totalColor.r}, ${totalColor.g}, 
${totalColor.b}, ${totalColor.a})`;
+        return {
+          ...dataPoint,
+          itemStyle: {
+            ...dataPoint.itemStyle,
+            color: updatedColor,
+            borderColor: updatedColor,
+          },
+        };
+      });
+
+      return {
+        ...series,
+        data: newData,
+      };
+    });
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: xAxisData,
+      },
+      series: processedSeries,
+    };
+  };
+
+  const getShowTotalOptions = (options: EChartsCoreOption) => {
+    if (showTotal) return options;
+
+    const totalsIndex =
+      ((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 xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ].filter((_, index) => !totalsIndex.includes(index));
+
+    const filteredSeries = ((options.series as any[]) || []).map(series => ({
+      ...series,
+      data: series.data.filter(
+        (_: any, index: number) => !totalsIndex.includes(index),
+      ),
+    }));
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: xAxisData,
+      },
+      series: filteredSeries,
+    };
+  };
+
+  const getSortedOptions = (options: EChartsCoreOption) => {
+    if (sortXAxis === 'none') return options;
+    const xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ];
+
+    const sortedData = [...xAxisData];
+
+    sortedData.sort((a, b) => {
+      if (typeof a === 'number' && typeof b === 'number') {
+        return sortXAxis === 'asc' ? a - b : b - a;
+      }
+      const aStr = String(a);
+      const bStr = String(b);
+      return sortXAxis === 'asc'
+        ? aStr.localeCompare(bStr)
+        : bStr.localeCompare(aStr);
+    });
+
+    const indexMap = new Map(xAxisData.map((val, index) => [val, index]));
+
+    const sortedSeries = ((options.series as any[]) || []).map(series => ({
+      ...series,
+      data: sortedData.map(value => {
+        const index = indexMap.get(value);
+        return index !== undefined ? (series as any).data[index] : null;
+      }),
+    }));
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: sortedData,
+      },
+      series: sortedSeries,
+    };
+  };
+
+  const getFlippedOptions = (options: EChartsCoreOption) => {
+    if (orientation === 'vertical') return options;
+
+    return {
+      ...options,
+      xAxis: {
+        ...((options.yAxis as any) || {}),
+        type: 'value',
+        axisLine: {
+          show: true,
+          lineStyle: {
+            color: theme.colors.grayscale.light3,
+            width: 1,
+          },
+        },
+        splitLine: {
+          show: true,
+          lineStyle: {
+            color: theme.colors.grayscale.light2,
+            width: 1,
+            type: 'solid',
+          },
+        },
+        name: (options.yAxis as any)?.name || '',
+        nameLocation: 'middle',
+      },
+      yAxis: {
+        ...((options.xAxis as any) || {}),
+        type: 'category',
+        axisLine: { show: true },
+        data: [...(options.xAxis as any).data].reverse(),
+        name: (options.xAxis as any)?.name || '',
+        nameLocation: 'middle',
+      },
+      series: Array.isArray(options.series)
+        ? options.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',
+            },
+          }))
+        : [],
+    };
+  };
+
+  const getFormattedAxisOptions = (options: EChartsCoreOption) => {
+    const { xTicksLayout, xTicksWrapLength } = props.formData;
+
+    // If no formatting needed, return original options
+    if (!boldTotal && !boldSubTotal && xTicksLayout !== 'flat') {
+      return options;
+    }
+
+    // Get total indices for bold formatting
+    const totalsIndex = boldTotal
+      ? ((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 (orientation === 'vertical') {
+        if (index === 0 && boldSubTotal) {
+          formattedValue = `{subtotal|${value}}`;
+        } else if (totalsIndex.includes(index) && boldTotal) {
+          formattedValue = `{total|${value}}`;
+        }
+      } else {
+        const axisData = (options.yAxis as { data?: any[] })?.data || [];
+        const isLast = index === axisData.length - 1;
+        if (isLast && boldSubTotal) {
+          formattedValue = `{subtotal|${value}}`;
+        } else if (totalsIndex.includes(index) && boldTotal) {
+          formattedValue = `{total|${value}}`;
+        }
+      }
+
+      // get the width of xAxis to calculate the maxCharsPerLine
+      const getAxisRange = (options: EChartsCoreOption) => {
+        if (orientation === 'vertical') {
+          const xAxis = options.xAxis as any;
+          const grid = options.grid as any;
+
+          // Get actual chart area width accounting for grid margins
+          const availableWidth = width - (grid?.left || 0) - (grid?.right || 
0);
+
+          if (xAxis?.type === 'value') {
+            const range = xAxis.max - xAxis.min;
+            return Math.min(range, availableWidth);
+          }
+          if (xAxis?.type === 'category') {
+            const categories = xAxis.data?.length || 1;
+            // Calculate space per category
+            return availableWidth / categories;
+          }
+        } else {
+          const yAxis = options.yAxis as any;
+          const grid = options.grid as any;
+
+          // Get actual chart area height accounting for grid margins
+          const availableHeight =
+            height - (grid?.top || 0) - (grid?.bottom || 0);
+
+          if (yAxis?.type === 'value') {
+            const range = yAxis.max - yAxis.min;
+            return Math.min(range, availableHeight);
+          }
+          if (yAxis?.type === 'category') {
+            const categories = yAxis.data?.length || 1;
+            // Calculate space per category
+            return availableHeight / categories;
+          }
+        }
+
+        // Fallback to a reasonable default
+        return orientation === 'vertical' ? width * 0.8 : height * 0.8;
+      };
+
+      // Handle text wrapping if needed
+      const maxWidth = getAxisRange(options); // chart width
+      const maxCharsPerLine = Math.floor(maxWidth / xTicksWrapLength); // 
Approx chars per line
+
+      const words = formattedValue.split(' ');
+      let line = '';
+      let wrappedText = '';
+
+      words.forEach(word => {
+        if ((line + word).length > maxCharsPerLine) {
+          wrappedText += `${line.trim()}\n`;
+          line = `${word} `;
+        } else {
+          line += `${word} `;
+        }
+      });
+
+      wrappedText += line.trim();
+
+      return wrappedText;
+    };
+
+    if (orientation === 'vertical') {
+      return {
+        ...options,
+        xAxis: {
+          ...(options.xAxis as any),
+          axisLabel: {
+            ...(options.xAxis as any)?.axisLabel,
+            formatter: formatText,
+            overflow: 'break',
+            rich: {
+              ...(options.xAxis as any)?.axisLabel?.rich,
+              subtotal: boldSubTotal ? { fontWeight: 'bold' } : undefined,
+              total: boldTotal ? { fontWeight: 'bold' } : undefined,
+            },
+          },
+        },
+      };
+    }
+
+    return {
+      ...options,
+      yAxis: {
+        ...(options.yAxis as any),
+        axisLabel: {
+          ...(options.yAxis as any)?.axisLabel,
+          formatter: formatText,
+          overflow: 'break',
+          rich: {
+            ...(options.yAxis as any)?.axisLabel?.rich,
+            subtotal: boldSubTotal ? { fontWeight: 'bold' } : undefined,
+            total: boldTotal ? { fontWeight: 'bold' } : undefined,
+          },
+        },
+      },
+    };
+  };
+
+  const getLabelDistanceOptions = (options: EChartsCoreOption) => {
+    if (Number.isNaN(Number(xAxisLabelDistance))) {
+      console.error('xAxisLabelDistance should be a number');

Review Comment:
   ### Inadequate Error Handling for Invalid Label Distance <sub>![category 
Error Handling](https://img.shields.io/badge/Error%20Handling-ea580c)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   Error handling in getLabelDistanceOptions only logs to console and continues 
execution with potentially invalid data.
   
   ###### Why this matters
   Silent failure with console logging can lead to unexpected chart behavior 
since invalid values are still used in chart rendering.
   
   ###### Suggested change ∙ *Feature Preview*
   ```typescript
   const getLabelDistanceOptions = (options: EChartsCoreOption) => {
       if (Number.isNaN(Number(xAxisLabelDistance))) {
           throw new Error('xAxisLabelDistance must be a valid number');
       }
   
       if (Number.isNaN(Number(yAxisLabelDistance))) {
           throw new Error('yAxisLabelDistance must be a valid number');
       }
   
       return {
           ...options,
           xAxis: {
               ...(options.xAxis as any),
               nameGap: Number(xAxisLabelDistance),
           },
           yAxis: {
               ...(options.yAxis as any),
               nameGap: Number(yAxisLabelDistance),
           },
       };
   };
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/6d220c37-7b62-4491-86f5-f46984c786e0/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/6d220c37-7b62-4491-86f5-f46984c786e0?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/6d220c37-7b62-4491-86f5-f46984c786e0?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/6d220c37-7b62-4491-86f5-f46984c786e0?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/6d220c37-7b62-4491-86f5-f46984c786e0)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:a2a9ea6c-3388-461d-993b-93870db96dff -->
   
   
   [](a2a9ea6c-3388-461d-993b-93870db96dff)



##########
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx:
##########
@@ -37,13 +161,351 @@ export default function EchartsWaterfall(
     },
   };
 
+  const getSubtotalOptions = (options: EChartsCoreOption) => {
+    if (!useFirstValueAsSubtotal) return options;
+
+    const xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ];
+
+    const processedSeries = ((options.series as any[]) || []).map(series => {
+      const newData = series.data.map((dataPoint: any, index: number) => {
+        if (index !== 0) return dataPoint;
+
+        const isTransparent =
+          dataPoint?.itemStyle?.color &&
+          dataPoint.itemStyle.color === 'transparent';
+
+        if (isTransparent) return dataPoint;
+
+        if (dataPoint.value === '-') return dataPoint;
+
+        const updatedColor = `rgba(${totalColor.r}, ${totalColor.g}, 
${totalColor.b}, ${totalColor.a})`;
+        return {
+          ...dataPoint,
+          itemStyle: {
+            ...dataPoint.itemStyle,
+            color: updatedColor,
+            borderColor: updatedColor,
+          },
+        };
+      });
+
+      return {
+        ...series,
+        data: newData,
+      };
+    });
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: xAxisData,
+      },
+      series: processedSeries,
+    };
+  };
+
+  const getShowTotalOptions = (options: EChartsCoreOption) => {
+    if (showTotal) return options;
+
+    const totalsIndex =
+      ((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 xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ].filter((_, index) => !totalsIndex.includes(index));
+
+    const filteredSeries = ((options.series as any[]) || []).map(series => ({
+      ...series,
+      data: series.data.filter(
+        (_: any, index: number) => !totalsIndex.includes(index),
+      ),
+    }));
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: xAxisData,
+      },
+      series: filteredSeries,
+    };
+  };
+
+  const getSortedOptions = (options: EChartsCoreOption) => {
+    if (sortXAxis === 'none') return options;
+    const xAxisData = [
+      ...((options.xAxis as { data: (string | number)[] }).data || []),
+    ];
+
+    const sortedData = [...xAxisData];
+
+    sortedData.sort((a, b) => {
+      if (typeof a === 'number' && typeof b === 'number') {
+        return sortXAxis === 'asc' ? a - b : b - a;
+      }
+      const aStr = String(a);
+      const bStr = String(b);
+      return sortXAxis === 'asc'
+        ? aStr.localeCompare(bStr)
+        : bStr.localeCompare(aStr);
+    });
+
+    const indexMap = new Map(xAxisData.map((val, index) => [val, index]));
+
+    const sortedSeries = ((options.series as any[]) || []).map(series => ({
+      ...series,
+      data: sortedData.map(value => {
+        const index = indexMap.get(value);
+        return index !== undefined ? (series as any).data[index] : null;
+      }),
+    }));
+
+    return {
+      ...options,
+      xAxis: {
+        ...(options.xAxis as any),
+        data: sortedData,
+      },
+      series: sortedSeries,
+    };
+  };
+
+  const getFlippedOptions = (options: EChartsCoreOption) => {
+    if (orientation === 'vertical') return options;
+
+    return {
+      ...options,
+      xAxis: {
+        ...((options.yAxis as any) || {}),
+        type: 'value',
+        axisLine: {
+          show: true,
+          lineStyle: {
+            color: theme.colors.grayscale.light3,
+            width: 1,
+          },
+        },
+        splitLine: {
+          show: true,
+          lineStyle: {
+            color: theme.colors.grayscale.light2,
+            width: 1,
+            type: 'solid',
+          },
+        },
+        name: (options.yAxis as any)?.name || '',
+        nameLocation: 'middle',
+      },
+      yAxis: {
+        ...((options.xAxis as any) || {}),
+        type: 'category',
+        axisLine: { show: true },
+        data: [...(options.xAxis as any).data].reverse(),
+        name: (options.xAxis as any)?.name || '',
+        nameLocation: 'middle',
+      },
+      series: Array.isArray(options.series)
+        ? options.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',
+            },
+          }))
+        : [],
+    };
+  };
+
+  const getFormattedAxisOptions = (options: EChartsCoreOption) => {
+    const { xTicksLayout, xTicksWrapLength } = props.formData;
+
+    // If no formatting needed, return original options
+    if (!boldTotal && !boldSubTotal && xTicksLayout !== 'flat') {
+      return options;
+    }
+
+    // Get total indices for bold formatting
+    const totalsIndex = boldTotal
+      ? ((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 (orientation === 'vertical') {
+        if (index === 0 && boldSubTotal) {
+          formattedValue = `{subtotal|${value}}`;
+        } else if (totalsIndex.includes(index) && boldTotal) {
+          formattedValue = `{total|${value}}`;
+        }
+      } else {
+        const axisData = (options.yAxis as { data?: any[] })?.data || [];
+        const isLast = index === axisData.length - 1;
+        if (isLast && boldSubTotal) {
+          formattedValue = `{subtotal|${value}}`;
+        } else if (totalsIndex.includes(index) && boldTotal) {
+          formattedValue = `{total|${value}}`;
+        }
+      }
+
+      // get the width of xAxis to calculate the maxCharsPerLine
+      const getAxisRange = (options: EChartsCoreOption) => {
+        if (orientation === 'vertical') {
+          const xAxis = options.xAxis as any;
+          const grid = options.grid as any;
+
+          // Get actual chart area width accounting for grid margins
+          const availableWidth = width - (grid?.left || 0) - (grid?.right || 
0);
+
+          if (xAxis?.type === 'value') {
+            const range = xAxis.max - xAxis.min;
+            return Math.min(range, availableWidth);
+          }
+          if (xAxis?.type === 'category') {
+            const categories = xAxis.data?.length || 1;
+            // Calculate space per category
+            return availableWidth / categories;
+          }
+        } else {
+          const yAxis = options.yAxis as any;
+          const grid = options.grid as any;
+
+          // Get actual chart area height accounting for grid margins
+          const availableHeight =
+            height - (grid?.top || 0) - (grid?.bottom || 0);
+
+          if (yAxis?.type === 'value') {
+            const range = yAxis.max - yAxis.min;
+            return Math.min(range, availableHeight);
+          }
+          if (yAxis?.type === 'category') {
+            const categories = yAxis.data?.length || 1;
+            // Calculate space per category
+            return availableHeight / categories;
+          }
+        }
+
+        // Fallback to a reasonable default
+        return orientation === 'vertical' ? width * 0.8 : height * 0.8;
+      };

Review Comment:
   ### Deeply nested complex function <sub>![category 
Readability](https://img.shields.io/badge/Readability-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The getAxisRange function is nested within getFormattedAxisOptions making it 
hard to read and maintain. It's a complex function that handles both vertical 
and horizontal orientations with deep nesting.
   
   ###### Why this matters
   Deeply nested functions with complex branching make the code harder to 
follow and maintain. It also makes it difficult to test the logic independently.
   
   ###### Suggested change ∙ *Feature Preview*
   Extract getAxisRange as a standalone function at the component level:
   ```typescript
   const getAxisRange = (options: EChartsCoreOption, width: number, height: 
number, orientation: string) => {
     const calculateValueRange = (axis: any, availableSpace: number) => {
       if (axis?.type === 'value') {
         return Math.min(axis.max - axis.min, availableSpace);
       }
       if (axis?.type === 'category') {
         return availableSpace / (axis.data?.length || 1);
       }
       return availableSpace * 0.8;
     };
   
     const grid = options.grid as any;
     if (orientation === 'vertical') {
       const availableWidth = width - (grid?.left || 0) - (grid?.right || 0);
       return calculateValueRange(options.xAxis, availableWidth);
     }
     
     const availableHeight = height - (grid?.top || 0) - (grid?.bottom || 0);
     return calculateValueRange(options.yAxis, availableHeight);
   };
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/80b0d8e9-e7e7-47e0-9230-a0b3b8d1157b/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/80b0d8e9-e7e7-47e0-9230-a0b3b8d1157b?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/80b0d8e9-e7e7-47e0-9230-a0b3b8d1157b?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/80b0d8e9-e7e7-47e0-9230-a0b3b8d1157b?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/80b0d8e9-e7e7-47e0-9230-a0b3b8d1157b)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:049bc31b-876c-488b-a00b-0cabbdfc4a29 -->
   
   
   [](049bc31b-876c-488b-a00b-0cabbdfc4a29)



-- 
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: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to