-
Notifications
You must be signed in to change notification settings - Fork 14.9k
feat: Improves waterfall chart #33146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review by Korbit AI
Korbit automatically attempts to detect when you fix issues in new commits.
Category | Issue | Status |
---|---|---|
Suspicious file suffix in types import ▹ view | ✅ Fix detected | |
Remove or implement commented validators ▹ view | 🧠 Incorrect | |
Order Direction Control Not Dependent on Column Selection ▹ view | 🧠 Incorrect | |
Deeply nested complex function ▹ view | 🧠 Incorrect | |
Scattered series transformation logic ▹ view | 🧠 Not in scope | |
Inconsistent Mixed-Type Data Sorting ▹ view | 🧠 Incorrect | |
Inaccurate Text Wrapping Calculation ▹ view | 🧠 Not in scope | |
Type inconsistency in default value ▹ view | 🧠 Not in standard | |
Missing documentation for complex click handler ▹ view | ||
Inadequate Error Handling for Invalid Label Distance ▹ view | 🧠 Incorrect |
Files scanned
File Path | Reviewed |
---|---|
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts | ✅ |
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts | ✅ |
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts | ✅ |
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts | ✅ |
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx | ✅ |
superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx | ✅ |
Explore our documentation to understand the languages and file types we support and the files we ignore.
Need a new review? Comment
/korbit-review
on this PR and I'll review your latest changes.Korbit Guide: Usage and Customization
Interacting with Korbit
- You can manually ask Korbit to review your PR using the
/korbit-review
command in a comment at the root of your PR.- You can ask Korbit to generate a new PR description using the
/korbit-generate-pr-description
command in any comment on your PR.- Too many Korbit comments? I can resolve all my comment threads if you use the
/korbit-resolve
command in any comment on your PR.- On any given comment that Korbit raises on your pull request, you can have a discussion with Korbit by replying to the comment.
- Help train Korbit to improve your reviews by giving a 👍 or 👎 on the comments Korbit posts.
Customizing Korbit
- Check out our docs on how you can make Korbit work best for you and your team.
- Customize Korbit for your organization through the Korbit Console.
Current Korbit Configuration
General Settings
Setting Value Review Schedule Automatic excluding drafts Max Issue Count 10 Automatic PR Descriptions ❌ Issue Categories
Category Enabled Documentation ✅ Logging ✅ Error Handling ✅ Readability ✅ Design ✅ Performance ✅ Security ✅ Functionality ✅ Feedback and Support
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'; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
renderTrigger: true, | ||
default: '25', | ||
isInt: true, | ||
// validators: [v => !Number.isNaN(v) && v >= 0], |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
default: '25', | ||
isInt: true, |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
name: 'seriesOrderDirection', | ||
config: { | ||
type: 'SelectControl', | ||
label: t('Order Direction'), | ||
choices: [ | ||
[null, t('None')], | ||
['ASC', t('Ascending')], | ||
['DESC', t('Descending')], | ||
], | ||
default: null, | ||
renderTrigger: true, |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
const eventHandlers: EventHandlers = { | ||
click: params => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing documentation for complex click handler 
Tell me more
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
💬 Looking for more details? Reply to this comment to chat with Korbit.
|
||
const getLabelDistanceOptions = (options: EChartsCoreOption) => { | ||
if (Number.isNaN(Number(xAxisLabelDistance))) { | ||
console.error('xAxisLabelDistance should be a number'); |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
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; | ||
}; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
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, | ||
}, | ||
})), | ||
})); |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
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); | ||
}); |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
|
||
// Handle text wrapping if needed | ||
const maxWidth = getAxisRange(options); // chart width | ||
const maxCharsPerLine = Math.floor(maxWidth / xTicksWrapLength); // Approx chars per line |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Can you add some details to the Description so we know the intent of the change? Thanks! |
Details are attached. Please let me know if you need more info. |
The work performed on Superset by @anantaoutlook in this PR includes the following improvements: 1.) Horizontal waterfall in addition to vertical. 2.) Axis label fixes to word-wrap longer labels instead of hiding them. This is extremely important for readability/usability. 3.) Sorting of Waterfall: It is now possible to sort the waterfall by a field other than the field indicated in the waterfall column headers. Again, extremely important. As an example, in the following chart, we are bridging from gross revenue to Profit. It's vital that these grouping appear in the correct order: 4.) Added option to use first value as subtotal, and bold first value as subtotal. These are important optional settings for the waterfall. In the example, these allow the Gross Revenue bar to be presented as a subtotal (full bar and grey), and the label for Gross Revenue is bold. 5.) Cross-filtering is now feasible for the waterfall chart. |
Thanks @anantaoutlook @rea725 for the great improvements! I updated the PR description with @rea725's comment.
ECharts automatically calculates what labels will be displayed to avoid label overlaps when the number of bars is high. I think the solution here would be to provide a checkbox to show all labels (disabled by default). The same feature is currently being discussed in #33191 so please check the comments of that PR.
Can we assume that if the first value is used as a subtotal, it's always displayed with bold? It's good to avoid additional controls if possible. |
@anantaoutlook Could you also fill the Testing Instructions part of the PR description? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First-pass comments. Great work!
name: 'seriesOrderByColumn', | ||
config: { | ||
type: 'SelectControl', | ||
label: t('Order Series By Column'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
label: t('Order Series By Column'), | |
label: t('Order series by column'), |
name: 'seriesOrderDirection', | ||
config: { | ||
type: 'SelectControl', | ||
label: t('Order Direction'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
label: t('Order Direction'), | |
label: t('Order direction'), |
default: null, | ||
renderTrigger: true, | ||
description: t( | ||
'Ordering direction for the series, to be used with "Order Series By Column"', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'Ordering direction for the series, to be used with "Order Series By Column"', | |
'Ordering direction for the series, to be used with "Order series by column"', |
), | ||
mapStateToProps: state => ({ | ||
choices: [ | ||
[null, t('None')], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's important that this select does not accept null values and cannot be cleared. It should be set with the x-axis column by default.
name: 'show_total', | ||
config: { | ||
type: 'CheckboxControl', | ||
label: t('Show Total'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
label: t('Show Total'), | |
label: t('Show total'), |
name: 'bold_sub_total', | ||
config: { | ||
type: 'CheckboxControl', | ||
label: t('Bold first value as subtotal'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option would be to have a control like "Bold totals and subtotals".
config: { | ||
type: 'TextControl', | ||
label: t('X Axis Label Distance'), | ||
description: t('Distance of the label from X axis (in pixels)'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this? ECharts automatically calculates the distances.
renderTrigger: true, | ||
default: '25', | ||
isInt: true, | ||
// validators: [v => !Number.isNaN(v) && v >= 0], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// validators: [v => !Number.isNaN(v) && v >= 0], |
@@ -133,6 +242,53 @@ const config: ControlPanelConfig = { | |||
description: t('The way the ticks are laid out on the X-axis'), | |||
}, | |||
}, | |||
{ | |||
name: 'x_ticks_wrap_length', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this be automatically calculated?
renderTrigger: true, | ||
default: '25', | ||
isInt: true, | ||
// validators: [v => !Number.isNaN(v) && v >= 0], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// validators: [v => !Number.isNaN(v) && v >= 0], |
SUMMARY
This PR includes the following improvements:
1.) Horizontal waterfall in addition to vertical.

2.) Axis label fixes to word-wrap longer labels instead of hiding them. This is extremely important for readability/usability.

For example. Prior to these fixes, roughly half of the following labels in this chart were hidden:
3.) Sorting of Waterfall: It is now possible to sort the waterfall by a field other than the field indicated in the waterfall column headers. Again, extremely important. As an example, in the following chart, we are bridging from gross revenue to Profit. It's vital that these grouping appear in the correct order:


4.) Added option to use first value as subtotal, and bold first value as subtotal. These are important optional settings for the waterfall. In the example, these allow the Gross Revenue bar to be presented as a subtotal (full bar and grey), and the label for Gross Revenue is bold.


Before:
After:
5.) Cross-filtering is now feasible for the waterfall chart.
TESTING INSTRUCTIONS
ADDITIONAL INFORMATION