Skip to content

Commit 14e88d2

Browse files
committed
draft: standalone legend component
1 parent 80bd3bc commit 14e88d2

File tree

3 files changed

+236
-1
lines changed

3 files changed

+236
-1
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { StandaloneLegend } from "../../lib/components/internal-do-not-use/standalone-legend";
5+
import { PageSettingsForm } from "../common/page-settings";
6+
import { Page } from "../common/templates";
7+
8+
const series: Highcharts.SeriesOptionsType[] = [
9+
{
10+
name: "A",
11+
type: "line",
12+
data: [],
13+
},
14+
{
15+
name: "B",
16+
type: "line",
17+
data: [],
18+
},
19+
{
20+
name: "C",
21+
type: "line",
22+
data: [],
23+
},
24+
{
25+
name: "X",
26+
type: "scatter",
27+
data: [{ x: 1601012700000, y: 500000 }],
28+
marker: { symbol: "square" },
29+
showInLegend: false,
30+
},
31+
];
32+
33+
export default function () {
34+
return (
35+
<Page
36+
title="Core chart demo"
37+
subtitle="The page demonstrates the use of the core chart, including additional legend settings."
38+
settings={
39+
<PageSettingsForm
40+
selectedSettings={["showLegend", "legendPosition", "showLegendTitle", "showLegendActions", "useFallback"]}
41+
/>
42+
}
43+
>
44+
<StandaloneLegend position="bottom" series={series} />
45+
</Page>
46+
);
47+
}

