Skip to content

Commit 7a3e853

Browse files
committed
feat: set the legend horizontal alignment
1 parent 067f70d commit 7a3e853

File tree

12 files changed

+346
-6
lines changed

12 files changed

+346
-6
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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 ColumnLayout from "@cloudscape-design/components/column-layout";
7+
import SpaceBetween from "@cloudscape-design/components/space-between";
8+
9+
import { CoreChartProps } from "../../lib/components/core/interfaces";
10+
import CoreChart from "../../lib/components/internal-do-not-use/core-chart";
11+
import { PageSettingsForm, useChartSettings } from "../common/page-settings";
12+
import { Page } from "../common/templates";
13+
import pseudoRandom from "../utils/pseudo-random";
14+
15+
function randomInt(min: number, max: number) {
16+
return min + Math.floor(pseudoRandom() * (max - min));
17+
}
18+
19+
const lineChartData = [
20+
{ x: 1600984800000, y: 58020 },
21+
{ x: 1600985700000, y: 102402 },
22+
{ x: 1600986600000, y: 104920 },
23+
{ x: 1600987500000, y: 94031 },
24+
{ x: 1600988400000, y: 125021 },
25+
{ x: 1600989300000, y: 159219 },
26+
{ x: 1600990200000, y: 193082 },
27+
{ x: 1600991100000, y: 162592 },
28+
{ x: 1600992000000, y: 274021 },
29+
{ x: 1600992900000, y: 264286 },
30+
{ x: 1600993800000, y: 289210 },
31+
{ x: 1600994700000, y: 256362 },
32+
{ x: 1600995600000, y: 257306 },
33+
{ x: 1600996500000, y: 186776 },
34+
{ x: 1600997400000, y: 294020 },
35+
{ x: 1600998300000, y: 385975 },
36+
{ x: 1600999200000, y: 486039 },
37+
{ x: 1601000100000, y: 490447 },
38+
{ x: 1601001000000, y: 361845 },
39+
{ x: 1601001900000, y: 339058 },
40+
{ x: 1601002800000, y: 298028 },
41+
{ x: 1601003400000, y: 255555 },
42+
{ x: 1601003700000, y: 231902 },
43+
{ x: 1601004600000, y: 224558 },
44+
{ x: 1601005500000, y: 253901 },
45+
{ x: 1601006400000, y: 102839 },
46+
{ x: 1601007300000, y: 234943 },
47+
{ x: 1601008200000, y: 204405 },
48+
{ x: 1601009100000, y: 190391 },
49+
{ x: 1601010000000, y: 183570 },
50+
{ x: 1601010900000, y: 162592 },
51+
{ x: 1601011800000, y: 148910 },
52+
];
53+
54+
export default function () {
55+
const { chartProps } = useChartSettings({ solidgauge: true });
56+
57+
const charts: ((horizontalAlignment: CoreChartProps.LegendOptionsHorizontalAlignment) => CoreChartProps)[] = [
58+
(horizontalAlignment) => ({
59+
...omit(chartProps.cartesian, "ref"),
60+
legend: {
61+
horizontalAlignment,
62+
title: chartProps.cartesian.legend?.title,
63+
actions: chartProps.cartesian.legend?.actions,
64+
},
65+
options: {
66+
chart: {
67+
type: "pie",
68+
},
69+
title: {
70+
text: `${horizontalAlignment} aligned Pie`,
71+
},
72+
yAxis: {
73+
min: 0,
74+
max: 100,
75+
title: {
76+
text: "Usage",
77+
},
78+
},
79+
series: [
80+
{
81+
name: "Storage Distribution",
82+
type: "pie",
83+
data: [
84+
{ y: randomInt(30, 40), name: "Documents" },
85+
{ y: randomInt(20, 30), name: "Images" },
86+
{ y: randomInt(15, 25), name: "Videos" },
87+
{ y: randomInt(10, 15), name: "Other" },
88+
],
89+
},
90+
],
91+
},
92+
}),
93+
(horizontalAlignment) => ({
94+
...omit(chartProps.cartesian, "ref"),
95+
legend: {
96+
horizontalAlignment,
97+
title: chartProps.cartesian.legend?.title,
98+
actions: chartProps.cartesian.legend?.actions,
99+
},
100+
options: {
101+
chart: {
102+
type: "solidgauge",
103+
},
104+
title: {
105+
text: `${horizontalAlignment} aligned Gauge`,
106+
},
107+
yAxis: {
108+
min: 0,
109+
max: 100,
110+
},
111+
series: [
112+
{
113+
type: "solidgauge",
114+
name: "Download speed",
115+
data: [randomInt(50, 100)],
116+
showInLegend: true,
117+
dataLabels: {
118+
format: "{y} MB/s",
119+
},
120+
},
121+
],
122+
},
123+
}),
124+
(horizontalAlignment) => ({
125+
...omit(chartProps.cartesian, "ref"),
126+
legend: {
127+
horizontalAlignment,
128+
title: chartProps.cartesian.legend?.title,
129+
actions: chartProps.cartesian.legend?.actions,
130+
},
131+
options: {
132+
chart: {
133+
type: "line",
134+
},
135+
title: {
136+
text: `${horizontalAlignment} aligned Line`,
137+
},
138+
series: [
139+
{
140+
type: "line",
141+
name: "Download speed",
142+
data: lineChartData,
143+
},
144+
],
145+
},
146+
}),
147+
];
148+
149+
return (
150+
<Page
151+
title="Core Legend horizontal alignment demo"
152+
subtitle="The page demonstrates the horizontal alignments of the core legend."
153+
settings={<PageSettingsForm selectedSettings={["showLegendTitle", "showLegendActions"]} />}
154+
>
155+
<SpaceBetween direction="vertical" size="m">
156+
{charts.map((chart, i) => (
157+
<ColumnLayout key={i} columns={2}>
158+
<CoreChart {...chart("start")}></CoreChart>
159+
<CoreChart {...chart("center")}></CoreChart>
160+
</ColumnLayout>
161+
))}
162+
</SpaceBetween>
163+
</Page>
164+
);
165+
}

