Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions pages/01-cartesian-chart/dual-axis-chart.page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Page
title="Cartesian dual-axis chart"
subtitle="This page demonstrates the CartesianChart component with two Y axes for displaying data with different scales."
>
<CartesianChart
{...chartProps.cartesian}
{...dualAxisProps}
series={[
{ type: "line", name: "Events", yAxis: "events", data: eventsData },
{ type: "line", name: "Percentage", yAxis: "percentage", dashStyle: "Dash", data: percentageData },
]}
/>
</Page>
);
}
79 changes: 12 additions & 67 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<string>",
},
{
"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": [
Expand Down Expand Up @@ -1764,7 +1709,7 @@ not overridden, but merged with Cloudscape event handlers so that both are getti
"type": "union",
"valueDescriptions": undefined,
"values": [
"Omit<Highcharts.Options, "xAxis" | "yAxis">",
"Omit<Highcharts.Options, "yAxis" | "xAxis">",
"{ xAxis?: CoreChartProps.XAxisOptions | Array<CoreChartProps.XAxisOptions> | undefined; yAxis?: CoreChartProps.YAxisOptions | Array<CoreChartProps.YAxisOptions> | undefined; }",
],
},
Expand Down
94 changes: 94 additions & 0 deletions src/cartesian-chart/__tests__/cartesian-chart-series.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
3 changes: 2 additions & 1 deletion src/cartesian-chart/chart-cartesian-internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
13 changes: 12 additions & 1 deletion src/cartesian-chart/chart-series-cartesian.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Copy Markdown
Member

@pan-kot pan-kot Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }

type: "line",
id: s.id,
name: s.name,
yAxis,
data,
custom,
enableMouseTracking,
...style,
...shared,
};
}
if (s.type === "errorbar") {
const color = s.color ?? colorChartsErrorBarMarker;
Expand Down
Loading
Loading