Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 2 additions & 3 deletions pages/03-core/core-line-chart.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0

import Highcharts from "highcharts";
import { omit } from "lodash";

import Button from "@cloudscape-design/components/button";
import Link from "@cloudscape-design/components/link";
Expand Down Expand Up @@ -98,12 +97,13 @@ export default function () {
"showLegendTitle",
"showLegendActions",
"useFallback",
"tooltipSeriesSorting",
]}
/>
}
>
<CoreChart
{...omit(chartProps.cartesian, "ref")}
{...chartProps.core}
highcharts={Highcharts}
options={{
lang: {
Expand All @@ -127,7 +127,6 @@ export default function () {
},
}}
chartHeight={400}
tooltip={{ placement: "outside" }}
getTooltipContent={() => ({
point({ item, hideTooltip }) {
const value = item ? (item.point.y ?? null) : null;
Expand Down
33 changes: 33 additions & 0 deletions pages/common/page-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface PageSettings {
emphasizeBaseline: boolean;
tooltipPlacement: "default" | "middle" | "outside";
tooltipSize: "small" | "medium" | "large";
tooltipSeriesSorting: CoreChartProps.TooltipOptions["seriesSorting"];
showLegend: boolean;
showLegendTitle: boolean;
showLegendActions: boolean;
Expand Down Expand Up @@ -68,6 +69,7 @@ const DEFAULT_SETTINGS: PageSettings = {
showHeaderFilter: false,
showCustomFooter: false,
useFallback: false,
tooltipSeriesSorting: "as-added",
};

export const PageSettingsContext = createContext<PageSettings>(DEFAULT_SETTINGS);
Expand All @@ -85,6 +87,7 @@ export function useChartSettings<SettingsType extends PageSettings = PageSetting
chartProps: {
cartesian: Omit<CartesianChartProps, "series"> & { ref: React.Ref<CartesianChartProps.Ref> };
pie: Omit<PieChartProps, "series"> & { ref: React.Ref<PieChartProps.Ref> };
core: Omit<CoreChartProps, "series" | "options">;
};
isEmpty: boolean;
} {
Expand Down Expand Up @@ -178,6 +181,18 @@ export function useChartSettings<SettingsType extends PageSettings = PageSetting
tooltip: { size: settings.tooltipSize },
legend,
},
core: {
highcharts,
noData,
tooltip: {
placement: settings.tooltipPlacement === "default" ? undefined : settings.tooltipPlacement,
size: settings.tooltipSize,
seriesSorting: settings.tooltipSeriesSorting,
},
legend,
emphasizeBaseline: settings.emphasizeBaseline,
verticalAxisTitlePlacement: settings.verticalAxisTitlePlacement,
},
},
isEmpty: settings.emptySeries || settings.seriesLoading || settings.seriesError,
};
Expand All @@ -189,6 +204,11 @@ const tooltipSizeOptions = [{ value: "small" }, { value: "medium" }, { value: "l

const verticalAxisTitlePlacementOptions = [{ value: "top" }, { value: "side" }];

const tooltipSeriesSortingOptions = [
{ id: "as-added", text: "as-added" },
{ id: "by-value-desc", text: "by-value-desc" },
];

export function PageSettingsForm({
selectedSettings,
}: {
Expand Down Expand Up @@ -339,6 +359,19 @@ export function PageSettingsForm({
/>
</FormField>
);
case "tooltipSeriesSorting":
return (
<SegmentedControl
label="Tooltip Series Sorting"
selectedId={settings.tooltipSeriesSorting ?? null}
options={tooltipSeriesSortingOptions}
onChange={({ detail }) =>
setSettings({
tooltipSeriesSorting: detail.selectedId as CoreChartProps.TooltipOptions["seriesSorting"],
})
}
/>
);
case "showLegend":
return (
<Checkbox
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1764,6 +1764,20 @@ for each visited { x, y } point.",
"optional": true,
"type": "string",
},
{
"inlineType": {
"name": ""as-added" | "by-value-desc"",
"type": "union",
"valueDescriptions": undefined,
"values": [
"as-added",
"by-value-desc",
],
},
"name": "seriesSorting",
"optional": true,
"type": "string",
},
{
"inlineType": {
"name": ""small" | "medium" | "large"",
Expand Down
128 changes: 128 additions & 0 deletions src/core/__tests__/chart-core-tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -588,4 +588,132 @@ describe("CoreChart: tooltip", () => {
});
});
});

describe("series sorting", () => {
const lineSeries: Highcharts.SeriesOptionsType[] = [
{
type: "line",
name: "Line series 1",
data: [
{ x: 1, y: 11 },
{ x: 2, y: 23 },
],
},
{
type: "line",
name: "Line series 2",
data: [
{ x: 1, y: 21 },
{ x: 2, y: 11 },
],
},
];

test("maintains series order when explicitly set to 'as-added'", () => {
const { wrapper } = renderChart({
highcharts,
options: {
series: lineSeries,
},
tooltip: { seriesSorting: "as-added" },
getTooltipContent: () => ({
header: () => "Header",
body: ({ items }) => (
<div>
{items.map((item, i) => (
<div key={i} data-testid={`series-${i}`}>
{item.point.series.name}: {item.point.y}
</div>
))}
</div>
),
}),
});

act(() => hc.highlightChartPoint(0, 0));

expect(wrapper.findTooltip()).not.toBe(null);
const series0 = wrapper.findTooltip()!.find('[data-testid="series-0"]');
const series1 = wrapper.findTooltip()!.find('[data-testid="series-1"]');

expect(series0!.getElement().textContent).toBe("Line series 1: 11");
expect(series1!.getElement().textContent).toBe("Line series 2: 21");
});

describe('seriesSorting: "by-value-desc"', () => {
test("sorts series by value in descending order", () => {
const { wrapper } = renderChart({
highcharts,
options: {
series: lineSeries,
},
tooltip: { seriesSorting: "by-value-desc" },
getTooltipContent: () => ({
header: () => "Header",
body: ({ items }) => (
<div>
{items.map((item, i) => (
<div key={i} data-testid={`series-${i}`}>
{item.point.series.name}: {item.point.y}
</div>
))}
</div>
),
}),
});

act(() => hc.highlightChartPoint(0, 0));

expect(wrapper.findTooltip()).not.toBe(null);
const series0 = wrapper.findTooltip()!.find('[data-testid="series-0"]');
const series1 = wrapper.findTooltip()!.find('[data-testid="series-1"]');

expect(series0!.getElement().textContent).toBe("Line series 2: 21");
expect(series1!.getElement().textContent).toBe("Line series 1: 11");
});

test("re-sorts series when hovering different x positions", () => {
const { wrapper } = renderChart({
highcharts,
options: {
series: lineSeries,
},
tooltip: { seriesSorting: "by-value-desc" },
getTooltipContent: () => ({
header: () => "Header",
body: ({ items }) => (
<div>
{items.map((item, i) => (
<div key={i} data-testid={`series-${i}`}>
{item.point.series.name}: {item.point.y}
</div>
))}
</div>
),
}),
});

act(() => hc.highlightChartPoint(0, 0));

{
expect(wrapper.findTooltip()).not.toBe(null);
const series0 = wrapper.findTooltip()!.find('[data-testid="series-0"]');
const series1 = wrapper.findTooltip()!.find('[data-testid="series-1"]');

expect(series0!.getElement().textContent).toBe("Line series 2: 21");
expect(series1!.getElement().textContent).toBe("Line series 1: 11");
}
act(() => hc.highlightChartPoint(0, 1));

{
expect(wrapper.findTooltip()).not.toBe(null);
const series0 = wrapper.findTooltip()!.find('[data-testid="series-0"]');
const series1 = wrapper.findTooltip()!.find('[data-testid="series-1"]');

expect(series0!.getElement().textContent).toBe("Line series 1: 23");
expect(series1!.getElement().textContent).toBe("Line series 2: 11");
}
});
});
});
});
13 changes: 11 additions & 2 deletions src/core/components/core-tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function ChartTooltip({
getTooltipContent: getTooltipContentOverrides,
api,
i18nStrings,
seriesSorting = "as-added",
}: CoreChartProps.TooltipOptions & {
i18nStrings?: BaseI18nStrings;
getTooltipContent?: CoreChartProps.GetTooltipContent;
Expand All @@ -70,6 +71,7 @@ export function ChartTooltip({
group: tooltip.group,
expandedSeries,
setExpandedSeries,
seriesSorting,
hideTooltip: () => {
api.hideTooltip();
},
Expand Down Expand Up @@ -121,6 +123,7 @@ function getTooltipContent(
props: CoreChartProps.GetTooltipContentProps & {
renderers?: CoreChartProps.TooltipContentRenderer;
hideTooltip: () => void;
seriesSorting: NonNullable<CoreChartProps.TooltipOptions["seriesSorting"]>;
} & ExpandedSeriesStateProps,
): null | RenderedTooltipContent {
if (props.point && props.point.series.type === "pie") {
Expand All @@ -141,9 +144,11 @@ function getTooltipContentCartesian(
renderers = {},
setExpandedSeries,
hideTooltip,
seriesSorting,
}: CoreChartProps.GetTooltipContentProps & {
renderers?: CoreChartProps.TooltipContentRenderer;
hideTooltip: () => void;
seriesSorting: NonNullable<CoreChartProps.TooltipOptions["seriesSorting"]>;
} & ExpandedSeriesStateProps,
): RenderedTooltipContent {
// The cartesian tooltip might or might not have a selected point, but it always has a non-empty group.
Expand All @@ -152,7 +157,7 @@ function getTooltipContentCartesian(
const chart = group[0].series.chart;
const getSeriesMarker = (series: Highcharts.Series) =>
api.renderMarker(getSeriesMarkerType(series), getSeriesColor(series), true);
const matchedItems = findTooltipSeriesItems(getChartSeries(chart.series), group);
const matchedItems = findTooltipSeriesItems(getChartSeries(chart.series), group, seriesSorting);
const detailItems: ChartSeriesDetailItem[] = matchedItems.map((item) => {
const valueFormatter = getFormatter(item.point.series.yAxis);
const itemY = isXThreshold(item.point.series) ? null : (item.point.y ?? null);
Expand Down Expand Up @@ -260,6 +265,7 @@ function getTooltipContentPie(
function findTooltipSeriesItems(
series: readonly Highcharts.Series[],
group: readonly Highcharts.Point[],
seriesSorting: NonNullable<CoreChartProps.TooltipOptions["seriesSorting"]>,
): MatchedItem[] {
const seriesOrder = series.reduce((d, s, i) => d.set(s, i), new Map<Highcharts.Series, number>());
const getSeriesIndex = (s: Highcharts.Series) => seriesOrder.get(s) ?? -1;
Expand Down Expand Up @@ -304,8 +310,11 @@ function findTooltipSeriesItems(
}
return (
matchedItems
// We sort matched items by series order. If there are multiple items that belong to the same series, we sort them by value.
.sort((i1, i2) => {
if (seriesSorting === "by-value-desc") {
return (i2.point.y ?? 0) - (i1.point.y ?? 0);
}
// We sort matched items by series order. If there are multiple items that belong to the same series, we sort them by value.
const s1 = getSeriesIndex(i1.point.series) - getSeriesIndex(i2.point.series);
return s1 || (i1.point.y ?? 0) - (i2.point.y ?? 0);
})
Expand Down
1 change: 1 addition & 0 deletions src/core/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ export namespace CoreChartProps {
enabled?: boolean;
placement?: "middle" | "outside" | "target";
size?: "small" | "medium" | "large";
seriesSorting?: "as-added" | "by-value-desc";
}

export type GetTooltipContent = (props: GetTooltipContentProps) => TooltipContentRenderer;
Expand Down