pages/03-core/core-line-chart.page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,14 @@ export default function () {
9191
subtitle="The page demonstrates the use of the core chart, including additional legend settings."
9292
settings={
9393
<PageSettingsForm
94-
selectedSettings={["showLegend", "legendPosition", "showLegendTitle", "showLegendActions", "useFallback"]}
94+
selectedSettings={[
95+
"showLegend",
96+
"legendPosition",
97+
"legendHorizontalAlign",
98+
"showLegendTitle",
99+
"showLegendActions",
100+
"useFallback",
101+
]}
95102
/>
96103
}
97104
>

pages/common/page-settings.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Select from "@cloudscape-design/components/select";
1515
import SpaceBetween from "@cloudscape-design/components/space-between";
1616

1717
import { CartesianChartProps, PieChartProps } from "../../lib/components";
18+
import { CoreChartProps } from "../../src/core/interfaces";
1819
import AppContext, { AppContextType } from "../app/app-context";
1920
import { useHighcharts } from "./use-highcharts";
2021

@@ -36,6 +37,7 @@ export interface PageSettings {
3637
showLegendActions: boolean;
3738
legendBottomMaxHeight?: number;
3839
legendPosition: "bottom" | "side";
40+
legendHorizontalAlign: CoreChartProps.LegendOptionsHorizontalAlignment;
3941
showCustomHeader: boolean;
4042
showHeaderFilter: boolean;
4143
showCustomFooter: boolean;
@@ -60,6 +62,7 @@ const DEFAULT_SETTINGS: PageSettings = {
6062
showLegend: true,
6163
showLegendTitle: false,
6264
legendPosition: "bottom",
65+
legendHorizontalAlign: "start",
6366
showLegendActions: false,
6467
showCustomHeader: false,
6568
showHeaderFilter: false,
@@ -149,7 +152,8 @@ export function useChartSettings<SettingsType extends PageSettings = PageSetting
149152
actions: settings.showLegendActions ? <Button variant="icon" iconName="search" /> : undefined,
150153
position: settings.legendPosition,
151154
bottomMaxHeight: settings.legendBottomMaxHeight,
152-
};
155+
horizontalAlignment: settings.legendHorizontalAlign,
156+
} satisfies CoreChartProps.LegendOptions;
153157
return {
154158
settings,
155159
setSettings,
@@ -387,6 +391,32 @@ export function PageSettingsForm({
387391
}
388392
/>
389393
);
394+
case "legendHorizontalAlign":
395+
return (
396+
<SegmentedControl
397+
label="Legend Horizontal Align"
398+
selectedId={
399+
settings.showLegend && settings.legendPosition === "bottom" ? settings.legendHorizontalAlign : null
400+
}
401+
options={[
402+
{
403+
text: "Start",
404+
id: "start",
405+
disabled: !settings.showLegend || settings.legendPosition !== "bottom",
406+
},
407+
{
408+
text: "Center",
409+
id: "center",
410+
disabled: !settings.showLegend || settings.legendPosition !== "bottom",
411+
},
412+
]}
413+
onChange={({ detail }) =>
414+
setSettings({
415+
legendHorizontalAlign: detail.selectedId as CoreChartProps.LegendOptionsHorizontalAlignment,
416+
})
417+
}
418+
/>
419+
);
390420
case "showCustomHeader":
391421
return (
392422
<Checkbox

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,20 @@ Supported Highcharts versions: 12.",
15891589
"optional": true,
15901590
"type": "boolean",
15911591
},
1592+
{
1593+
"inlineType": {
1594+
"name": "CoreChartProps.LegendOptionsHorizontalAlignment",
1595+
"type": "union",
1596+
"valueDescriptions": undefined,
1597+
"values": [
1598+
"start",
1599+
"center",
1600+
],
1601+
},
1602+
"name": "horizontalAlignment",
1603+
"optional": true,
1604+
"type": "string",
1605+
},
15921606
{
15931607
"inlineType": {
15941608
"name": ""side" | "bottom"",

src/core/components/core-legend.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ export function ChartLegend({
1717
i18nStrings,
1818
onItemHighlight,
1919
getLegendTooltipContent,
20+
horizontalAlignment = "start",
2021
}: {
2122
api: ChartAPI;
2223
title?: string;
2324
actions?: React.ReactNode;
2425
position: "bottom" | "side";
26+
horizontalAlignment?: CoreChartProps.LegendOptionsHorizontalAlignment;
2527
i18nStrings?: BaseI18nStrings;
2628
onItemHighlight?: NonCancelableEventHandler<CoreChartProps.LegendItemHighlightDetail>;
2729
getLegendTooltipContent?: CoreChartProps.GetLegendTooltipContent;
@@ -39,6 +41,7 @@ export function ChartLegend({
3941
ariaLabel={ariaLabel}
4042
legendTitle={title}
4143
items={legendItems}
44+
horizontalAlignment={horizontalAlignment}
4245
actions={actions}
4346
position={position}
4447
onItemVisibilityChange={api.onItemVisibilityChange}

src/core/interfaces.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,14 @@ export namespace CoreChartProps {
414414
export interface LegendOptions extends BaseLegendOptions {
415415
bottomMaxHeight?: number;
416416
position?: "bottom" | "side";
417+
/**
418+
* When {@link position} is set to "bottom", horizontalAlignment sets the legend horizontal alignment.
419+
*/
420+
horizontalAlignment?: LegendOptionsHorizontalAlignment;
417421
}
422+
423+
export type LegendOptionsHorizontalAlignment = "start" | "center";
424+
418425
export type LegendItem = InternalComponentTypes.LegendItem;
419426
export type LegendTooltipContent = InternalComponentTypes.LegendTooltipContent;
420427
export type GetLegendTooltipContent = InternalComponentTypes.GetLegendTooltipContent;

src/internal-do-not-use/core-legend/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const CoreLegend = ({
2828
actions={actions}
2929
legendTitle={title}
3030
position={position}
31+
horizontalAlignment="start"
3132
ariaLabel={ariaLabel}
3233
getTooltipContent={(props) => getLegendTooltipContent?.(props) ?? null}
3334
onItemHighlightExit={() => fireNonCancelableEvent(onClearHighlight)}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { ChartLegend, ChartLegendProps } from "@lib/components/internal/components/chart-legend";
5+
import { render } from "@testing-library/react";
6+
import { vi } from "vitest";
7+
8+
import styles from "@lib/components/internal/components/chart-legend/styles.selectors.js";
9+
import testClasses from "@lib/components/internal/components/chart-legend/test-classes/styles.selectors.js";
10+
11+
const defaultProps: ChartLegendProps = {
12+
items: [
13+
{ id: "item1", name: "Item 1", visible: true, highlighted: false, marker: <div></div> },
14+
{ id: "item2", name: "Item 2", visible: true, highlighted: false, marker: <div></div> },
15+
],
16+
position: "bottom",
17+
horizontalAlignment: "start",
18+
onItemHighlightEnter: vi.fn(),
19+
onItemHighlightExit: vi.fn(),
20+
onItemVisibilityChange: vi.fn(),
21+
getTooltipContent: () => null,
22+
};
23+
24+
describe("ChartLegend horizontalAlignment", () => {
25+
describe("when horizontalAlignment is 'start'", () => {
26+
test("applies correct CSS class to legend title", () => {
27+
const { container } = render(
28+
<ChartLegend {...defaultProps} legendTitle="Test Legend" horizontalAlignment="start" />,
29+
);
30+
31+
const titleElement = container.querySelector(`.${testClasses.title}`);
32+
expect(titleElement).toHaveClass(styles["legend-title-start"]);
33+
expect(titleElement).not.toHaveClass(styles["legend-title-center"]);
34+
});
35+
36+
test("applies correct CSS class to list element when position is bottom", () => {
37+
const { container } = render(<ChartLegend {...defaultProps} position="bottom" horizontalAlignment="start" />);
38+
39+
const listElement = container.querySelector(`.${styles.list}`);
40+
expect(listElement).toHaveClass(styles["list-bottom-start"]);
41+
expect(listElement).not.toHaveClass(styles["list-bottom-center"]);
42+
});
43+
44+
test("does not apply list-bottom alignment classes when position is side", () => {
45+
const { container } = render(<ChartLegend {...defaultProps} position="side" horizontalAlignment="start" />);
46+
47+
const listElement = container.querySelector(`.${styles.list}`);
48+
expect(listElement).not.toHaveClass(styles["list-bottom-start"]);
49+
expect(listElement).not.toHaveClass(styles["list-bottom-center"]);
50+
});
51+
});
52+
53+
describe("when horizontalAlignment is 'center'", () => {
54+
test("applies correct CSS class to legend title", () => {
55+
const { container } = render(
56+
<ChartLegend {...defaultProps} legendTitle="Test Legend" horizontalAlignment="center" />,
57+
);
58+
59+
const titleElement = container.querySelector(`.${testClasses.title}`);
60+
expect(titleElement).toHaveClass(styles["legend-title-center"]);
61+
expect(titleElement).not.toHaveClass(styles["legend-title-start"]);
62+
});
63+
64+
test("applies correct CSS class to list element when position is bottom", () => {
65+
const { container } = render(<ChartLegend {...defaultProps} position="bottom" horizontalAlignment="center" />);
66+
67+
const listElement = container.querySelector(`.${styles.list}`);
68+
expect(listElement).toHaveClass(styles["list-bottom-center"]);
69+
expect(listElement).not.toHaveClass(styles["list-bottom-start"]);
70+
});
71+
72+
test("does not apply list-bottom alignment classes when position is side", () => {
73+
const { container } = render(<ChartLegend {...defaultProps} position="side" horizontalAlignment="center" />);
74+
75+
const listElement = container.querySelector(`.${styles.list}`);
76+
expect(listElement).not.toHaveClass(styles["list-bottom-start"]);
77+
expect(listElement).not.toHaveClass(styles["list-bottom-center"]);
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)