diff --git a/pages/01-cartesian-chart/dual-axis-chart.page.tsx b/pages/01-cartesian-chart/dual-axis-chart.page.tsx
new file mode 100644
index 00000000..f5d22b93
--- /dev/null
+++ b/pages/01-cartesian-chart/dual-axis-chart.page.tsx
@@ -0,0 +1,82 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { CartesianChart, CartesianChartProps } from "../../lib/components";
+import { dateFormatter } from "../common/formatters";
+import { useChartSettings } from "../common/page-settings";
+import { Page } from "../common/templates";
+import pseudoRandom from "../utils/pseudo-random";
+
+function randomInt(min: number, max: number) {
+ return min + Math.floor(pseudoRandom() * (max - min));
+}
+
+const baseline = [
+ { x: 1600984800000, y: 58020 },
+ { x: 1600985700000, y: 102402 },
+ { x: 1600986600000, y: 104920 },
+ { x: 1600987500000, y: 94031 },
+ { x: 1600988400000, y: 125021 },
+ { x: 1600989300000, y: 159219 },
+ { x: 1600990200000, y: 193082 },
+ { x: 1600991100000, y: 162592 },
+ { x: 1600992000000, y: 274021 },
+ { x: 1600992900000, y: 264286 },
+ { x: 1600993800000, y: 289210 },
+ { x: 1600994700000, y: 256362 },
+ { x: 1600995600000, y: 257306 },
+ { x: 1600996500000, y: 186776 },
+ { x: 1600997400000, y: 294020 },
+ { x: 1600998300000, y: 385975 },
+ { x: 1600999200000, y: 486039 },
+ { x: 1601000100000, y: 490447 },
+ { x: 1601001000000, y: 361845 },
+ { x: 1601001900000, y: 339058 },
+ { x: 1601002800000, y: 298028 },
+ { x: 1601003400000, y: 255555 },
+ { x: 1601003700000, y: 231902 },
+ { x: 1601004600000, y: 224558 },
+ { x: 1601005500000, y: 253901 },
+ { x: 1601006400000, y: 102839 },
+ { x: 1601007300000, y: 234943 },
+ { x: 1601008200000, y: 204405 },
+ { x: 1601009100000, y: 190391 },
+ { x: 1601010000000, y: 183570 },
+ { x: 1601010900000, y: 162592 },
+ { x: 1601011800000, y: 148910 },
+];
+
+const eventsData = baseline.map(({ x, y }) => ({ x, y: y + randomInt(-50000, 50000) }));
+const percentageData = baseline.map(({ x, y }) => ({ x, y: (y / 10000) * randomInt(3, 10) }));
+
+const dualAxisProps = {
+ chartHeight: 400,
+ xAxis: {
+ title: "Time (UTC)",
+ type: "datetime" as const,
+ valueFormatter: dateFormatter,
+ },
+ yAxis: [
+ { id: "events", title: "Events" },
+ { id: "percentage", title: "Percentage (%)" },
+ ] as [CartesianChartProps.YAxisWithId, CartesianChartProps.YAxisWithId],
+};
+
+export default function () {
+ const { chartProps } = useChartSettings();
+ return (
+
+
+
+ );
+}
diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap
index 6d29eee6..0f0f5c6f 100644
--- a/src/__tests__/__snapshots__/documenter.test.ts.snap
+++ b/src/__tests__/__snapshots__/documenter.test.ts.snap
@@ -562,9 +562,13 @@ applies to the tooltip header.",
},
{
"description": "Defines options of the chart's y axis.
-This property corresponds to [xAxis](https://api.highcharts.com/highcharts/yAxis), and extends it
+This property corresponds to [yAxis](https://api.highcharts.com/highcharts/yAxis), and extends it
with a custom value formatter.
+Use a single object for a single y axis, or a tuple of two objects for a dual-axis chart.
+When using a tuple, both axes must have an \`id\`. The second axis is automatically rendered on the opposite (right) side.
+Series reference their axis by setting \`yAxis\` to the axis \`id\`.
+
Supported options:
* \`title\` (optional, string) - Axis title.
* \`type\` (optional, "linear" | "datetime" | "category" | "logarithmic") - Axis type.
@@ -580,75 +584,16 @@ Supported options:
* \`valueFormatter\` (optional, function) - Takes axis tick as input and returns a formatted string. This formatter also
applies to the tooltip points values.",
"inlineType": {
- "name": "CartesianChartProps.YAxisOptions",
- "properties": [
- {
- "name": "categories",
- "optional": true,
- "type": "Array",
- },
- {
- "name": "max",
- "optional": true,
- "type": "number",
- },
- {
- "name": "min",
- "optional": true,
- "type": "number",
- },
- {
- "name": "reversedStacks",
- "optional": true,
- "type": "boolean",
- },
- {
- "name": "tickInterval",
- "optional": true,
- "type": "number",
- },
- {
- "name": "title",
- "optional": true,
- "type": "string",
- },
- {
- "inlineType": {
- "name": ""category" | "datetime" | "linear" | "logarithmic"",
- "type": "union",
- "values": [
- "category",
- "datetime",
- "linear",
- "logarithmic",
- ],
- },
- "name": "type",
- "optional": true,
- "type": "string",
- },
- {
- "inlineType": {
- "name": "(value: number | null) => string",
- "parameters": [
- {
- "name": "value",
- "type": "number | null",
- },
- ],
- "returnType": "string",
- "type": "function",
- },
- "name": "valueFormatter",
- "optional": true,
- "type": "((value: number | null) => string)",
- },
+ "name": "CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisOptions, CartesianChartProps.YAxisOptions]",
+ "type": "union",
+ "values": [
+ "CartesianChartProps.YAxisOptions",
+ "[CartesianChartProps.YAxisOptions, CartesianChartProps.YAxisOptions]",
],
- "type": "object",
},
"name": "yAxis",
"optional": true,
- "type": "CartesianChartProps.YAxisOptions",
+ "type": "CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisOptions, CartesianChartProps.YAxisOptions]",
},
],
"regions": [
@@ -1764,7 +1709,7 @@ not overridden, but merged with Cloudscape event handlers so that both are getti
"type": "union",
"valueDescriptions": undefined,
"values": [
- "Omit",
+ "Omit",
"{ xAxis?: CoreChartProps.XAxisOptions | Array | undefined; yAxis?: CoreChartProps.YAxisOptions | Array | undefined; }",
],
},
diff --git a/src/cartesian-chart/__tests__/cartesian-chart-series.test.tsx b/src/cartesian-chart/__tests__/cartesian-chart-series.test.tsx
index 39499771..03e6ae9b 100644
--- a/src/cartesian-chart/__tests__/cartesian-chart-series.test.tsx
+++ b/src/cartesian-chart/__tests__/cartesian-chart-series.test.tsx
@@ -157,4 +157,98 @@ describe("CartesianChart: series", () => {
});
expect((hc.getChartSeries(0).options as SeriesLineOptions).dashStyle).toBe("Solid");
});
+
+ test("series yAxis is passed through to Highcharts", () => {
+ renderCartesianChart({
+ highcharts,
+ series: [
+ { type: "line", name: "Primary", data: [{ x: 1, y: 1 }], yAxis: "primary" },
+ { type: "line", name: "Secondary", data: [{ x: 1, y: 2 }], yAxis: "secondary" },
+ ],
+ yAxis: [
+ { id: "primary", title: "Primary" },
+ { id: "secondary", title: "Secondary" },
+ ],
+ });
+ expect(hc.getChartSeries(0).options.yAxis).toBe("primary");
+ expect(hc.getChartSeries(1).options.yAxis).toBe("secondary");
+ });
+
+ test("dual yAxis renders two axes with opposite on the secondary", () => {
+ renderCartesianChart({
+ highcharts,
+ series: [
+ { type: "line", name: "Primary", data: [{ x: 1, y: 1 }], yAxis: "primary" },
+ { type: "line", name: "Secondary", data: [{ x: 1, y: 2 }], yAxis: "secondary" },
+ ],
+ yAxis: [
+ { id: "primary", title: "Primary" },
+ { id: "secondary", title: "Secondary" },
+ ],
+ });
+ const chart = hc.getChart();
+ expect(chart.yAxis).toHaveLength(2);
+ expect(chart.yAxis[0].options.opposite).toBeFalsy();
+ expect(chart.yAxis[1].options.opposite).toBe(true);
+ });
+
+ test("series yAxis is not set when not provided", () => {
+ renderCartesianChart({
+ highcharts,
+ series: [{ type: "line", name: "Line", data: [{ x: 1, y: 1 }] }],
+ });
+ expect(hc.getChartSeries(0).options.yAxis).toBeUndefined();
+ });
+
+ test("findLegend with axisId returns null for single-axis chart", () => {
+ renderCartesianChart({
+ highcharts,
+ series: [{ type: "line", name: "Line", data: [{ x: 1, y: 1 }] }],
+ });
+ expect(getChart().findLegend({ axisId: "secondary" })).toBeNull();
+ });
+
+ test("findLegend with axisId returns secondary legend for dual-axis chart", () => {
+ renderCartesianChart({
+ highcharts,
+ series: [
+ { type: "line", name: "Primary", data: [{ x: 1, y: 1 }], yAxis: "primary" },
+ { type: "line", name: "Secondary", data: [{ x: 1, y: 2 }], yAxis: "secondary" },
+ ],
+ yAxis: [
+ { id: "primary", title: "Primary" },
+ { id: "secondary", title: "Secondary" },
+ ],
+ });
+ const secondaryLegend = getChart().findLegend({ axisId: "secondary" });
+ expect(secondaryLegend).not.toBeNull();
+ expect(secondaryLegend!.findItems()).toHaveLength(1);
+ expect(secondaryLegend!.findItems()[0].getElement().textContent).toBe("Secondary");
+ });
+
+ test("findYAxisTitle with axisId returns null for single-axis chart", () => {
+ renderCartesianChart({
+ highcharts,
+ series: [{ type: "line", name: "Line", data: [{ x: 1, y: 1 }] }],
+ yAxis: { title: "Only axis" },
+ });
+ expect(getChart().findYAxisTitle({ axisId: "secondary" })).toBeNull();
+ });
+
+ test("findYAxisTitle with axisId returns secondary axis title for dual-axis chart", () => {
+ renderCartesianChart({
+ highcharts,
+ series: [
+ { type: "line", name: "Primary", data: [{ x: 1, y: 1 }], yAxis: "primary" },
+ { type: "line", name: "Secondary", data: [{ x: 1, y: 2 }], yAxis: "secondary" },
+ ],
+ yAxis: [
+ { id: "primary", title: "Primary axis" },
+ { id: "secondary", title: "Secondary axis" },
+ ],
+ });
+ const secondaryTitle = getChart().findYAxisTitle({ axisId: "secondary" });
+ expect(secondaryTitle).not.toBeNull();
+ expect(secondaryTitle!.getElement().textContent).toBe("Secondary axis");
+ });
});
diff --git a/src/cartesian-chart/chart-cartesian-internal.tsx b/src/cartesian-chart/chart-cartesian-internal.tsx
index be4ff121..6c0d0f7a 100644
--- a/src/cartesian-chart/chart-cartesian-internal.tsx
+++ b/src/cartesian-chart/chart-cartesian-internal.tsx
@@ -116,10 +116,11 @@ export const InternalCartesianChart = forwardRef(
title: { text: xAxisProps.title },
plotLines: xPlotLines,
})),
- yAxis: castArray(props.yAxis)?.map((yAxisProps) => ({
+ yAxis: castArray(props.yAxis)?.map((yAxisProps, index) => ({
...yAxisProps,
title: { text: yAxisProps.title },
plotLines: yPlotLines,
+ ...(index === 1 ? { opposite: true } : {}),
})),
}}
tooltip={tooltip}
diff --git a/src/cartesian-chart/chart-series-cartesian.tsx b/src/cartesian-chart/chart-series-cartesian.tsx
index 59bb3752..030227dc 100644
--- a/src/cartesian-chart/chart-series-cartesian.tsx
+++ b/src/cartesian-chart/chart-series-cartesian.tsx
@@ -53,7 +53,18 @@ export const transformCartesianSeries = (
color: s.color ?? Styles.thresholdSeries.color,
dashStyle: s.dashStyle ?? Styles.thresholdSeries.dashStyle,
};
- return { type: "line", id: s.id, name: s.name, data, custom, enableMouseTracking, ...style, ...shared };
+ const yAxis = s.type === "y-threshold" ? s.yAxis : undefined;
+ return {
+ type: "line",
+ id: s.id,
+ name: s.name,
+ yAxis,
+ data,
+ custom,
+ enableMouseTracking,
+ ...style,
+ ...shared,
+ };
}
if (s.type === "errorbar") {
const color = s.color ?? colorChartsErrorBarMarker;
diff --git a/src/cartesian-chart/index.tsx b/src/cartesian-chart/index.tsx
index e7188817..fa1994ac 100644
--- a/src/cartesian-chart/index.tsx
+++ b/src/cartesian-chart/index.tsx
@@ -108,24 +108,33 @@ function transformLineLikeSeries<
| CartesianChartProps.SplineSeriesOptions,
>(s: S): null | S {
const data = transformPointData(s.data);
- return { type: s.type, id: s.id, name: s.name, color: s.color, dashStyle: s.dashStyle, data } as S;
+ return { type: s.type, id: s.id, name: s.name, color: s.color, dashStyle: s.dashStyle, yAxis: s.yAxis, data } as S;
}
function transformColumnSeries(s: S): null | S {
const data = transformPointData(s.data);
- return { type: s.type, id: s.id, name: s.name, color: s.color, data } as S;
+ return { type: s.type, id: s.id, name: s.name, color: s.color, yAxis: s.yAxis, data } as S;
}
function transformScatterSeries(s: S): null | S {
const data = transformPointData(s.data);
const marker = s.marker ?? {};
- return { type: s.type, id: s.id, name: s.name, color: s.color, data, marker } as S;
+ return { type: s.type, id: s.id, name: s.name, color: s.color, yAxis: s.yAxis, data, marker } as S;
}
function transformThresholdSeries<
S extends CartesianChartProps.XThresholdSeriesOptions | CartesianChartProps.YThresholdSeriesOptions,
>(s: S): null | S {
- return { type: s.type, id: s.id, name: s.name, color: s.color, value: s.value, dashStyle: s.dashStyle } as S;
+ const yAxis = s.type === "y-threshold" ? s.yAxis : undefined;
+ return {
+ type: s.type,
+ id: s.id,
+ name: s.name,
+ color: s.color,
+ yAxis,
+ value: s.value,
+ dashStyle: s.dashStyle,
+ } as S;
}
function transformErrorBarSeries(
@@ -157,7 +166,15 @@ function transformErrorBarSeries(
return null;
}
const data = transformRangeData(series.data);
- return { type: series.type, id: series.id, name: series.name, color: series.color, linkedTo: series.linkedTo, data };
+ return {
+ type: series.type,
+ id: series.id,
+ name: series.name,
+ color: series.color,
+ yAxis: series.yAxis,
+ linkedTo: series.linkedTo,
+ data,
+ };
}
function transformPointData(data: readonly PointDataItemType[]): readonly PointDataItemType[] {
@@ -172,14 +189,24 @@ function transformXAxisOptions(axis?: CartesianChartProps.XAxisOptions): Cartesi
return transformAxisOptions(axis);
}
-function transformYAxisOptions(axis?: CartesianChartProps.YAxisOptions): CartesianChartProps.YAxisOptions {
- return { ...transformAxisOptions(axis), reversedStacks: axis?.reversedStacks };
+function transformYAxisOptions(
+ axis?: CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisWithId, CartesianChartProps.YAxisWithId],
+): CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisWithId, CartesianChartProps.YAxisWithId] {
+ if (Array.isArray(axis)) {
+ return [transformSingleYAxisOptions(axis[0]), transformSingleYAxisOptions(axis[1])];
+ }
+ return transformSingleYAxisOptions(axis);
+}
+
+function transformSingleYAxisOptions(axis?: T): T {
+ return { ...transformAxisOptions(axis), reversedStacks: axis?.reversedStacks } as T;
}
function transformAxisOptions(
axis?: O,
): O {
return {
+ id: axis?.id,
type: axis?.type,
title: axis?.title,
min: axis?.min,
diff --git a/src/cartesian-chart/interfaces.ts b/src/cartesian-chart/interfaces.ts
index 79b32313..5e53ed5f 100644
--- a/src/cartesian-chart/interfaces.ts
+++ b/src/cartesian-chart/interfaces.ts
@@ -74,9 +74,13 @@ export interface CartesianChartProps
/**
* Defines options of the chart's y axis.
- * This property corresponds to [xAxis](https://api.highcharts.com/highcharts/yAxis), and extends it
+ * This property corresponds to [yAxis](https://api.highcharts.com/highcharts/yAxis), and extends it
* with a custom value formatter.
*
+ * Use a single object for a single y axis, or a tuple of two objects for a dual-axis chart.
+ * When using a tuple, both axes must have an `id`. The second axis is automatically rendered on the opposite (right) side.
+ * Series reference their axis by setting `yAxis` to the axis `id`.
+ *
* Supported options:
* * `title` (optional, string) - Axis title.
* * `type` (optional, "linear" | "datetime" | "category" | "logarithmic") - Axis type.
@@ -92,7 +96,7 @@ export interface CartesianChartProps
* * `valueFormatter` (optional, function) - Takes axis tick as input and returns a formatted string. This formatter also
* applies to the tooltip points values.
*/
- yAxis?: CartesianChartProps.YAxisOptions;
+ yAxis?: CartesianChartProps.YAxisOptions | [CartesianChartProps.YAxisWithId, CartesianChartProps.YAxisWithId];
/**
* Specifies which series to show using their IDs. By default, all series are visible and managed by the component.
@@ -141,6 +145,7 @@ export namespace CartesianChartProps {
export type YThresholdSeriesOptions = CoreTypes.YThresholdSeriesOptions;
interface AxisOptions {
+ id?: string;
title?: string;
type?: "linear" | "datetime" | "category" | "logarithmic";
min?: number;
@@ -156,6 +161,10 @@ export namespace CartesianChartProps {
reversedStacks?: boolean;
}
+ export interface YAxisWithId extends YAxisOptions {
+ id: string;
+ }
+
export interface TooltipOptions {
enabled?: boolean;
placement?: "middle" | "outside";
diff --git a/src/core/__tests__/chart-core-legend.test.tsx b/src/core/__tests__/chart-core-legend.test.tsx
index 194d66a6..b6511e0c 100644
--- a/src/core/__tests__/chart-core-legend.test.tsx
+++ b/src/core/__tests__/chart-core-legend.test.tsx
@@ -600,17 +600,17 @@ describe("CoreChart: legend", () => {
describe("CoreChart: secondary legend", () => {
test("does not render when no secondary axis series exist", () => {
renderChart({ highcharts, options: { series: primarySeries, yAxis: yAxes } });
- expect(createChartWrapper().findSecondaryLegend()).toBe(null);
+ expect(createChartWrapper().findLegend({ axisId: "secondary" })).toBe(null);
});
test("renders when only secondary axis series exist", () => {
renderChart({ highcharts, options: { series: secondarySeries, yAxis: yAxes } });
- expect(createChartWrapper().findSecondaryLegend()).not.toBe(null);
+ expect(createChartWrapper().findLegend({ axisId: "secondary" })).not.toBe(null);
});
test("renders when both primary and secondary axis series exist", () => {
renderChart({ highcharts, options: { series: [...primarySeries, ...secondarySeries], yAxis: yAxes } });
- expect(createChartWrapper().findSecondaryLegend()).not.toBe(null);
+ expect(createChartWrapper().findLegend({ axisId: "secondary" })).not.toBe(null);
});
test("renders no secondary legend when legend.enabled=false", () => {
@@ -619,7 +619,7 @@ describe("CoreChart: secondary legend", () => {
legend: { enabled: false },
options: { series: [...primarySeries, ...secondarySeries], yAxis: yAxes },
});
- expect(createChartWrapper().findSecondaryLegend()).toBe(null);
+ expect(createChartWrapper().findLegend({ axisId: "secondary" })).toBe(null);
});
test("renders expected secondary legend items", () => {
@@ -629,7 +629,7 @@ describe("CoreChart: secondary legend", () => {
options: { series: [...primarySeries, ...secondarySeries], yAxis: yAxes },
});
- const items = createChartWrapper().findSecondaryLegend()!.findItems();
+ const items = createChartWrapper().findLegend({ axisId: "secondary" })!.findItems();
expect(items.map((w) => w.getElement().textContent)).toEqual(["Secondary 1", "Secondary 2"]);
});
@@ -640,7 +640,9 @@ describe("CoreChart: secondary legend", () => {
options: { series: [...primarySeries, ...secondarySeries], yAxis: yAxes },
});
- expect(createChartWrapper().findSecondaryLegend()!.findTitle()!.getElement().textContent).toBe("Secondary Legend");
+ expect(createChartWrapper().findLegend({ axisId: "secondary" })!.findTitle()!.getElement().textContent).toBe(
+ "Secondary Legend",
+ );
});
test("renders secondary legend actions if specified", () => {
@@ -650,7 +652,7 @@ describe("CoreChart: secondary legend", () => {
options: { series: [...primarySeries, ...secondarySeries], yAxis: yAxes },
});
- expect(createChartWrapper().findSecondaryLegend()!.findActions()!.getElement().textContent).toBe(
+ expect(createChartWrapper().findLegend({ axisId: "secondary" })!.findActions()!.getElement().textContent).toBe(
"Secondary Actions",
);
});
diff --git a/src/core/__tests__/common.tsx b/src/core/__tests__/common.tsx
index f62236e2..66f1fc5c 100644
--- a/src/core/__tests__/common.tsx
+++ b/src/core/__tests__/common.tsx
@@ -100,16 +100,16 @@ export function leaveLegendItem(index: number, wrapper: BaseChartWrapper = creat
}
export function selectSecondaryLegendItem(index: number, wrapper: ExtendedTestWrapper = createChartWrapper()) {
- act(() => wrapper.findSecondaryLegend()!.findItems()[index].click());
+ act(() => wrapper.findLegend({ axisId: "secondary" })!.findItems()[index].click());
}
export function toggleSecondaryLegendItem(index: number, wrapper: ExtendedTestWrapper = createChartWrapper()) {
const modifier = Math.random() > 0.5 ? { metaKey: true } : { ctrlKey: true };
- act(() => wrapper.findSecondaryLegend()!.findItems()[index].click(modifier));
+ act(() => wrapper.findLegend({ axisId: "secondary" })!.findItems()[index].click(modifier));
}
export function hoverSecondaryLegendItem(index: number, wrapper: ExtendedTestWrapper = createChartWrapper()) {
act(() => {
- fireEvent.mouseOver(wrapper.findSecondaryLegend()!.findItems()[index].getElement());
+ fireEvent.mouseOver(wrapper.findLegend({ axisId: "secondary" })!.findItems()[index].getElement());
});
}
diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx
index 0e4f0547..254aadd5 100644
--- a/src/core/chart-core.tsx
+++ b/src/core/chart-core.tsx
@@ -235,7 +235,7 @@ export function InternalCoreChart({
// Depending on the chart.inverted the y-axis can be rendered as vertical, and needs to respect page direction.
reversed: inverted && isRtl ? !yAxisOptions.reversed : yAxisOptions.reversed,
opposite: !inverted && isRtl ? !yAxisOptions.opposite : yAxisOptions.opposite,
- className: yAxisClassName(inverted, yAxisOptions.className),
+ className: yAxisClassName(inverted, yAxisOptions.className, yAxisOptions.id),
title: axisTitle(yAxisOptions.title ?? {}, inverted || verticalAxisTitlePlacement === "side"),
labels: axisLabels(yAxisOptions.labels ?? {}),
plotLines: yAxisPlotLines(yAxisOptions.plotLines, emphasizeBaseline),
@@ -408,11 +408,12 @@ function xAxisClassName(inverted: boolean, customClassName?: string) {
);
}
-function yAxisClassName(inverted: boolean, customClassName?: string) {
+function yAxisClassName(inverted: boolean, customClassName?: string, axisId?: string) {
return clsx(
testClasses["axis-y"],
inverted ? testClasses["axis-horizontal"] : testClasses["axis-vertical"],
customClassName,
+ axisId && `awsui-axis-${axisId}`,
);
}
diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts
index 5c1a5b28..928aef6a 100644
--- a/src/core/interfaces.ts
+++ b/src/core/interfaces.ts
@@ -244,7 +244,9 @@ export interface ErrorBarSeriesOptions extends Omit {
type: "x-threshold";
value: number;
}
@@ -290,7 +292,9 @@ interface BaseSeriesOptions {
color?: string;
}
-type BaseCartesianSeriesOptions = BaseSeriesOptions;
+interface BaseCartesianSeriesOptions extends BaseSeriesOptions {
+ yAxis?: string;
+}
interface BaseCartesianLineLikeOptions extends BaseCartesianSeriesOptions {
dashStyle?: Highcharts.DashStyleValue;
diff --git a/src/core/utils.ts b/src/core/utils.ts
index aac480f7..e41b257f 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -204,6 +204,9 @@ export function getVisibleLegendItems(options: Highcharts.Options) {
const valueAxes = (isInverted ? castArray(options.xAxis) : castArray(options.yAxis)) ?? [];
const defaultOpposite = valueAxes.length > 0 ? (valueAxes[0].opposite ?? false) : false;
+ const primaryAxis = valueAxes.find((a) => !(a.opposite ?? false));
+ const secondaryAxis = valueAxes.find((a) => a.opposite === true);
+
const primaryItems: LegendItemOptions[] = [];
const secondaryItems: LegendItemOptions[] = [];
const addLegendItem = (item: LegendItemOptions) => {
@@ -236,7 +239,7 @@ export function getVisibleLegendItems(options: Highcharts.Options) {
}
});
- return { primaryItems, secondaryItems };
+ return { primaryItems, secondaryItems, primaryAxisId: primaryAxis?.id, secondaryAxisId: secondaryAxis?.id };
}
function isSecondaryLegendItem(
@@ -262,13 +265,14 @@ export function getLegendsProps(
// While Highcharts supports more than two axes, this
// implementation supports at most two, in which case one
// of them must be set as opposite (secondary).
- const { primaryItems, secondaryItems } = getVisibleLegendItems(options);
+ const { primaryItems, secondaryItems, primaryAxisId, secondaryAxisId } = getVisibleLegendItems(options);
return {
primary:
primaryItems.length === 0
? undefined
: ({
isSecondary: false,
+ axisId: primaryAxisId,
title: legendOptions?.title,
actions: legendOptions?.actions,
alignment: legendOptions?.position === "side" ? "vertical" : "horizontal",
@@ -279,6 +283,7 @@ export function getLegendsProps(
? undefined
: ({
isSecondary: true,
+ axisId: secondaryAxisId,
title: legendOptions?.secondaryLegendTitle,
actions: legendOptions?.secondaryLegendActions,
alignment: legendOptions?.position === "side" ? "vertical" : "horizontal",
diff --git a/src/internal/components/chart-legend/index.tsx b/src/internal/components/chart-legend/index.tsx
index 9ae50040..67e89dd9 100644
--- a/src/internal/components/chart-legend/index.tsx
+++ b/src/internal/components/chart-legend/index.tsx
@@ -35,6 +35,7 @@ export interface ChartLegendProps {
items: readonly LegendItem[];
legendTitle?: string;
ariaLabel?: string;
+ axisId?: string;
className?: string;
actions?: React.ReactNode;
someHighlighted: boolean;
@@ -51,6 +52,7 @@ export const ChartLegend = ({
items,
legendTitle,
ariaLabel,
+ axisId,
actions,
alignment,
className,
@@ -223,6 +225,7 @@ export const ChartLegend = ({
role="toolbar"
aria-label={legendTitle || ariaLabel}
className={clsx(testClasses.root, styles.root, className)}
+ data-axisid={axisId}
onMouseEnter={() => (isMouseInContainer.current = true)}
onMouseLeave={() => (isMouseInContainer.current = false)}
>
diff --git a/src/test-utils/dom/internal/base.ts b/src/test-utils/dom/internal/base.ts
index 46dd8523..b962e42b 100644
--- a/src/test-utils/dom/internal/base.ts
+++ b/src/test-utils/dom/internal/base.ts
@@ -20,9 +20,13 @@ export default class BaseChartWrapper extends ComponentWrapper {
/**
* Finds chart's legend when defined.
+ * @param axisId Optional axis ID to target a specific legend (e.g. "primary", "secondary").
*/
- public findLegend(): null | BaseChartLegendWrapper {
- return this.findComponent(`.${BaseChartLegendWrapper.rootSelector}`, BaseChartLegendWrapper);
+ public findLegend({ axisId }: { axisId?: string } = {}): null | BaseChartLegendWrapper {
+ const selector = axisId
+ ? `.${BaseChartLegendWrapper.rootSelector}[data-axisid="${axisId}"]`
+ : `.${BaseChartLegendWrapper.rootSelector}`;
+ return this.findComponent(selector, BaseChartLegendWrapper);
}
/**
@@ -66,8 +70,12 @@ export default class BaseChartWrapper extends ComponentWrapper {
/**
* Finds visible title of the y axis.
+ * @param axisId Optional axis ID to target a specific y axis title (e.g. "secondary").
*/
- public findYAxisTitle(): null | ElementWrapper {
+ public findYAxisTitle({ axisId }: { axisId?: string } = {}): null | ElementWrapper {
+ if (axisId) {
+ return this.find(`.highcharts-axis.awsui-axis-${axisId} > .highcharts-axis-title`);
+ }
return (
this.findByClassName(testClasses["axis-y-title"]) ??
this.find(`.highcharts-axis.${testClasses["axis-y"]} > .highcharts-axis-title`)
diff --git a/src/test-utils/dom/internal/core.ts b/src/test-utils/dom/internal/core.ts
index 9861408e..c14f4f6b 100644
--- a/src/test-utils/dom/internal/core.ts
+++ b/src/test-utils/dom/internal/core.ts
@@ -21,18 +21,11 @@ export default class CoreChartWrapper extends BaseChartWrapper {
return this.findByClassName(testClasses["chart-navigator"]);
}
- public findLegend(): null | CoreChartLegendWrapper {
- return this.findComponent(
- `.${CoreChartLegendWrapper.rootSelector}.${testClasses["legend-primary"]}`,
- CoreChartLegendWrapper,
- );
- }
-
- public findSecondaryLegend(): null | CoreChartLegendWrapper {
- return this.findComponent(
- `.${CoreChartLegendWrapper.rootSelector}.${testClasses["legend-secondary"]}`,
- CoreChartLegendWrapper,
- );
+ public findLegend({ axisId }: { axisId?: string } = {}): null | CoreChartLegendWrapper {
+ const selector = axisId
+ ? `.${CoreChartLegendWrapper.rootSelector}[data-axisid="${axisId}"]`
+ : `.${CoreChartLegendWrapper.rootSelector}.${testClasses["legend-primary"]}`;
+ return this.findComponent(selector, CoreChartLegendWrapper);
}
public findVerticalAxisTitle(): null | ElementWrapper {