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
15 changes: 11 additions & 4 deletions pages/03-core/core-legend.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,21 @@ const initialLegendItems: readonly LegendItem[] = [
{
id: "CPU Utilization",
name: "CPU Utilization",
marker: <ChartSeriesMarker color={colors[0]} type={"square"} visible={true} />,
marker: <ChartSeriesMarker status={"default"} color={colors[0]} type={"square"} visible={true} />,
visible: true,
highlighted: false,
},
{
id: "Memory Usage",
name: "Memory Usage",
marker: <ChartSeriesMarker color={colors[1]} type={"square"} visible={true} />,
marker: <ChartSeriesMarker status={"default"} color={colors[1]} type={"square"} visible={true} />,
visible: true,
highlighted: false,
},
{
id: "Storage Capacity",
name: "Storage Capacity",
marker: <ChartSeriesMarker color={colors[2]} type={"square"} visible={true} />,
marker: <ChartSeriesMarker status={"default"} color={colors[2]} type={"square"} visible={true} />,
visible: true,
highlighted: false,
},
Expand Down Expand Up @@ -180,7 +180,14 @@ export default function () {
...item,
visible,
highlighted: visible,
marker: <ChartSeriesMarker color={colors[index % colors.length]} type={"square"} visible={visible} />,
marker: (
<ChartSeriesMarker
status={"default"}
color={colors[index % colors.length]}
type={"square"}
visible={visible}
/>
),
};
});
});
Expand Down
12 changes: 12 additions & 0 deletions pages/03-core/core-line-chart.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const series: Highcharts.SeriesOptionsType[] = [
data: dataB,
},
{
id: "A",
name: "Comprehensive System Resource Utilization Measurements Over Time",
type: "line",
data: dataC,
Expand Down Expand Up @@ -105,6 +106,14 @@ export default function () {
<CoreChart
{...chartProps.core}
highcharts={Highcharts}
i18nStrings={{
itemMarkerAriaLabel: (status) => {
if (status === "warning") {
return "Warning status";
}
return status as never;
},
}}
options={{
lang: {
accessibility: {
Expand All @@ -126,6 +135,9 @@ export default function () {
},
},
}}
getItemOptions={({ itemId }) => ({
status: itemId === "A" ? "warning" : "default",
})}
chartHeight={400}
getTooltipContent={() => ({
point({ item, hideTooltip }) {
Expand Down
2 changes: 1 addition & 1 deletion pages/03-core/events-sync-demo.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function LegendEventsDemo() {
const legendItems: LegendItem[] = series.map((s) => ({
id: s.name,
name: s.name,
marker: <ChartSeriesMarker color={s.color} type="line" />,
marker: <ChartSeriesMarker color={s.color} type="line" status="default" />,
visible: visibleItems.has(s.name),
highlighted: highlightedItem === s.name,
}));
Expand Down
21 changes: 20 additions & 1 deletion pages/03-core/marker-permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {

import { ChartSeriesMarker, ChartSeriesMarkerProps } from "../../lib/components/internal/components/series-marker";
import PermutationsView, { createPermutations } from "../common/permutations";
import { Page } from "../common/templates";
import { Page, PageSection } from "../common/templates";

const permutationsForColors = [
colorChartsPaletteCategorical1,
Expand Down Expand Up @@ -53,10 +53,16 @@ const permutationsForColors = [
"triangle-down",
],
color: [color],
ariaLabel: ["aria"],
status: ["default"],
},
]),
);

const permutationsForWarningColors = permutationsForColors.map((permutations) =>
permutations.map((permutation) => ({ ...permutation, status: "warning" as const })),
);

export default function MarkerPermutations() {
return (
<Page title="Marker permutations" subtitle="This page lists all markers that we currently support.">
Expand All @@ -70,6 +76,19 @@ export default function MarkerPermutations() {
/>
))}
</SpaceBetween>

<PageSection title="Warning state">
<SpaceBetween size="m">
{permutationsForWarningColors.map((permutations, index) => (
<PermutationsView
key={index}
permutations={permutations}
render={(permutation) => <ChartSeriesMarker {...permutation} />}
direction="horizontal"
/>
))}
</SpaceBetween>
</PageSection>
</Page>
);
}
5 changes: 4 additions & 1 deletion pages/06-visual-tests/cartesian-tooltip.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const dataA = baseline.map(({ x, y }) => ({ x, y }));
const dataB = baseline.map(({ x, y }, index) => ({ x, y: y + index * 10000 }));

const series: Highcharts.SeriesOptionsType[] = [
{ name: "A", type: "spline", data: dataA },
{ id: "A", name: "A", type: "spline", data: dataA },
{ name: "B", type: "spline", data: dataB },
];

Expand All @@ -49,6 +49,9 @@ export default function () {
}}
chartHeight={400}
tooltip={{ placement: "outside" }}
getItemOptions={({ itemId }) => ({
status: itemId === "A" ? "warning" : "default",
})}
getTooltipContent={() => ({
footer() {
return <Button>Footer action</Button>;
Expand Down
4 changes: 4 additions & 0 deletions pages/06-visual-tests/column-hover.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function Chart({ type }: { type: "single" | "stacked" | "grouped" }) {
plotOptions: { series: { stacking: type === "stacked" ? "normal" : undefined } },
series: [
{
id: "Severe",
name: "Severe",
type: "column" as const,
data: [22, 28, 25, 13, 28],
Expand Down Expand Up @@ -75,6 +76,9 @@ function Chart({ type }: { type: "single" | "stacked" | "grouped" }) {
],
yAxis: [{ title: { text: "Error count" } }],
}}
getItemOptions={({ itemId }) => ({
status: itemId === "Severe" ? "warning" : "default",
})}
callback={(api) => {
setTimeout(() => {
if (api.chart.series) {
Expand Down
5 changes: 4 additions & 1 deletion pages/06-visual-tests/pie-tooltip.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const series: Highcharts.SeriesOptionsType[] = [
type: "pie",
data: [
{ name: "Running", y: 60 },
{ name: "Failed", y: 30 },
{ name: "Failed", y: 30, id: "Failed" },
{ name: "In-progress", y: 10 },
],
},
Expand All @@ -31,6 +31,9 @@ export default function () {
options={{
series: series,
}}
getItemOptions={({ itemId }) => ({
status: itemId === "Failed" ? "warning" : "default",
})}
chartHeight={400}
getTooltipContent={() => ({
footer() {
Expand Down
23 changes: 23 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,29 @@ minimum width, the horizontal scrollbar is automatically added.",
"type": "CoreChartProps.FooterOptions",
"visualRefreshTag": undefined,
},
{
"analyticsTag": undefined,
"defaultValue": undefined,
"deprecatedTag": undefined,
"description": "Specifies the options for each item in the chart.",
"i18nTag": undefined,
"inlineType": {
"name": "CoreChartProps.GetItemOptions",
"parameters": [
{
"name": "props",
"type": "CoreChartProps.GetItemOptionsProps",
},
],
"returnType": "CoreChartProps.ChartItemOptions",
"type": "function",
},
"name": "getItemOptions",
"optional": true,
"systemTags": undefined,
"type": "CoreChartProps.GetItemOptions",
"visualRefreshTag": undefined,
},
{
"analyticsTag": undefined,
"defaultValue": undefined,
Expand Down
48 changes: 47 additions & 1 deletion src/core/__tests__/chart-core-legend.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

import { act } from "react";
import highcharts from "highcharts";
import { vi } from "vitest";
import { describe, vi } from "vitest";

import { KeyCode } from "@cloudscape-design/component-toolkit/internal";

import * as seriesMarker from "../../../lib/components/internal/components/series-marker";
import {
createChartWrapper,
hoverLegendItem,
Expand Down Expand Up @@ -93,6 +94,8 @@ const mouseOver = (element: HTMLElement) => element.dispatchEvent(new MouseEvent
const mouseOut = (element: HTMLElement) => element.dispatchEvent(new MouseEvent("mouseout", { bubbles: true }));
const mouseLeavePause = () => new Promise((resolve) => setTimeout(resolve, 300));

vi.mock(import("../../../lib/components/internal/components/series-marker"), { spy: true });

describe("CoreChart: legend", () => {
test("renders no legend when legend.enabled=false", () => {
renderChart({ highcharts, options: { series }, legend: { enabled: false } });
Expand Down Expand Up @@ -527,6 +530,49 @@ describe("CoreChart: legend", () => {
rerender({ highcharts, options: { series: lineSeries.filter((s) => s.name !== "L1") } });
expect(getItems({ dimmed: false, active: true }).map((w) => w.getElement().textContent)).toEqual(["L2", "Line 3"]);
});

describe("Marker status", () => {
const seriesMarkerMock = vi.mocked(seriesMarker.ChartSeriesMarker);

beforeEach(() => {
seriesMarkerMock.mockImplementation((props) => {
return <div data-testid={props.status}></div>;
});
});
afterEach(() => {
seriesMarkerMock.mockReset();
});

test("should render markers using the corresponding status", () => {
const { wrapper } = renderChart({
highcharts,
options: {
series: [
{
id: "L1",
type: "line",
name: "L1",
data: [1],
},
{
id: "L2",
type: "line",
name: "L2",
data: [1],
},
],
},
getItemOptions: ({ itemId }) => ({
status: itemId === "L1" ? "warning" : "default",
}),
});

const warnings = wrapper.findAll('[data-testid="warning"]');
const defaults = wrapper.findAll('[data-testid="default"]');
expect(warnings).toHaveLength(1);
expect(defaults).toHaveLength(1);
});
});
});

describe("CoreChart: secondary legend", () => {
Expand Down
6 changes: 3 additions & 3 deletions src/core/__tests__/chart-core-utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe("CoreChart: utils", () => {
callback: (api) => (chartApi = api),
});

const items = getChartLegendItems(chartApi!.chart);
const items = getChartLegendItems(chartApi!.chart, () => ({}), undefined);
expect(items[0].isSecondary).toBe(axisOptions.opposite);
},
);
Expand Down Expand Up @@ -109,7 +109,7 @@ describe("CoreChart: utils", () => {
callback: (api) => (chartApi = api),
});

const items = getChartLegendItems(chartApi!.chart);
const items = getChartLegendItems(chartApi!.chart, () => ({}), undefined);
expect(items).toHaveLength(2);
expect(items[0].isSecondary).toBe(false);
expect(items[1].isSecondary).toBe(true);
Expand Down Expand Up @@ -137,7 +137,7 @@ describe("CoreChart: utils", () => {
callback: (api) => (chartApi = api),
});

const items = getChartLegendItems(chartApi!.chart);
const items = getChartLegendItems(chartApi!.chart, () => ({}), undefined);

if (type === "gauge" || type === "solidgauge") {
expect(items).toHaveLength(1);
Expand Down
6 changes: 5 additions & 1 deletion src/core/chart-api/chart-extra-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { NonCancelableEventHandler } from "../../internal/events";
import { getChartSeries } from "../../internal/utils/chart-series";
import { getSeriesData } from "../../internal/utils/series-data";
import { ChartLabels } from "../i18n-utils";
import { CoreChartProps, Rect } from "../interfaces";
import { CoreChartProps, CoreI18nStrings, Rect } from "../interfaces";
import { getGroupRect, isSeriesStacked } from "../utils";

// Chart API context is used for dependency injection for chart utilities.
Expand All @@ -32,6 +32,8 @@ export namespace ChartExtraContext {
tooltipEnabled: boolean;
keyboardNavigationEnabled: boolean;
labels: ChartLabels;
getItemOptions: CoreChartProps.GetItemOptions;
itemMarkerAriaLabel: CoreI18nStrings["itemMarkerAriaLabel"];
}

export interface Handlers {
Expand Down Expand Up @@ -63,6 +65,8 @@ export function createChartContext(): ChartExtraContext {
tooltipEnabled: false,
keyboardNavigationEnabled: false,
labels: {},
getItemOptions: () => ({}),
itemMarkerAriaLabel: () => "",
},
handlers: {},
state: {},
Expand Down
31 changes: 23 additions & 8 deletions src/core/chart-api/chart-extra-legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type Highcharts from "highcharts";

import { LegendItem } from "../../internal/components/interfaces";
import { ChartSeriesMarker, ChartSeriesMarkerType } from "../../internal/components/series-marker";
import { ChartSeriesMarkerStatus } from "../../internal/components/series-marker/interfaces";
import { fireNonCancelableEvent } from "../../internal/events";
import AsyncStore from "../../internal/utils/async-store";
import { getChartSeries } from "../../internal/utils/chart-series";
Expand Down Expand Up @@ -83,11 +84,17 @@ export class ChartExtraLegend extends AsyncStore<ReactiveLegendState> {

private initLegend = () => {
const prevState = this.get().items.reduce((map, item) => map.set(item.id, item), new Map<string, LegendItem>());
const itemSpecs = getChartLegendItems(this.context.chart());
const legendItems = itemSpecs.map(({ id, name, color, markerType, visible, isSecondary }) => {
const marker = this.renderMarker(markerType, color, visible);
return { id, name, marker, visible, isSecondary, highlighted: prevState.get(id)?.highlighted ?? false };
});
const itemSpecs = getChartLegendItems(
this.context.chart(),
this.context.settings.getItemOptions,
this.context.settings.itemMarkerAriaLabel,
);
const legendItems = itemSpecs.map(
({ id, name, color, markerType, visible, status, isSecondary, markerAriaLabel }) => {
const marker = this.renderMarker(markerType, color, visible, status, markerAriaLabel);
return { id, name, marker, visible, isSecondary, highlighted: prevState.get(id)?.highlighted ?? false };
},
);
this.updateLegendItems(legendItems);
};

Expand All @@ -109,9 +116,17 @@ export class ChartExtraLegend extends AsyncStore<ReactiveLegendState> {
// The chart markers derive from type and color and are cached to avoid unnecessary renders,
// and allow comparing them by reference.
private markersCache = new Map<string, React.ReactNode>();
public renderMarker(type: ChartSeriesMarkerType, color: string, visible = true): React.ReactNode {
const key = `${type}:${color}:${visible}`;
const marker = this.markersCache.get(key) ?? <ChartSeriesMarker type={type} color={color} visible={visible} />;
public renderMarker(
type: ChartSeriesMarkerType,
color: string,
visible = true,
status?: ChartSeriesMarkerStatus,
ariaLabel?: string,
): React.ReactNode {
const key = `${type}:${color}:${visible}:${status}`;
const marker = this.markersCache.get(key) ?? (
<ChartSeriesMarker type={type} color={color} visible={visible} status={status} ariaLabel={ariaLabel} />
);
this.markersCache.set(key, marker);
return marker;
}
Expand Down
Loading
Loading