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