forked from cloudscape-design/chart-components
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchart-extra-legend.tsx
More file actions
166 lines (147 loc) · 6.73 KB
/
chart-extra-legend.tsx
File metadata and controls
166 lines (147 loc) · 6.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
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";
import { isEqualArrays } from "../../internal/utils/utils";
import { CoreChartProps } from "../interfaces";
import { getChartLegendItems, getPointId, getSeriesId } from "../utils";
import { ChartExtraContext } from "./chart-extra-context";
// The reactive state is used to propagate changes in legend items to the core legend React component.
export interface ReactiveLegendState {
items: readonly CoreChartProps.LegendItem[];
}
// Chart helper that implements custom legend behaviors.
export class ChartExtraLegend extends AsyncStore<ReactiveLegendState> {
private context: ChartExtraContext;
private visibilityMode: "internal" | "external" = "external";
constructor(context: ChartExtraContext) {
super({ items: [] });
this.context = context;
}
public onChartRender = () => {
this.initLegend();
this.updateItemsVisibility();
};
// If visible items are explicitly provided, we use them to update visibility of chart's series or points (by ID).
// If not provided, the visibility state is managed internally.
public updateItemsVisibility = () => {
if (this.context.state.visibleItems) {
this.visibilityMode = "external";
updateItemsVisibility(this.context.chart(), this.get().items, this.context.state.visibleItems);
} else {
this.visibilityMode = "internal";
}
};
// A callback to be called when items visibility changes from the outside or from the legend.
public onItemVisibilityChange = (visibleItems: readonly string[], { isApiCall }: { isApiCall: boolean }) => {
const currentItems = this.get().items;
const updatedItems = currentItems.map((i) => ({ ...i, visible: visibleItems.includes(i.id) }));
if (this.visibilityMode === "internal") {
this.updateLegendItems(updatedItems);
updateItemsVisibility(this.context.chart(), this.get().items, visibleItems);
}
fireNonCancelableEvent(this.context.handlers.onVisibleItemsChange, { items: updatedItems, isApiCall });
};
// Updates legend highlight state when chart's point is highlighted.
public onHighlightPoint = (point: Highcharts.Point) => {
const visibleItems = point.series.type === "pie" ? [getPointId(point)] : [getSeriesId(point.series)];
this.onHighlightItems(visibleItems);
};
// Updates legend highlight state when chart's group of points is highlighted.
public onHighlightGroup = (group: readonly Highcharts.Point[]) => {
const visibleItems = group.map((point) => getSeriesId(point.series));
this.onHighlightItems(visibleItems);
};
// Updates legend highlight state given an explicit list of item IDs. This is used to update state
// when a legend item gets hovered or focused.
public onHighlightItems = (highlightedItems: readonly string[]) => {
const currentItems = this.get().items;
const updatedItems = currentItems.map(({ ...i }) => ({ ...i, highlighted: highlightedItems.includes(i.id) }));
this.updateLegendItems(updatedItems);
};
// Clears legend highlight state.
public onClearHighlight = () => {
const nextItems = this.get().items.map(({ ...item }) => ({ ...item, highlighted: false }));
this.updateLegendItems(nextItems);
};
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(),
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);
};
private updateLegendItems = (nextItems: CoreChartProps.LegendItem[]) => {
function isLegendItemsEqual(a: CoreChartProps.LegendItem, b: CoreChartProps.LegendItem) {
return (
a.id === b.id &&
a.name === b.name &&
a.marker === b.marker &&
a.visible === b.visible &&
a.highlighted === b.highlighted
);
}
if (!isEqualArrays(this.get().items, nextItems, isLegendItemsEqual)) {
this.set(() => ({ items: nextItems }));
}
};
// 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,
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;
}
}
function updateItemsVisibility(
chart: Highcharts.Chart,
legendItems: readonly CoreChartProps.LegendItem[],
visibleItems?: readonly string[],
) {
const availableItemsSet = new Set(legendItems.map((i) => i.id));
const visibleItemsSet = new Set(visibleItems);
let updatesCounter = 0;
const getVisibleAndCount = (id: string, visible: boolean) => {
const nextVisible = visibleItemsSet.has(id);
updatesCounter += nextVisible !== visible ? 1 : 0;
return nextVisible;
};
for (const series of getChartSeries(chart.series)) {
if (availableItemsSet.has(getSeriesId(series))) {
series.setVisible(getVisibleAndCount(getSeriesId(series), series.visible), false);
}
for (const point of series.data) {
if (typeof point.setVisible === "function" && availableItemsSet.has(getPointId(point))) {
point.setVisible(getVisibleAndCount(getPointId(point), point.visible), false);
}
}
}
// The call `seriesOrPoint.setVisible(visible, false)` does not trigger the chart redraw, as it would otherwise
// impact the performance. Instead, we trigger the redraw explicitly, if any change to visibility has been made.
if (updatesCounter > 0) {
chart.redraw();
}
}