Skip to content

Commit 80443d9

Browse files
authored
feat: dual axis support in legend (#122)
* feat: dual axis support in legend * fix: update snapshots * chore: add tests * fix: add test for secondary axis-only series scenario * refactor: use real Highcharts instances in chart utils tests * fix: add gauge chart support to legend utils test * refactor: improve aria label computation logic * refactor(chart-core): extract legend configuration into utility function * refactor(core): extract CoreI18nStrings interface from BaseI18nStrings * docs: expand shouldShowSecondaryLegend function documentation * chore: update test snapshots * feat: add support for inverted charts in secondary legend * feat(core-legend): add i18n support for legend aria label * refactor(chart-legend): extract and improve type definitions * fix(legend): remove auto i18n for secondary legend ARIA label * fix: always render secondary series through secondary legend * fix: distinguish primary and secondary legends with unique selectors * docs: update secondary legend comments to reflect current constraints * refactor(utils): hasVisibleLegendItems to getVisibleLegendSeries * fix: secondary axis detection in chart legend items * chore: improve chart legend test coverage and descriptions * fix(chart): include secondaryLegend in noData display condition * refactor: simplify and document isSecondaryLegendItem logic * refactor: address comments * fix: return defaultOpposite in isSecondaryLegendItem * fix: typos in code comments * refactor: isSecondary axis tests to use parameterized testing * docs: fix typo in core-legend component comment * fix: remove unused legendHorizontalAlign from dual-axis chart settings * fix: account for right-to-left configurations in getLegendProps * fix: remove array shuffling for more consistency when running screenshot testing * refactor(chart-core): axis configuration logic * doc(chart-core): explain why passing rtl-adjusted axes is needed
1 parent 805cc3c commit 80443d9

File tree

21 files changed

+1045
-256
lines changed

21 files changed

+1045
-256
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { omit } from "lodash";
5+
6+
import Link from "@cloudscape-design/components/link";
7+
8+
import CoreChart from "../../lib/components/internal-do-not-use/core-chart";
9+
import { dateFormatter } from "../common/formatters";
10+
import { PageSettingsForm, useChartSettings } from "../common/page-settings";
11+
import { Page } from "../common/templates";
12+
import pseudoRandom from "../utils/pseudo-random";
13+
14+
function randomInt(min: number, max: number) {
15+
return min + Math.floor(pseudoRandom() * (max - min));
16+
}
17+
18+
const colors = [
19+
"#F15C80",
20+
"#2B908F",
21+
"#F45B5B",
22+
"#91E8E1",
23+
"#8085E9",
24+
"#E4D354",
25+
"#8D4654",
26+
"#7798BF",
27+
"#AAEEEE",
28+
"#FF9655",
29+
];
30+
31+
const baseline = [
32+
{ x: 1600984800000, y: 58020 },
33+
{ x: 1600985700000, y: 102402 },
34+
{ x: 1600986600000, y: 104920 },
35+
{ x: 1600987500000, y: 94031 },
36+
{ x: 1600988400000, y: 125021 },
37+
{ x: 1600989300000, y: 159219 },
38+
{ x: 1600990200000, y: 193082 },
39+
{ x: 1600991100000, y: 162592 },
40+
{ x: 1600992000000, y: 274021 },
41+
{ x: 1600992900000, y: 264286 },
42+
{ x: 1600993800000, y: 289210 },
43+
{ x: 1600994700000, y: 256362 },
44+
{ x: 1600995600000, y: 257306 },
45+
{ x: 1600996500000, y: 186776 },
46+
{ x: 1600997400000, y: 294020 },
47+
{ x: 1600998300000, y: 385975 },
48+
{ x: 1600999200000, y: 486039 },
49+
{ x: 1601000100000, y: 490447 },
50+
{ x: 1601001000000, y: 361845 },
51+
{ x: 1601001900000, y: 339058 },
52+
{ x: 1601002800000, y: 298028 },
53+
{ x: 1601003400000, y: 255555 },
54+
{ x: 1601003700000, y: 231902 },
55+
{ x: 1601004600000, y: 224558 },
56+
{ x: 1601005500000, y: 253901 },
57+
{ x: 1601006400000, y: 102839 },
58+
{ x: 1601007300000, y: 234943 },
59+
{ x: 1601008200000, y: 204405 },
60+
{ x: 1601009100000, y: 190391 },
61+
{ x: 1601010000000, y: 183570 },
62+
{ x: 1601010900000, y: 162592 },
63+
{ x: 1601011800000, y: 148910 },
64+
];
65+
66+
const generatePrimaryAxisData = (letter: string, index: number) => {
67+
return baseline.map(({ x, y }) => ({
68+
name: `Events ${letter}`,
69+
x,
70+
y: y === null ? null : y + randomInt(-100000 * ((index % 3) + 1), 100000 * ((index % 3) + 1)),
71+
}));
72+
};
73+
74+
const generateSecondaryAxisData = (letter: string, index: number) => {
75+
return baseline.map(({ x, y }) => ({
76+
name: `Percentage ${letter}`,
77+
x,
78+
y: y === null ? null : (y / 10000) * randomInt(3 + (index % 5), 10 + (index % 10)),
79+
}));
80+
};
81+
82+
const primarySeriesData: Record<string, ReturnType<typeof generatePrimaryAxisData>> = {};
83+
for (let i = 0; i < 10; i++) {
84+
const letter = String.fromCharCode(65 + i);
85+
primarySeriesData[`data${letter}`] = generatePrimaryAxisData(letter, i);
86+
}
87+
88+
const secondarySeriesData: Record<string, ReturnType<typeof generatePrimaryAxisData>> = {};
89+
for (let i = 0; i < 10; i++) {
90+
const letter = String.fromCharCode(65 + i);
91+
secondarySeriesData[`data${letter}`] = generateSecondaryAxisData(letter, i);
92+
}
93+
94+
const series: Highcharts.SeriesOptionsType[] = [
95+
...Object.values(primarySeriesData),
96+
...Object.values(secondarySeriesData),
97+
].map((data, index) => {
98+
const name = data[0].name;
99+
const isPercentage = name.startsWith("Percentage");
100+
return {
101+
name,
102+
type: "line",
103+
data: data,
104+
yAxis: isPercentage ? 1 : 0,
105+
color: colors[index % colors.length],
106+
dashStyle: isPercentage ? "Dash" : "Solid",
107+
};
108+
});
109+
110+
export default function () {
111+
const { chartProps } = useChartSettings();
112+
return (
113+
<Page
114+
title="Core dual-axis chart demo"
115+
subtitle="This page demonstrates the use of the core chart with two Y axes for displaying data with different scales."
116+
settings={
117+
<PageSettingsForm
118+
selectedSettings={[
119+
"showLegend",
120+
"legendPosition",
121+
"legendBottomMaxHeight",
122+
"showLegendTitle",
123+
"showLegendActions",
124+
]}
125+
/>
126+
}
127+
>
128+
<CoreChart
129+
{...omit(chartProps.cartesian, "ref")}
130+
chartHeight={400}
131+
ariaLabel="Dual axis line chart"
132+
tooltip={{ placement: "outside" }}
133+
options={{
134+
series: series,
135+
xAxis: [
136+
{
137+
type: "datetime",
138+
title: { text: "Time (UTC)" },
139+
valueFormatter: dateFormatter,
140+
},
141+
],
142+
yAxis: [
143+
{
144+
title: { text: "Events" },
145+
},
146+
{
147+
opposite: true,
148+
title: { text: "Percentage (%)" },
149+
},
150+
],
151+
}}
152+
getLegendTooltipContent={({ legendItem }) => ({
153+
header: (
154+
<div style={{ display: "flex" }}>
155+
{legendItem.marker}
156+
{legendItem.name}
157+
</div>
158+
),
159+
body: (
160+
<table>
161+
<tbody style={{ textAlign: "left" }}>
162+
<tr>
163+
<th scope="row">Period</th>
164+
<td>15 min</td>
165+
</tr>
166+
<tr>
167+
<th scope="row">Statistic</th>
168+
<td>Average</td>
169+
</tr>
170+
<tr>
171+
<th scope="row">Unit</th>
172+
<td>Count</td>
173+
</tr>
174+
</tbody>
175+
</table>
176+
),
177+
footer: (
178+
<Link external={true} href="https://example.com/" variant="primary">
179+
Learn more
180+
</Link>
181+
),
182+
})}
183+
/>
184+
</Page>
185+
);
186+
}

pages/common/page-settings.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ export function useChartSettings<SettingsType extends PageSettings = PageSetting
150150
const legend = {
151151
enabled: settings.showLegend,
152152
title: settings.showLegendTitle ? "Legend title" : undefined,
153+
secondaryLegendTitle: settings.showLegendTitle ? "Secondary Legend title" : undefined,
153154
actions: settings.showLegendActions ? <Button variant="icon" iconName="search" /> : undefined,
155+
secondaryLegendActions: settings.showLegendActions ? <Button variant="icon" iconName="calendar" /> : undefined,
154156
position: settings.legendPosition,
155157
bottomMaxHeight: settings.legendBottomMaxHeight,
156158
horizontalAlignment: settings.legendHorizontalAlign,

src/__tests__/__snapshots__/documenter.test.ts.snap

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,11 @@ exports[`internal core API matches snapshot > internal-core-chart 1`] = `
12021202
"optional": false,
12031203
"type": "string",
12041204
},
1205+
{
1206+
"name": "isSecondary",
1207+
"optional": true,
1208+
"type": "boolean",
1209+
},
12051210
{
12061211
"name": "marker",
12071212
"optional": true,
@@ -1538,18 +1543,19 @@ Supported Highcharts versions: 12.",
15381543
"description": "An object that contains all of the localized strings required by the component.",
15391544
"i18nTag": true,
15401545
"inlineType": {
1541-
"name": "CartesianI18nStrings & PieI18nStrings",
1546+
"name": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings",
15421547
"type": "union",
15431548
"valueDescriptions": undefined,
15441549
"values": [
15451550
"CartesianI18nStrings",
15461551
"PieI18nStrings",
1552+
"CoreI18nStrings",
15471553
],
15481554
},
15491555
"name": "i18nStrings",
15501556
"optional": true,
15511557
"systemTags": undefined,
1552-
"type": "CartesianI18nStrings & PieI18nStrings",
1558+
"type": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings",
15531559
"visualRefreshTag": undefined,
15541560
},
15551561
{
@@ -1617,6 +1623,16 @@ Supported Highcharts versions: 12.",
16171623
"optional": true,
16181624
"type": "string",
16191625
},
1626+
{
1627+
"name": "secondaryLegendActions",
1628+
"optional": true,
1629+
"type": "React.ReactNode",
1630+
},
1631+
{
1632+
"name": "secondaryLegendTitle",
1633+
"optional": true,
1634+
"type": "string",
1635+
},
16201636
{
16211637
"name": "title",
16221638
"optional": true,

0 commit comments

Comments
 (0)