diff --git a/pages/01-cartesian-chart/linked-series.page.tsx b/pages/01-cartesian-chart/linked-series.page.tsx
index d4fd3484..e1cd768a 100644
--- a/pages/01-cartesian-chart/linked-series.page.tsx
+++ b/pages/01-cartesian-chart/linked-series.page.tsx
@@ -1,129 +1,63 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { colorChartsPaletteCategorical5 } from "@cloudscape-design/design-tokens";
-
import { CartesianChart } from "../../lib/components";
import { useChartSettings } from "../common/page-settings";
import { Page } from "../common/templates";
+import pseudoRandom from "../utils/pseudo-random";
+
+const data = [
+ { x: 1600984800000, y: 58020 },
+ { x: 1600985700000, y: 102402 },
+ { x: 1600986600000, y: 104920 },
+ { x: 1600987500000, y: 94031 },
+ { x: 1600988400000, y: 125021 },
+ { x: 1600989300000, y: 159219 },
+ { x: 1600990200000, y: 193082 },
+ { x: 1600991100000, y: 162592 },
+ { x: 1600992000000, y: 274021 },
+ { x: 1600992900000, y: 264286 },
+ { x: 1600993800000, y: 289210 },
+ { x: 1600994700000, y: 256362 },
+ { x: 1600995600000, y: 257306 },
+ { x: 1600996500000, y: 186776 },
+ { x: 1600997400000, y: 294020 },
+ { x: 1600998300000, y: 385975 },
+ { x: 1600999200000, y: 486039 },
+ { x: 1601000100000, y: 490447 },
+ { x: 1601001000000, y: 361845 },
+ { x: 1601001900000, y: 339058 },
+ { x: 1601002800000, y: 298028 },
+ { x: 1601003700000, y: 231902 },
+ { x: 1601004600000, y: 224558 },
+];
+
+const dataProjected = data.map(({ x, y }) => ({ x, y: y + pseudoRandom() * 50000 }));
export default function () {
const { chartProps } = useChartSettings();
return (
{
+ const seriesName = item.series.name;
+ return { key: seriesName === "Site 1" ? "Site 1 avg" : seriesName };
+ },
+ }}
/>
);
diff --git a/pages/01-cartesian-chart/zoned-series.page.tsx b/pages/01-cartesian-chart/zoned-series.page.tsx
new file mode 100644
index 00000000..676dd6b8
--- /dev/null
+++ b/pages/01-cartesian-chart/zoned-series.page.tsx
@@ -0,0 +1,90 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import SpaceBetween from "@cloudscape-design/components/space-between";
+import { colorChartsThresholdNegative, colorChartsThresholdNeutral } from "@cloudscape-design/design-tokens";
+
+import { CartesianChart, CartesianChartProps } from "../../lib/components";
+import { useChartSettings } from "../common/page-settings";
+import { Page } from "../common/templates";
+import pseudoRandom from "../utils/pseudo-random";
+
+const data1 = [
+ { x: 1600984800000, y: 58020 },
+ { x: 1600985700000, y: 102402 },
+ { x: 1600986600000, y: 104920 },
+ { x: 1600987500000, y: 94031 },
+ { x: 1600988400000, y: 125021 },
+ { x: 1600989300000, y: 159219 },
+ { x: 1600990200000, y: 193082 },
+ { x: 1600991100000, y: 162592 },
+ { x: 1600992000000, y: 274021 },
+ { x: 1600992900000, y: 264286 },
+ { x: 1600993800000, y: 289210 },
+ { x: 1600994700000, y: 256362 },
+ { x: 1600995600000, y: 257306 },
+ { x: 1600996500000, y: 186776 },
+ { x: 1600997400000, y: 294020 },
+ { x: 1600998300000, y: 385975 },
+ { x: 1600999200000, y: 486039 },
+ { x: 1601000100000, y: 490447 },
+ { x: 1601001000000, y: 361845 },
+ { x: 1601001900000, y: 339058 },
+ { x: 1601002800000, y: 298028 },
+ { x: 1601003700000, y: 231902 },
+ { x: 1601004600000, y: 364558 },
+ { x: 1601005500000, y: 413901 },
+ { x: 1601006400000, y: 432839 },
+ { x: 1601007300000, y: 464943 },
+ { x: 1601008200000, y: 464405 },
+ { x: 1601009100000, y: 490391 },
+ { x: 1601010000000, y: 513570 },
+ { x: 1601010900000, y: 552592 },
+ { x: 1601011800000, y: 538910 },
+ { x: 1601012700000, y: 599492 },
+ { x: 1601013600000, y: 643910 },
+];
+const data2 = data1.map(({ x, y }) => ({ x, y: y + 10_000 + pseudoRandom() * 100_000 }));
+
+export default function () {
+ const { chartProps } = useChartSettings();
+
+ const xyzZones: CartesianChartProps.SeriesZone[] = [
+ { value: 1601004100000, dashStyle: "Solid" },
+ { value: undefined, dashStyle: "Dash" },
+ ];
+
+ const performanceZones: CartesianChartProps.SeriesZone[] = [
+ { value: 250000, color: colorChartsThresholdNegative, dashStyle: "Dash" },
+ { value: undefined, dashStyle: "Solid" },
+ ];
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/cartesian-chart/chart-series-cartesian.tsx b/src/cartesian-chart/chart-series-cartesian.tsx
index dc064596..e53dd16a 100644
--- a/src/cartesian-chart/chart-series-cartesian.tsx
+++ b/src/cartesian-chart/chart-series-cartesian.tsx
@@ -5,7 +5,7 @@ import type Highcharts from "highcharts";
import { colorChartsErrorBarMarker } from "@cloudscape-design/design-tokens";
-import { PointDataItemType, RangeDataItemOptions } from "../core/interfaces";
+import { RangeDataItemOptions } from "../core/interfaces";
import { createBubbleMetadata, createThresholdMetadata, getOptionsId } from "../core/utils";
import * as Styles from "../internal/chart-styles";
import { Writeable } from "../internal/utils/utils";
@@ -90,7 +90,7 @@ export const transformCartesianSeries = (
const { custom } = createBubbleMetadata(s);
return { ...s, data: s.data.map((p) => ({ x: p.x, y: p.y, z: p.size })), ...shared, custom, ...getColorProps(s) };
}
- return { ...s, data: s.data as Writeable, ...shared, ...getColorProps(s) };
+ return { ...s, data: s.data, ...shared, ...getColorProps(s) } as Highcharts.SeriesOptionsType;
}
const series = originalSeries.map(transformSeriesToHighcharts);
// We inject a fake empty series so that the empty state still shows axes, if defined.
diff --git a/src/cartesian-chart/index.tsx b/src/cartesian-chart/index.tsx
index 6b7b52d7..cb20b301 100644
--- a/src/cartesian-chart/index.tsx
+++ b/src/cartesian-chart/index.tsx
@@ -5,7 +5,7 @@ import { forwardRef } from "react";
import { warnOnce } from "@cloudscape-design/component-toolkit/internal";
-import { PointDataItemType, RangeDataItemOptions, SizePointDataItemOptions } from "../core/interfaces";
+import { PointDataItemType, RangeDataItemOptions, SizePointDataItemOptions, WithZones } from "../core/interfaces";
import { getDataAttributes } from "../internal/base-component/get-data-attributes";
import useBaseComponent from "../internal/base-component/use-base-component";
import { applyDisplayName } from "../internal/utils/apply-display-name";
@@ -141,6 +141,7 @@ function transformLineLikeSeries<
dashStyle: s.dashStyle,
yAxis: s.yAxis,
linkedTo: s.linkedTo,
+ ...transformZones(s),
data,
} as S;
}
@@ -153,7 +154,16 @@ function transformColumnSeries(
@@ -251,6 +261,12 @@ function transformRangeData(data: readonly RangeDataItemOptions[]): readonly Ran
return data.map((d) => ({ x: d.x, low: d.low, high: d.high }));
}
+function transformZones(series: WithZones) {
+ const zoneAxis = series.zoneAxis === "x" || series.zoneAxis === "y" ? series.zoneAxis : undefined;
+ const zones = series.zones?.map(({ value, color, dashStyle }) => ({ value, color, dashStyle }));
+ return { zoneAxis, zones };
+}
+
function transformXAxisOptions(axis?: CartesianChartProps.XAxisOptions): CartesianChartProps.XAxisOptions {
return transformAxisOptions(axis);
}
diff --git a/src/cartesian-chart/interfaces.ts b/src/cartesian-chart/interfaces.ts
index 9a0d5cfa..e5203545 100644
--- a/src/cartesian-chart/interfaces.ts
+++ b/src/cartesian-chart/interfaces.ts
@@ -212,6 +212,8 @@ export namespace CartesianChartProps {
}
export type TooltipPointFormatted = CoreTypes.BaseTooltipPointFormatted;
+ export type SeriesZone = CoreTypes.SeriesZone;
+
export type LegendOptions = CoreTypes.BaseLegendOptions;
export type FilterOptions = CoreTypes.BaseFilterOptions;
diff --git a/src/core/__tests__/chart-core-linked-series.test.tsx b/src/core/__tests__/chart-core-linked-series.test.tsx
index 31ed4ef4..e6f9309f 100644
--- a/src/core/__tests__/chart-core-linked-series.test.tsx
+++ b/src/core/__tests__/chart-core-linked-series.test.tsx
@@ -199,51 +199,7 @@ describe("CoreChart: linked series — keyboard navigation", () => {
expect(describeFocusedElement()).toContain("Primary");
});
- // Series layout for partial-overlap test:
- // Primary: x=1,2 | Projected (linked): x=2,3 — x=2 is shared (primary wins), x=3 is linked-only
- const seriesPartialOverlap: Highcharts.SeriesOptionsType[] = [
- {
- type: "line",
- id: "p",
- name: "Primary",
- data: [
- { x: 1, y: 10 },
- { x: 2, y: 20 },
- ],
- },
- {
- type: "line",
- id: "proj",
- name: "Projected",
- data: [
- { x: 2, y: 25 },
- { x: 3, y: 35 },
- ],
- linkedTo: "p",
- showInLegend: false,
- },
- ];
-
- test("primary wins at shared X; linked-only X values remain navigable", () => {
- renderChart({ highcharts, options: { series: seriesPartialOverlap }, ariaLabel: "Test chart" });
-
- focusApplication();
- keyDown(KeyCode.home); // group at x=1
- keyDown(KeyCode.down); // enter point — Primary x=1
-
- expect(describeFocusedElement()).toContain("Primary");
-
- keyDown(KeyCode.right); // Primary x=2 (wins over Projected x=2)
- expect(describeFocusedElement()).toContain("Primary");
-
- keyDown(KeyCode.right); // Projected x=3 (linked-only, no primary point here)
- expect(describeFocusedElement()).toContain("Projected");
-
- keyDown(KeyCode.right); // wraps back to Primary x=1
- expect(describeFocusedElement()).toContain("Primary");
- });
-
- test("linked series point is reachable from group navigation and can navigate back to master", () => {
+ test("linked series are navigable independently from master", () => {
renderChart({ highcharts, options: { series }, ariaLabel: "Test chart" });
focusApplication();
@@ -252,14 +208,15 @@ describe("CoreChart: linked series — keyboard navigation", () => {
expect(describeFocusedElement()).toContain("Other");
- // Navigate up within group to reach Projected, then Primary
+ // Navigate up within group to reach Projected
keyDown(KeyCode.up); // Projected x=1
expect(describeFocusedElement()).toContain("Projected");
- // From Projected, navigate backwards in series — should reach Primary
- keyDown(KeyCode.left); // wraps to last in family: Projected x=2
- keyDown(KeyCode.left); // Projected x=1
- keyDown(KeyCode.left); // Primary x=2
- expect(describeFocusedElement()).toContain("Primary");
+ // Linked series are navigated separately — left/right stays within Projected
+ keyDown(KeyCode.right); // Projected x=2
+ expect(describeFocusedElement()).toContain("Projected");
+
+ keyDown(KeyCode.right); // wraps back to Projected x=1
+ expect(describeFocusedElement()).toContain("Projected");
});
});
diff --git a/src/core/__tests__/chart-core-zoned-series.test.tsx b/src/core/__tests__/chart-core-zoned-series.test.tsx
new file mode 100644
index 00000000..4d63c57f
--- /dev/null
+++ b/src/core/__tests__/chart-core-zoned-series.test.tsx
@@ -0,0 +1,128 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { act } from "react";
+import highcharts from "highcharts";
+import { vi } from "vitest";
+
+import "highcharts/highcharts-more";
+import "highcharts/modules/accessibility";
+import markerStyles from "../../../lib/components/internal/components/series-marker/styles.selectors";
+import { createChartWrapper, renderChart } from "./common";
+import { HighchartsTestHelper } from "./highcharts-utils";
+
+const hc = new HighchartsTestHelper(highcharts);
+
+beforeAll(() => {
+ vi.useFakeTimers();
+});
+
+afterAll(() => {
+ vi.useRealTimers();
+});
+
+function getTooltipMarker(itemIndex: number) {
+ const wrapper = createChartWrapper();
+ const body = wrapper.findTooltip()!.findBody()!.getElement();
+ const markers = body.querySelectorAll(`.${markerStyles.marker}`);
+ return markers[itemIndex] as HTMLElement;
+}
+
+function getMarkerFillColor(marker: HTMLElement): string {
+ const svg = marker.querySelector("svg")!;
+ const filled = svg.querySelector("rect[fill]:not([fill='white']):not([fill='black'])") as SVGElement;
+ return filled?.getAttribute("fill") ?? "";
+}
+
+function isDashedMarker(marker: HTMLElement): boolean {
+ const svg = marker.querySelector("svg")!;
+ // Dashed marker has two rects (excluding mask rects)
+ const rects = svg.querySelectorAll("rect:not([fill='white']):not([fill='black'])");
+ return rects.length === 2;
+}
+
+describe("CoreChart: zoned series — tooltip marker", () => {
+ const seriesWithZones: Highcharts.SeriesOptionsType[] = [
+ {
+ type: "line",
+ id: "s1",
+ name: "Series 1",
+ color: "blue",
+ dashStyle: "Solid",
+ data: [
+ { x: 1, y: 10 },
+ { x: 2, y: 20 },
+ { x: 3, y: 30 },
+ ],
+ zoneAxis: "x",
+ zones: [
+ { value: 2, color: "red", dashStyle: "Dash" },
+ { color: "green" }, // no dashStyle — should fallback to series dashStyle (Solid)
+ ],
+ },
+ ];
+
+ test("tooltip marker uses zone color when point is in a zone", () => {
+ renderChart({ highcharts, options: { series: seriesWithZones } });
+
+ // Point at x=1 is in the first zone (value < 2), zone color is red
+ act(() => hc.highlightChartPoint(0, 0));
+
+ const marker = getTooltipMarker(0);
+ expect(getMarkerFillColor(marker)).toBe("red");
+ });
+
+ test("tooltip marker uses zone dashStyle when zone defines dashStyle", () => {
+ renderChart({ highcharts, options: { series: seriesWithZones } });
+
+ // Point at x=1 is in the first zone which has dashStyle: "Dash"
+ act(() => hc.highlightChartPoint(0, 0));
+
+ const marker = getTooltipMarker(0);
+ expect(isDashedMarker(marker)).toBe(true);
+ });
+
+ test("tooltip marker falls back to series color when zone has no color", () => {
+ const seriesNoZoneColor: Highcharts.SeriesOptionsType[] = [
+ {
+ type: "line",
+ id: "s1",
+ name: "Series 1",
+ color: "blue",
+ data: [
+ { x: 1, y: 10 },
+ { x: 2, y: 20 },
+ ],
+ zoneAxis: "x",
+ zones: [{ value: 2 }], // no color, no dashStyle
+ },
+ ];
+
+ renderChart({ highcharts, options: { series: seriesNoZoneColor } });
+
+ act(() => hc.highlightChartPoint(0, 0));
+
+ const marker = getTooltipMarker(0);
+ expect(getMarkerFillColor(marker)).toBe("blue");
+ });
+
+ test("tooltip marker falls back to series dashStyle when zone has no dashStyle", () => {
+ renderChart({ highcharts, options: { series: seriesWithZones } });
+
+ // Point at x=3 is in the second zone which has no dashStyle — series dashStyle is "Solid"
+ act(() => hc.highlightChartPoint(0, 2));
+
+ const marker = getTooltipMarker(0);
+ expect(isDashedMarker(marker)).toBe(false);
+ });
+
+ test("tooltip marker uses zone color for second zone", () => {
+ renderChart({ highcharts, options: { series: seriesWithZones } });
+
+ // Point at x=3 is in the second zone, zone color is green
+ act(() => hc.highlightChartPoint(0, 2));
+
+ const marker = getTooltipMarker(0);
+ expect(getMarkerFillColor(marker)).toBe("green");
+ });
+});
diff --git a/src/core/chart-api/chart-extra-context.tsx b/src/core/chart-api/chart-extra-context.tsx
index b0da6267..de1a10c5 100644
--- a/src/core/chart-api/chart-extra-context.tsx
+++ b/src/core/chart-api/chart-extra-context.tsx
@@ -120,30 +120,18 @@ function computeDerivedState(chart: SafeChart): ChartExtraContext.DerivedState {
}
const allX = Array.from(allXSet).sort(compareX);
- // For each series, compute the ordered list of points to navigate through. For a linked family
- // (primary + its linked children), all members share the same flat list: points sorted by X,
- // with family members interleaved in series order within the same X value.
const seriesPointsMap = new WeakMap();
for (const s of getChartSeries(chart)) {
- if (seriesPointsMap.has(s)) {
- continue; // Already computed as part of a family.
- }
- const master = s.linkedParent ?? s;
- const family = [master, ...master.linkedSeries];
- const allFamilyPoints = family
- .flatMap((m) => allPointsInSeries.get(m) ?? [])
- .sort(({ x: x1 }, { x: x2 }) => compareX(x1, x2));
+ const allSeriesPoints = (allPointsInSeries.get(s) ?? []).sort(({ x: x1 }, { x: x2 }) => compareX(x1, x2));
const usedXSet = new Set();
- const familyPoints = new Array();
- for (const point of allFamilyPoints) {
+ const navigableSeriesPoints = new Array();
+ for (const point of allSeriesPoints) {
if (!usedXSet.has(point.x)) {
- familyPoints.push(point);
+ navigableSeriesPoints.push(point);
usedXSet.add(point.x); // We ignore points that share X coordinate.
}
}
- for (const member of family) {
- seriesPointsMap.set(member, familyPoints);
- }
+ seriesPointsMap.set(s, navigableSeriesPoints);
}
return {
allX,
diff --git a/src/core/components/core-tooltip.tsx b/src/core/components/core-tooltip.tsx
index 14bcf6fc..8b2de2d8 100644
--- a/src/core/components/core-tooltip.tsx
+++ b/src/core/components/core-tooltip.tsx
@@ -18,7 +18,7 @@ import { BaseI18nStrings, CoreChartProps } from "../interfaces";
import {
getPointColor,
getPointId,
- getSeriesColor,
+ getPointMarkerType,
getSeriesId,
getSeriesMarkerType,
isXThreshold,
@@ -180,11 +180,11 @@ function getTooltipContentCartesian(
// By design, every point of the group has the same x value.
const x = group[0].x;
const chart = group[0].series.chart;
- const getSeriesMarker = (series: SafeSeries) => {
- const itemOptions = api.context.settings.getItemOptions?.(getSeriesId(series));
+ const getPointMarker = (point: Highcharts.Point) => {
+ const itemOptions = api.context.settings.getItemOptions?.(getSeriesId(point.series));
return api.renderMarker({
- type: getSeriesMarkerType(series),
- color: getSeriesColor(series),
+ type: getPointMarkerType(point),
+ color: getPointColor(point),
visible: true,
status: itemOptions?.status,
ariaLabel: api.context.settings.labels.itemMarkerLabel?.(itemOptions?.status),
@@ -205,7 +205,7 @@ function getTooltipContentCartesian(
return {
key: customContent?.key ?? item.point.series.name,
value: customContent?.value ?? defaultValue,
- marker: getSeriesMarker(item.point.series),
+ marker: getPointMarker(item.point),
subItems: customContent?.subItems ?? bubbleSubItems,
expandableId: customContent?.expandable ? item.point.series.name : undefined,
highlighted: item.point.x === point?.x && item.point.y === point?.y,
diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts
index df13ce71..729c011c 100644
--- a/src/core/interfaces.ts
+++ b/src/core/interfaces.ts
@@ -206,27 +206,27 @@ export interface BaseTooltipPointFormatted {
subItems?: ReadonlyArray<{ key: React.ReactNode; value: React.ReactNode }>;
}
-export interface AreaSeriesOptions extends BaseCartesianLineLikeOptions, LinkableSeries {
+export interface AreaSeriesOptions extends BaseCartesianLineLikeOptions, LinkableSeries, WithZones {
type: "area";
data: readonly PointDataItemType[];
}
-export interface AreaSplineSeriesOptions extends BaseCartesianLineLikeOptions, LinkableSeries {
+export interface AreaSplineSeriesOptions extends BaseCartesianLineLikeOptions, LinkableSeries, WithZones {
type: "areaspline";
data: readonly PointDataItemType[];
}
-export interface ColumnSeriesOptions extends BaseCartesianSeriesOptions, LinkableSeries {
+export interface ColumnSeriesOptions extends BaseCartesianSeriesOptions, LinkableSeries, WithZones {
type: "column";
data: readonly PointDataItemType[];
}
-export interface LineSeriesOptions extends BaseCartesianLineLikeOptions, LinkableSeries {
+export interface LineSeriesOptions extends BaseCartesianLineLikeOptions, LinkableSeries, WithZones {
type: "line";
data: readonly PointDataItemType[];
}
-export interface SplineSeriesOptions extends BaseCartesianLineLikeOptions, LinkableSeries {
+export interface SplineSeriesOptions extends BaseCartesianLineLikeOptions, LinkableSeries, WithZones {
type: "spline";
data: readonly PointDataItemType[];
}
@@ -319,6 +319,17 @@ export interface LinkableSeries {
linkedTo?: string;
}
+export interface WithZones {
+ zoneAxis?: "x" | "y";
+ zones?: readonly SeriesZone[];
+}
+
+export interface SeriesZone {
+ value?: number;
+ color?: string;
+ dashStyle?: Highcharts.DashStyleValue;
+}
+
interface BaseCartesianLineLikeOptions extends BaseCartesianSeriesOptions {
dashStyle?: Highcharts.DashStyleValue;
}
diff --git a/src/core/utils.ts b/src/core/utils.ts
index 6fcc7358..4c10b447 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -5,7 +5,14 @@ import type Highcharts from "highcharts";
import { ChartSeriesMarkerType } from "../internal/components/series-marker";
import { ChartSeriesMarkerStatus } from "../internal/components/series-marker/interfaces";
-import { getChartSeries, getSeriesData, SafeChart, SafeSeries } from "../internal/utils/highcharts";
+import {
+ getChartSeries,
+ getPointZone,
+ getSeriesData,
+ SafeChart,
+ SafePoint,
+ SafeSeries,
+} from "../internal/utils/highcharts";
import { castArray } from "../internal/utils/utils";
import { getFormatter } from "./formatters";
import { ChartLabels } from "./i18n-utils";
@@ -28,7 +35,7 @@ export interface LegendItemSpec {
export function getSeriesId(series: SafeSeries): string {
return getOptionsId(series.options);
}
-export function getPointId(point: Highcharts.Point): string {
+export function getPointId(point: SafePoint): string {
return getOptionsId(point.options);
}
export function getOptionsId(options: { id?: string; name?: string }): string {
@@ -87,14 +94,22 @@ export function getBubbleSeriesSizeAxis(series: SafeSeries): undefined | string
return custom?.awsui?.sizeAxis;
}
-export function getSeriesMarkerType(series?: SafeSeries): ChartSeriesMarkerType {
+export function getPointMarkerType(point?: SafePoint): ChartSeriesMarkerType {
+ const pointZone = point && getPointZone(point);
+ return getSeriesMarkerType(point?.series, pointZone?.dashStyle);
+}
+
+export function getSeriesMarkerType(series?: SafeSeries, dashStyle?: Highcharts.DashStyleValue): ChartSeriesMarkerType {
if (!series) {
return "large-square";
}
const seriesSymbol = "symbol" in series && typeof series.symbol === "string" ? series.symbol : "circle";
// In Highcharts, dashStyle supports different types of dashes: https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-dashstyle-all/
// Return a dashed legend symbol for all of these dashes, excluding the default "Solid" option
- if ("dashStyle" in series.options && series.options.dashStyle && series.options.dashStyle !== "Solid") {
+ if (
+ (dashStyle && dashStyle !== "Solid") ||
+ ("dashStyle" in series.options && series.options.dashStyle && series.options.dashStyle !== "Solid")
+ ) {
return "dashed";
}
switch (series.type) {
@@ -134,8 +149,11 @@ export function getSeriesMarkerType(series?: SafeSeries): ChartSeriesMarkerType
export function getSeriesColor(series?: SafeSeries): string {
return typeof series?.color === "string" ? series.color : "black";
}
-export function getPointColor(point?: Highcharts.Point): string {
- return typeof point?.color === "string" ? point.color : "black";
+export function getPointColor(point?: SafePoint): string {
+ const pointZone = point && getPointZone(point);
+ const zoneColor = typeof pointZone?.color === "string" ? pointZone.color : undefined;
+ const pointColor = typeof point?.color === "string" ? point.color : undefined;
+ return zoneColor ?? pointColor ?? "black";
}
// The custom legend implementation does not rely on the Highcharts legend. When Highcharts legend is disabled,
@@ -183,7 +201,7 @@ export function getChartLegendItems({
});
}
};
- const addPointItem = (point: Highcharts.Point, isSecondary: boolean) => {
+ const addPointItem = (point: SafePoint, isSecondary: boolean) => {
if (point?.series?.type === "pie") {
const pointId = getPointId(point);
legendItems.push({
@@ -306,7 +324,7 @@ export function getLegendsProps(
// This function returns coordinates of a rectangle, including the target point.
// There are differences in how the rectangle is computed, but in all cases it is supposed to
// enclose the point's visual representation in the chart, with no extra offsets.
-export function getPointRect(point: Highcharts.Point): Rect {
+export function getPointRect(point: SafePoint): Rect {
if (!point.series) {
return { x: 0, y: 0, width: 0, height: 0 };
}
@@ -324,7 +342,7 @@ export function getPointRect(point: Highcharts.Point): Rect {
// The group rect is only used for cartesian charts. It returns coordinates of a rectangle,
// which includes all given points, but also stretched vertically or horizontally (in inverted charts)
// to the entire chart's height or width.
-export function getGroupRect(points: readonly Highcharts.Point[]): Rect {
+export function getGroupRect(points: readonly SafePoint[]): Rect {
if (points.length === 0 || !points[0].series) {
return { x: 0, y: 0, width: 0, height: 0 };
}
@@ -362,7 +380,7 @@ export function matchSizeAxis(sizeAxis: readonly CoreChartProps.SizeAxisOptions[
}
export function getPointAccessibleDescription(
- point: Highcharts.Point,
+ point: SafePoint,
labels: ChartLabels,
additionalProps?: {
sizeAxis?: readonly CoreChartProps.SizeAxisOptions[];
@@ -387,14 +405,14 @@ export function getPointAccessibleDescription(
}
}
-export function getGroupAccessibleDescription(group: readonly Highcharts.Point[]) {
+export function getGroupAccessibleDescription(group: readonly SafePoint[]) {
const firstPoint = group[0];
return getFormatter(firstPoint.series.xAxis)(firstPoint.x);
}
// The area-, line-, or scatter series markers are rendered as single graphic elements,
// and we can use their respective b-boxes to compute rects.
-function getDefaultPointRect(point: Highcharts.Point): Rect {
+function getDefaultPointRect(point: SafePoint): Rect {
const chart = point.series.chart;
if (point.graphic) {
const box = point.graphic.getBBox();
@@ -405,7 +423,7 @@ function getDefaultPointRect(point: Highcharts.Point): Rect {
// The column series graphic elements are rectangles, and they are inverted if the chart is inverted,
// so that rect's width becomes height and vice-versa.
-function getColumnPointRect(point: Highcharts.Point): Rect {
+function getColumnPointRect(point: SafePoint): Rect {
const chart = point.series.chart;
if (point.graphic) {
return getChartRect(point.graphic.getBBox(), chart, true);
@@ -415,7 +433,7 @@ function getColumnPointRect(point: Highcharts.Point): Rect {
// The errorbar series point rect cannot be computed from the respective graphic element (it gives wrong position).
// Instead, we have to rely on the internal "whiskers" element b-box, which can be inverted, too.
-function getErrorBarPointRect(point: Highcharts.Point): Rect {
+function getErrorBarPointRect(point: SafePoint): Rect {
const chart = point.series.chart;
if ("whiskers" in point) {
return getChartRect((point.whiskers as Highcharts.SVGElement).getBBox(), chart, true);
@@ -428,7 +446,7 @@ function getErrorBarPointRect(point: Highcharts.Point): Rect {
// size is small, and thereby give it 0 width and height, which is alright as there is a
// small offset that we use for focus outline anyways.
// See: https://www.highcharts.com/blog/news/175-highcharts-performance-boost/
-function getPointRectFromCoordinates(point: Highcharts.Point) {
+function getPointRectFromCoordinates(point: SafePoint) {
const chart = point.series.chart;
const plotX = point.plotX ?? 0;
const plotY = point.plotY ?? 0;
diff --git a/src/internal/utils/highcharts.ts b/src/internal/utils/highcharts.ts
index af39ec15..661fa3fd 100644
--- a/src/internal/utils/highcharts.ts
+++ b/src/internal/utils/highcharts.ts
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import type { Chart, Point, Series } from "highcharts";
+import type { Chart, Point, Series, SeriesZonesOptionsObject } from "highcharts";
// The chart.series array can include internal series, that are unwanted.
// Use getChartSeries to access chart series instead.
@@ -11,6 +11,10 @@ export type SafeChart = Omit;
// Use getSeriesData to access series data points instead.
export type SafeSeries = Omit;
+// This point object without getZone API.
+// Use getPointZone to get point's zone instead.
+export type SafePoint = Omit;
+
// isInternal is currently not a publicly supported prop
// https://github.com/highcharts/highcharts/issues/23278
interface InternalSeries extends Series {
@@ -76,3 +80,21 @@ export const getSeriesData = (series: SafeSeries, options: { includeHiddenPoints
// eslint-disable-next-line no-restricted-syntax -- This is the safe wrapper itself.
return (series as Series).data.filter((p) => (options.includeHiddenPoints ? isPointValid(p) : isPointVisible(p)));
};
+
+/**
+ * Returns point's zone safely. The Point.getZone() API can crash on series with no zones,
+ * see: https://github.com/highcharts/highcharts/issues/24633.
+ *
+ * It is not clear how to check the presence of zones safely, as zones are not represented in the public Series API.
+ */
+export function getPointZone(point: SafePoint): null | SeriesZonesOptionsObject {
+ try {
+ const seriesZones = "zones" in point.series ? point.series.zones : undefined;
+ if (seriesZones && Array.isArray(seriesZones) && seriesZones.length > 0) {
+ return (point as Point).getZone();
+ }
+ } catch {
+ // no-op
+ }
+ return null;
+}