Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ff688d4
feat: add status property support for series markers
Pixselve Nov 5, 2025
a0ba1c4
chore: make the marker accessible
Pixselve Nov 11, 2025
3afac40
feat: overlap warning with marker
Pixselve Nov 24, 2025
7b5283e
feat: use a mask for the warning icon
Pixselve Nov 25, 2025
fab6169
fix: test import
Pixselve Nov 26, 2025
42308ab
chore: remove Core i18n from the public charts
Pixselve Nov 27, 2025
04918a0
feat: expose getSeriesStatus
Pixselve Dec 3, 2025
e4b6038
Merge remote-tracking branch 'refs/remotes/origin/main' into warning-…
Pixselve Dec 10, 2025
58ab771
chore: refactor getStatus to getItemProps
Pixselve Dec 10, 2025
61377c6
Merge remote-tracking branch 'origin/main' into warning-icon-to-markers
Pixselve Dec 15, 2025
9da5a51
a11y: adds aria description to markers
Pixselve Dec 15, 2025
516755b
style: add more space to legend elements
Pixselve Dec 16, 2025
f38f478
chore: move interfaces around and rename getItemProps
Pixselve Dec 19, 2025
74233e9
Merge remote-tracking branch 'refs/remotes/origin/main' into warning-…
Pixselve Jan 5, 2026
18ce39f
chore: renamed classname + useUniqueId
Pixselve Jan 6, 2026
3ec4ad0
chore: provide i18n via a method instead of strings
Pixselve Jan 6, 2026
595be38
chore: removed unused type
Pixselve Jan 6, 2026
ec045f8
test: update documenter
Pixselve Jan 6, 2026
6eb13fc
chore: some refactoring
Pixselve Jan 7, 2026
29e16d2
chore: update method signatures
Pixselve Jan 7, 2026
33305ec
chore: make itemMarkerLabel optional
Pixselve Jan 7, 2026
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
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={{
itemMarkerStatusAriaLabel: (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
19 changes: 18 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 @@ -57,6 +57,10 @@ const permutationsForColors = [
]),
);

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 +74,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
27 changes: 25 additions & 2 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": "itemId",
"type": "string",
},
],
"returnType": "CoreChartProps.ChartItemOptions",
"type": "function",
},
"name": "getItemOptions",
"optional": true,
"systemTags": undefined,
"type": "CoreChartProps.GetItemOptions",
"visualRefreshTag": undefined,
},
{
"analyticsTag": undefined,
"defaultValue": undefined,
Expand Down Expand Up @@ -1543,7 +1566,7 @@ Supported Highcharts versions: 12.",
"description": "An object that contains all of the localized strings required by the component.",
"i18nTag": true,
"inlineType": {
"name": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings",
"name": "CoreChartProps.I18nStrings",
"type": "union",
"valueDescriptions": undefined,
"values": [
Expand All @@ -1555,7 +1578,7 @@ Supported Highcharts versions: 12.",
"name": "i18nStrings",
"optional": true,
"systemTags": undefined,
"type": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings",
"type": "CoreChartProps.I18nStrings",
"visualRefreshTag": 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({ chart: chartApi!.chart });
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({ chart: chartApi!.chart });
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({ chart: chartApi!.chart });

if (type === "gauge" || type === "solidgauge") {
expect(items).toHaveLength(1);
Expand Down
1 change: 1 addition & 0 deletions src/core/chart-api/chart-extra-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export namespace ChartExtraContext {
tooltipEnabled: boolean;
keyboardNavigationEnabled: boolean;
labels: ChartLabels;
getItemOptions?: CoreChartProps.GetItemOptions;
}

export interface Handlers {
Expand Down
35 changes: 28 additions & 7 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({
chart: this.context.chart(),
getItemOptions: this.context.settings.getItemOptions,
itemMarkerStatusAriaLabel: this.context.settings.labels.itemMarkerLabel,
});
const legendItems = itemSpecs.map(
({ id, name, color, markerType, visible, status, isSecondary, markerAriaLabel }) => {
const marker = this.renderMarker({ type: markerType, color, visible, status, ariaLabel: markerAriaLabel });
return { id, name, marker, visible, isSecondary, highlighted: prevState.get(id)?.highlighted ?? false };
},
);
this.updateLegendItems(legendItems);
};

Expand All @@ -109,9 +116,23 @@ 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,
status = "default",
color,
visible = true,
ariaLabel,
}: {
type: ChartSeriesMarkerType;
color: string;
visible?: boolean;
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
5 changes: 4 additions & 1 deletion src/core/chart-core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { InternalBaseComponentProps } from "../internal/base-component/use-base-
import * as Styles from "../internal/chart-styles";
import { castArray } from "../internal/utils/utils";
import { useChartAPI } from "./chart-api";
import { ChartExtraContext } from "./chart-api/chart-extra-context";
import { ChartContainer } from "./chart-container";
import { ChartApplication } from "./components/core-application";
import { ChartFilters } from "./components/core-filters";
Expand Down Expand Up @@ -63,17 +64,19 @@ export function InternalCoreChart({
onVisibleItemsChange,
visibleItems,
__internalRootRef,
getItemOptions,
...rest
}: CoreChartProps & InternalBaseComponentProps) {
const highcharts = rest.highcharts as null | typeof Highcharts;
const labels = useChartI18n({ ariaLabel, ariaDescription, i18nStrings });
const context = {
const context: ChartExtraContext["settings"] = {
chartId: useUniqueId(),
noDataEnabled: !!noDataOptions,
legendEnabled: legendOptions?.enabled !== false,
tooltipEnabled: tooltipOptions?.enabled !== false,
keyboardNavigationEnabled: keyboardNavigation,
labels,
getItemOptions: getItemOptions ?? (() => ({})),
};
const handlers = { onHighlight, onClearHighlight, onVisibleItemsChange };
const state = { visibleItems };
Expand Down
Loading
Loading