Skip to content

Commit 5fe2f67

Browse files
committed
feat: dual axis support in legend
1 parent 0e235d0 commit 5fe2f67

File tree

15 files changed

+533
-207
lines changed

15 files changed

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

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/core/__tests__/chart-core-series-filter.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ describe("CoreChart: series filter", () => {
6363
expect.objectContaining({
6464
detail: {
6565
items: [
66-
{ id: "L1", name: "L1", marker: expect.anything(), visible: false, highlighted: false },
67-
{ id: "L2", name: "L2", marker: expect.anything(), visible: true, highlighted: false },
66+
{ id: "L1", name: "L1", marker: expect.anything(), visible: false, highlighted: false, isSecondary: false },
67+
{ id: "L2", name: "L2", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
6868
],
6969
isApiCall: false,
7070
},

src/core/__tests__/chart-core-visibility.test.tsx

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ describe("CoreChart: visibility", () => {
135135
expect.objectContaining({
136136
detail: {
137137
items: [
138-
{ id: "L1", name: "L1", marker: expect.anything(), visible: false, highlighted: false },
139-
{ id: "L2", name: "L2", marker: expect.anything(), visible: true, highlighted: false },
140-
{ id: "L3", name: "L3", marker: expect.anything(), visible: true, highlighted: false },
138+
{ id: "L1", name: "L1", marker: expect.anything(), visible: false, highlighted: false, isSecondary: false },
139+
{ id: "L2", name: "L2", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
140+
{ id: "L3", name: "L3", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
141141
],
142142
isApiCall: false,
143143
},
@@ -152,9 +152,9 @@ describe("CoreChart: visibility", () => {
152152
expect.objectContaining({
153153
detail: {
154154
items: [
155-
{ id: "L1", name: "L1", marker: expect.anything(), visible: false, highlighted: false },
156-
{ id: "L2", name: "L2", marker: expect.anything(), visible: true, highlighted: false },
157-
{ id: "L3", name: "L3", marker: expect.anything(), visible: false, highlighted: false },
155+
{ id: "L1", name: "L1", marker: expect.anything(), visible: false, highlighted: false, isSecondary: false },
156+
{ id: "L2", name: "L2", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
157+
{ id: "L3", name: "L3", marker: expect.anything(), visible: false, highlighted: false, isSecondary: false },
158158
],
159159
isApiCall: false,
160160
},
@@ -169,9 +169,9 @@ describe("CoreChart: visibility", () => {
169169
expect.objectContaining({
170170
detail: {
171171
items: [
172-
{ id: "L1", name: "L1", marker: expect.anything(), visible: true, highlighted: false },
173-
{ id: "L2", name: "L2", marker: expect.anything(), visible: true, highlighted: false },
174-
{ id: "L3", name: "L3", marker: expect.anything(), visible: true, highlighted: false },
172+
{ id: "L1", name: "L1", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
173+
{ id: "L2", name: "L2", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
174+
{ id: "L3", name: "L3", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
175175
],
176176
isApiCall: false,
177177
},
@@ -234,8 +234,8 @@ describe("CoreChart: visibility", () => {
234234
expect.objectContaining({
235235
detail: {
236236
items: [
237-
{ id: "1", name: "Line", marker: expect.anything(), visible: true, highlighted: false },
238-
{ id: "2", name: "Line", marker: expect.anything(), visible: true, highlighted: false },
237+
{ id: "1", name: "Line", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
238+
{ id: "2", name: "Line", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
239239
],
240240
isApiCall: false,
241241
},
@@ -248,8 +248,22 @@ describe("CoreChart: visibility", () => {
248248
expect.objectContaining({
249249
detail: {
250250
items: [
251-
{ id: "1", name: "Line", marker: expect.anything(), visible: false, highlighted: false },
252-
{ id: "2", name: "Line", marker: expect.anything(), visible: false, highlighted: false },
251+
{
252+
id: "1",
253+
name: "Line",
254+
marker: expect.anything(),
255+
visible: false,
256+
highlighted: false,
257+
isSecondary: false,
258+
},
259+
{
260+
id: "2",
261+
name: "Line",
262+
marker: expect.anything(),
263+
visible: false,
264+
highlighted: false,
265+
isSecondary: false,
266+
},
253267
],
254268
isApiCall: false,
255269
},
@@ -292,9 +306,9 @@ describe("CoreChart: visibility", () => {
292306
expect.objectContaining({
293307
detail: {
294308
items: [
295-
{ id: "A", name: "A", marker: expect.anything(), visible: true, highlighted: false },
296-
{ id: "B", name: "B", marker: expect.anything(), visible: false, highlighted: false },
297-
{ id: "C", name: "C", marker: expect.anything(), visible: true, highlighted: false },
309+
{ id: "A", name: "A", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
310+
{ id: "B", name: "B", marker: expect.anything(), visible: false, highlighted: false, isSecondary: false },
311+
{ id: "C", name: "C", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
298312
],
299313
isApiCall: false,
300314
},
@@ -309,9 +323,9 @@ describe("CoreChart: visibility", () => {
309323
expect.objectContaining({
310324
detail: {
311325
items: [
312-
{ id: "A", name: "A", marker: expect.anything(), visible: true, highlighted: false },
313-
{ id: "B", name: "B", marker: expect.anything(), visible: false, highlighted: false },
314-
{ id: "C", name: "C", marker: expect.anything(), visible: false, highlighted: false },
326+
{ id: "A", name: "A", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
327+
{ id: "B", name: "B", marker: expect.anything(), visible: false, highlighted: false, isSecondary: false },
328+
{ id: "C", name: "C", marker: expect.anything(), visible: false, highlighted: false, isSecondary: false },
315329
],
316330
isApiCall: false,
317331
},
@@ -326,9 +340,9 @@ describe("CoreChart: visibility", () => {
326340
expect.objectContaining({
327341
detail: {
328342
items: [
329-
{ id: "A", name: "A", marker: expect.anything(), visible: true, highlighted: false },
330-
{ id: "B", name: "B", marker: expect.anything(), visible: true, highlighted: false },
331-
{ id: "C", name: "C", marker: expect.anything(), visible: true, highlighted: false },
343+
{ id: "A", name: "A", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
344+
{ id: "B", name: "B", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
345+
{ id: "C", name: "C", marker: expect.anything(), visible: true, highlighted: false, isSecondary: false },
332346
],
333347
isApiCall: false,
334348
},
@@ -398,8 +412,22 @@ describe("CoreChart: visibility", () => {
398412
expect.objectContaining({
399413
detail: {
400414
items: [
401-
{ id: "1", name: "Segment", marker: expect.anything(), visible: true, highlighted: false },
402-
{ id: "2", name: "Segment", marker: expect.anything(), visible: true, highlighted: false },
415+
{
416+
id: "1",
417+
name: "Segment",
418+
marker: expect.anything(),
419+
visible: true,
420+
highlighted: false,
421+
isSecondary: false,
422+
},
423+
{
424+
id: "2",
425+
name: "Segment",
426+
marker: expect.anything(),
427+
visible: true,
428+
highlighted: false,
429+
isSecondary: false,
430+
},
403431
],
404432
isApiCall: false,
405433
},
@@ -412,8 +440,22 @@ describe("CoreChart: visibility", () => {
412440
expect.objectContaining({
413441
detail: {
414442
items: [
415-
{ id: "1", name: "Segment", marker: expect.anything(), visible: false, highlighted: false },
416-
{ id: "2", name: "Segment", marker: expect.anything(), visible: false, highlighted: false },
443+
{
444+
id: "1",
445+
name: "Segment",
446+
marker: expect.anything(),
447+
visible: false,
448+
highlighted: false,
449+
isSecondary: false,
450+
},
451+
{
452+
id: "2",
453+
name: "Segment",
454+
marker: expect.anything(),
455+
visible: false,
456+
highlighted: false,
457+
isSecondary: false,
458+
},
417459
],
418460
isApiCall: false,
419461
},

src/core/chart-api/chart-extra-legend.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ export class ChartExtraLegend extends AsyncStore<ReactiveLegendState> {
8282

8383
private initLegend = () => {
8484
const itemSpecs = getChartLegendItems(this.context.chart());
85-
const legendItems = itemSpecs.map(({ id, name, color, markerType, visible }) => {
85+
const legendItems = itemSpecs.map(({ id, name, color, markerType, visible, isSecondary }) => {
8686
const marker = this.renderMarker(markerType, color, visible);
87-
return { id, name, marker, visible, highlighted: false };
87+
return { id, name, marker, visible, isSecondary, highlighted: false };
8888
});
8989
this.updateLegendItems(legendItems);
9090
};

0 commit comments

Comments
 (0)