feat: add dual y-axis support to CartesianChart#182
feat: add dual y-axis support to CartesianChart#182abenitesDC wants to merge 1 commit intocloudscape-design:mainfrom
Conversation
src/cartesian-chart/interfaces.ts
Outdated
| * | ||
| * Use a single object for a single y axis, or a tuple of two objects for a dual-axis chart. | ||
| * When using a tuple, the second axis is automatically rendered on the opposite (right) side. | ||
| * Series can reference the secondary axis by setting `yAxis: 1`. |
There was a problem hiding this comment.
In highcharts, one can also reference axis by a given axis ID - should we add support for it as well?
let yAxis = { id: 'percentage' }
let series = { type: 'line', data: [...], yAxis: 'percentage' }
There was a problem hiding this comment.
In fact, we can consider making by-id reference the only supported one, to lesser a mistake risk
There was a problem hiding this comment.
aligned, will support series id
src/core/interfaces.ts
Outdated
|
|
||
| type BaseCartesianSeriesOptions = BaseSeriesOptions; | ||
| interface BaseCartesianSeriesOptions extends BaseSeriesOptions { | ||
| yAxis?: number; |
There was a problem hiding this comment.
The XThresholdSeriesOptions should not have yAxis
| function transformYAxisOptions( | ||
| axis?: CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisOptions, CartesianChartProps.YAxisOptions], | ||
| ): CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisOptions, CartesianChartProps.YAxisOptions] { | ||
| if (Array.isArray(axis)) { |
There was a problem hiding this comment.
There is an existing castArray util, can we use it here instead?
There was a problem hiding this comment.
castArray always returns an array (or undefined). On the other hand, this Y-axis transform function needs to return either a single value, or an array depending on the Y-axis value passed in. So it cannot be used directly to replace these 4 lines.
However, if you are asking to use castArray to remove the isArray check, then IMO it would be more verbose, and would not recommend it compared to the proposed change. Thoughts?
function transformYAxisOptions(
axis?: CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisOptions, CartesianChartProps.YAxisOptions],
): CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisOptions, CartesianChartProps.YAxisOptions] {
// castArray wraps a single value into [value], passes arrays through, and returns undefined for falsy input.
const axes = castArray(axis);
// Only true when axis is undefined, since castArray returns undefined for falsy values.
if (!axes) {
return transformSingleYAxisOptions(axis);
}
const result = axes.map(transformSingleYAxisOptions);
// A single YAxisOptions was wrapped into a one-element array by castArray,
// so we need to unwrap it back to preserve the original single-value return type.
// For the tuple case (length 2), we need a type assertion since .map() returns a generic array.
return axes.length === 1
? result[0]
: (result as [CartesianChartProps.YAxisOptions, CartesianChartProps.YAxisOptions]);
}
There was a problem hiding this comment.
Makes sense - let's keep the less verbose variant.
|
We wanted something like this! Thanks for implementing it. |
| dashStyle: s.dashStyle ?? Styles.thresholdSeries.dashStyle, | ||
| }; | ||
| return { type: "line", id: s.id, name: s.name, data, custom, enableMouseTracking, ...style, ...shared }; | ||
| return { |
There was a problem hiding this comment.
nit: we can improve clarity / consistency with the above code by doing:
const yAxis = s.type === "y-threshold" ? s.yAxis : undefined;
return { type: "line", ..., yAxis }
src/cartesian-chart/index.tsx
Outdated
| id: s.id, | ||
| name: s.name, | ||
| color: s.color, | ||
| ...(s.type === "y-threshold" && { yAxis: s.yAxis }), |
There was a problem hiding this comment.
same here - I recommend to compute the yAxis in a separate line for better structure
src/core/interfaces.ts
Outdated
| subItems?: ReadonlyArray<{ key: React.ReactNode; value: React.ReactNode }>; | ||
| } | ||
|
|
||
| export interface AreaSeriesOptions extends BaseCartesianLineLikeOptions { |
There was a problem hiding this comment.
Let's keep the names the same - w/o introducing the YAxis as it makes it more confusing. It is best to annotate with code comments why do we not treat the XThreshold as line-like.
| /** | ||
| * Finds chart's secondary legend when using dual-axis charts. | ||
| */ | ||
| public findSecondaryLegend(): null | BaseChartLegendWrapper { |
There was a problem hiding this comment.
- The findLegend and findYAxisTitle belong to the BaseChartWrapper - so we should target that one, not CartesianChartWrapper.
- It is best to make the methods more precise. The "secondary" term is not represented in the API - we use IDs instead. We can parametrise findLegend and findYAxisTitle with
{ axisId?: string }- to target specific axis/legend.
There was a problem hiding this comment.
Overall comment, if we use axis id, then the chart component would need to add a data-axisid so that the test utils can find the correct axis. That involves refactoring the core classes. Plus, we are only enforcing axis id if we use dual axis. So, we'd need to keep the 1-axis logic as part of this refactoring. I think searching by axis id be more ideal, but will increase the code logic for a maximum of 2 y-axis.
// Example
// tests-utils/.../base.ts
// current
public findLegend(): null | BaseChartLegendWrapper {
return this.findComponent(`.${BaseChartLegendWrapper.rootSelector}`, BaseChartLegendWrapper);
}
// proposed
public findLegend({ axisId }: { axisId?: string } = {}): null | BaseChartLegendWrapper {
const selector = axisId
? `.${BaseChartLegendWrapper.rootSelector}[data-axisid="${axisId}"]`
: `.${BaseChartLegendWrapper.rootSelector}`;
return this.findComponent(selector, BaseChartLegendWrapper);
}
On the other hand, we can still accept axis ID and change the test utilities to search for the existing legend and title primary and secondary CSS classes, without refactoring any core component. The drawback here is that in tests we are constrained to either use y-axis IDs only primary and secondary (so that they be matched to the existing css classes).
The last alternative would be to not have the axis ID param in the test utils, and instead have fixed methods findSecondaryXXX that internally will map to the respective css classes (similar to current PR changes).
what are your thoughts?
There was a problem hiding this comment.
Discussed in DM - we can attached test-id for the legends and use a custom class name with an ID as a workaround to find axes.
Allow CartesianChart to render two Y axes by passing a tuple to the yAxis prop. The second axis is automatically set to opposite (right side). Series can reference the secondary axis with yAxis: 1. Changes: - Add yAxis?: number to BaseCartesianSeriesOptions - Accept yAxis as single or [primary, secondary] tuple in CartesianChartProps - Pass yAxis through all series transform functions - Auto-set opposite: true on the secondary axis - Add unit tests and demo page
95b98d6 to
5d96606
Compare
Summary
yAxis?: numberto cartesian series types so series can reference which Y axis they belong toyAxisas a single object or[primary, secondary]tuple inCartesianChartPropsopposite: trueon the secondary axis when using the tuple formyAxisthrough all series transform functions (line, column, scatter, threshold, error bar)Test plan
tsc --noEmit)01-cartesian-chart/dual-axis-chart