src/core/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function getPointId(point: Highcharts.Point): string {
2929
export function getOptionsId(options: { id?: string; name?: string }): string {
3030
return options.id ?? options.name ?? noIdPlaceholder();
3131
}
32-
function noIdPlaceholder(): string {
32+
export function noIdPlaceholder(): string {
3333
const rand = (Math.random() * 1_000_000).toFixed(0).padStart(6, "0");
3434
return "awsui-no-id-placeholder-" + rand;
3535
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { useState } from "react";
5+
6+
import { useInternalI18n } from "@cloudscape-design/components/internal/do-not-use/i18n";
7+
8+
import { BaseI18nStrings, CoreChartProps } from "../../core/interfaces";
9+
import { LegendItemSpec, noIdPlaceholder } from "../../core/utils";
10+
import { ChartLegend as ChartLegendComponent } from "../../internal/components/chart-legend";
11+
import { ChartSeriesMarker, ChartSeriesMarkerType } from "../../internal/components/series-marker";
12+
import { fireNonCancelableEvent } from "../../internal/events";
13+
import { isEqualArrays } from "../../internal/utils/utils";
14+
15+
function isLegendItemsEqual(a: CoreChartProps.LegendItem, b: CoreChartProps.LegendItem) {
16+
return (
17+
a.id === b.id &&
18+
a.name === b.name &&
19+
a.marker === b.marker &&
20+
a.visible === b.visible &&
21+
a.highlighted === b.highlighted
22+
);
23+
}
24+
25+
export function getOptionsId(options: { id?: string; name?: string }): string {
26+
return options.id ?? options.name ?? noIdPlaceholder();
27+
}
28+
export function getPointColor(point?: Highcharts.PointOptionsObject): string {
29+
return typeof point?.color === "string" ? point.color : "black";
30+
}
31+
32+
const markersCache = new Map<string, React.ReactNode>();
33+
function renderMarker(type: ChartSeriesMarkerType, color: string, visible = true): React.ReactNode {
34+
const key = `${type}:${color}:${visible}`;
35+
const marker = markersCache.get(key) ?? <ChartSeriesMarker type={type} color={color} visible={visible} />;
36+
markersCache.set(key, marker);
37+
return marker;
38+
}
39+
40+
export function getSeriesMarkerType(series?: Highcharts.SeriesOptionsType): ChartSeriesMarkerType {
41+
if (!series) {
42+
return "large-square";
43+
}
44+
const seriesSymbol = "symbol" in series && typeof series.symbol === "string" ? series.symbol : "circle";
45+
if ("dashStyle" in series && series.dashStyle) {
46+
return "dashed";
47+
}
48+
switch (series.type) {
49+
case "area":
50+
case "areaspline":
51+
return "hollow-square";
52+
case "line":
53+
case "spline":
54+
return "line";
55+
case "scatter":
56+
switch (seriesSymbol) {
57+
case "square":
58+
return "square";
59+
case "diamond":
60+
return "diamond";
61+
case "triangle":
62+
return "triangle";
63+
case "triangle-down":
64+
return "triangle-down";
65+
case "circle":
66+
default:
67+
return "circle";
68+
}
69+
case "column":
70+
case "pie":
71+
return "large-square";
72+
case "errorbar":
73+
default:
74+
return "large-square";
75+
}
76+
}
77+
78+
export function getChartLegendItems(series: Highcharts.SeriesOptionsType[]): readonly LegendItemSpec[] {
79+
const legendItems: LegendItemSpec[] = [];
80+
const addSeriesItem = (series: Highcharts.SeriesOptionsType) => {
81+
// The pie series is not shown in the legend. Instead, we show pie segments.
82+
if (series.type === "pie") {
83+
return;
84+
}
85+
// We only support errorbar series that are linked to other series. Those are not represented separately
86+
// in the legend, but can be controlled from the outside, using controllable items visibility API.
87+
if (series.type === "errorbar") {
88+
return;
89+
}
90+
// We respect Highcharts showInLegend option to allow hiding certain series from the legend.
91+
// The same is not supported for pie chart segments.
92+
// if (series.options.showInLegend !== false) {
93+
// legendItems.push({
94+
// id: getSeriesId(series),
95+
// name: series.name,
96+
// markerType: getSeriesMarkerType(series),
97+
// color: getSeriesColor(series),
98+
// visible: series.visible,
99+
// });
100+
// }
101+
};
102+
for (const s of series) {
103+
addSeriesItem(s);
104+
if ("data" in s && Array.isArray(s.data) && s.type === "pie") {
105+
s.data?.forEach((p, idx) => {
106+
if (p instanceof Object && !Array.isArray(p)) {
107+
legendItems.push({
108+
id: getOptionsId(p),
109+
name: p.name ?? `P${idx}`,
110+
markerType: getSeriesMarkerType(s),
111+
color: getPointColor(p),
112+
visible: true,
113+
// visible: p.visible, TODO: Is it safe to always set to true?
114+
});
115+
}
116+
});
117+
}
118+
}
119+
return legendItems;
120+
}
121+
122+
export function StandaloneLegend({
123+
title,
124+
series,
125+
actions,
126+
position,
127+
i18nStrings,
128+
onItemHighlight,
129+
onVisibleItemsChange,
130+
getLegendTooltipContent,
131+
}: {
132+
title?: string;
133+
actions?: React.ReactNode;
134+
position: "bottom" | "side";
135+
i18nStrings?: BaseI18nStrings;
136+
series: Highcharts.SeriesOptionsType[];
137+
onItemHighlight?: CoreChartProps["onLegendItemHighlight"];
138+
onVisibleItemsChange?: CoreChartProps["onVisibleItemsChange"];
139+
getLegendTooltipContent?: CoreChartProps["getLegendTooltipContent"];
140+
}) {
141+
const i18n = useInternalI18n("[charts]");
142+
const ariaLabel = i18n("i18nStrings.legendAriaLabel", i18nStrings?.legendAriaLabel);
143+
144+
const [items, setItems] = useState<CoreChartProps.LegendItem[]>(() => {
145+
const itemSpecs = getChartLegendItems(series);
146+
const legendItems = itemSpecs.map(({ id, name, color, markerType, visible }) => {
147+
const marker = renderMarker(markerType, color, visible);
148+
return { id, name, marker, visible, highlighted: false };
149+
});
150+
return legendItems;
151+
});
152+
const updateItems = (items: CoreChartProps.LegendItem[]) => {
153+
setItems((prevItems) => {
154+
if (isEqualArrays(prevItems, items, isLegendItemsEqual)) {
155+
return prevItems;
156+
}
157+
return items;
158+
});
159+
};
160+
161+
if (items.length === 0) {
162+
return null;
163+
}
164+
return (
165+
<ChartLegendComponent
166+
items={items}
167+
ariaLabel={ariaLabel}
168+
actions={actions}
169+
legendTitle={title}
170+
position={position}
171+
onItemHighlightEnter={(item) => {
172+
const newItems = items.map((i) => ({ ...i, visible: item.id === i.id }));
173+
updateItems(newItems);
174+
fireNonCancelableEvent(onItemHighlight, { item });
175+
}}
176+
onItemHighlightExit={() => {
177+
const newItems = items.map(({ ...item }) => ({ ...item, highlighted: false }));
178+
updateItems(newItems);
179+
}}
180+
onItemVisibilityChange={(visibleItems) => {
181+
const newItems = items.map((i) => ({ ...i, visible: visibleItems.includes(i.id) }));
182+
updateItems(newItems);
183+
fireNonCancelableEvent(onVisibleItemsChange, { items: newItems, isApiCall: false });
184+
}}
185+
getTooltipContent={(props) => getLegendTooltipContent?.(props) ?? null}
186+
/>
187+
);
188+
}

0 commit comments

Comments
 (0)