@octanejs/recharts 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/animation/CSSTransitionAnimate.tsrx +142 -0
- package/src/cartesian/Area.tsrx +1096 -0
- package/src/cartesian/Area.tsrx.d.ts +340 -0
- package/src/cartesian/AreaRevealShape.tsrx +202 -0
- package/src/cartesian/CartesianGrid.tsrx +643 -0
- package/src/cartesian/CartesianGrid.tsrx.d.ts +219 -0
- package/src/cartesian/ErrorBar.tsrx +322 -0
- package/src/cartesian/ErrorBar.tsrx.d.ts +131 -0
- package/src/cartesian/Funnel.tsrx +833 -0
- package/src/cartesian/Funnel.tsrx.d.ts +274 -0
- package/src/cartesian/ReferenceLine.tsrx +452 -0
- package/src/cartesian/ReferenceLine.tsrx.d.ts +178 -0
- package/src/cartesian/Scatter.tsrx +1076 -0
- package/src/cartesian/Scatter.tsrx.d.ts +351 -0
- package/src/cartesian/ZAxis.tsrx +148 -0
- package/src/cartesian/ZAxis.tsrx.d.ts +102 -0
- package/src/chart/AreaChart.tsrx +22 -0
- package/src/chart/AreaChart.tsrx.d.ts +12 -0
- package/src/chart/ComposedChart.tsrx +22 -0
- package/src/chart/ComposedChart.tsrx.d.ts +12 -0
- package/src/chart/FunnelChart.tsrx +22 -0
- package/src/chart/FunnelChart.tsrx.d.ts +12 -0
- package/src/chart/PieChart.tsrx +31 -0
- package/src/chart/PieChart.tsrx.d.ts +31 -0
- package/src/chart/PolarChart.tsrx +121 -0
- package/src/chart/RadarChart.tsrx +53 -0
- package/src/chart/RadarChart.tsrx.d.ts +31 -0
- package/src/chart/RadialBarChart.tsrx +31 -0
- package/src/chart/RadialBarChart.tsrx.d.ts +31 -0
- package/src/chart/Sankey.tsrx +1542 -0
- package/src/chart/Sankey.tsrx.d.ts +253 -0
- package/src/chart/ScatterChart.tsrx +22 -0
- package/src/chart/ScatterChart.tsrx.d.ts +12 -0
- package/src/chart/SunburstChart.tsrx +495 -0
- package/src/chart/SunburstChart.tsrx.d.ts +146 -0
- package/src/chart/types.ts +19 -0
- package/src/component/Cursor.tsrx +157 -0
- package/src/component/DefaultLegendContent.tsrx +288 -0
- package/src/component/DefaultLegendContent.tsrx.d.ts +125 -0
- package/src/component/DefaultTooltipContent.tsrx +215 -0
- package/src/component/DefaultTooltipContent.tsrx.d.ts +87 -0
- package/src/component/Legend.tsrx +302 -0
- package/src/component/Legend.tsrx.d.ts +88 -0
- package/src/component/ResponsiveContainer.ts +1 -22
- package/src/component/ResponsiveContainer.tsrx +171 -0
- package/src/component/ResponsiveContainer.tsrx.d.ts +81 -0
- package/src/component/Tooltip.tsrx +425 -0
- package/src/component/Tooltip.tsrx.d.ts +246 -0
- package/src/component/TooltipBoundingBox.tsrx +135 -0
- package/src/context/CellsContext.ts +12 -0
- package/src/context/legendPayloadContext.tsrx +11 -0
- package/src/context/useTooltipAxis.ts +20 -0
- package/src/index.d.ts +236 -0
- package/src/index.ts +67 -0
- package/src/polar/Pie.tsrx +1224 -0
- package/src/polar/Pie.tsrx.d.ts +436 -0
- package/src/polar/PolarAngleAxis.tsrx +554 -0
- package/src/polar/PolarAngleAxis.tsrx.d.ts +215 -0
- package/src/polar/PolarGrid.tsrx +317 -0
- package/src/polar/PolarGrid.tsrx.d.ts +117 -0
- package/src/polar/PolarRadiusAxis.tsrx +463 -0
- package/src/polar/PolarRadiusAxis.tsrx.d.ts +209 -0
- package/src/polar/Radar.tsrx +696 -0
- package/src/polar/Radar.tsrx.d.ts +215 -0
- package/src/polar/RadialBar.tsrx +887 -0
- package/src/polar/RadialBar.tsrx.d.ts +340 -0
- package/src/state/ReportPolarOptions.tsrx +11 -0
- package/src/state/selectors/areaSelectors.ts +257 -0
- package/src/state/selectors/funnelSelectors.ts +86 -0
- package/src/state/selectors/pieSelectors.ts +134 -0
- package/src/state/selectors/polarGridSelectors.ts +43 -0
- package/src/state/selectors/polarScaleSelectors.ts +159 -0
- package/src/state/selectors/polarSelectors.ts +198 -0
- package/src/state/selectors/radarSelectors.ts +228 -0
- package/src/state/selectors/radialBarSelectors.ts +511 -0
- package/src/state/selectors/scatterSelectors.ts +168 -0
- package/src/util/FunnelUtils.tsrx +19 -0
- package/src/util/RadialBarUtils.tsrx +29 -0
- package/src/util/ScatterUtils.tsrx +34 -0
- package/src/util/cursor/getCursorPoints.ts +45 -0
- package/src/util/cursor/getCursorRectangle.ts +28 -0
- package/src/util/cursor/getRadialCursorPoints.ts +31 -0
- package/src/util/payload/getUniqPayload.ts +28 -0
- package/src/util/scale/CartesianScaleHelper.ts +71 -0
- package/src/util/tooltip/translate.ts +168 -0
- package/src/util/types.ts +3 -0
- package/src/util/useElementOffset.ts +147 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview Default Tooltip Content
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'octane';
|
|
6
|
+
import { CSSProperties, HTMLAttributes, ReactNode, SVGProps, type OctaneNode } from 'octane';
|
|
7
|
+
import sortBy from 'es-toolkit/compat/sortBy';
|
|
8
|
+
import { clsx } from 'clsx';
|
|
9
|
+
import { isNullish, isNumOrStr } from '../util/DataUtils';
|
|
10
|
+
import { DataKey } from '../util/types';
|
|
11
|
+
import { TooltipPayload, TooltipPayloadEntry } from '../state/tooltipSlice';
|
|
12
|
+
|
|
13
|
+
function defaultFormatter(value: ValueType | undefined): OctaneNode {
|
|
14
|
+
return Array.isArray(value) && isNumOrStr(value[0]) && isNumOrStr(value[1])
|
|
15
|
+
? value.join(' ~ ')
|
|
16
|
+
: value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type TooltipType = 'none';
|
|
20
|
+
export type ValueType = number | string | ReadonlyArray<number | string>;
|
|
21
|
+
export type NameType = number | string;
|
|
22
|
+
export type Formatter<TValue extends ValueType = ValueType, TName extends NameType = NameType> =
|
|
23
|
+
(value: TValue | undefined, name: TName | undefined, item: Payload<
|
|
24
|
+
TValue,
|
|
25
|
+
TName
|
|
26
|
+
>, index: number, payload: ReadonlyArray<Payload<TValue, TName>>) => | [OctaneNode, TName]
|
|
27
|
+
| OctaneNode;
|
|
28
|
+
|
|
29
|
+
export interface Payload<
|
|
30
|
+
TValue extends ValueType = ValueType,
|
|
31
|
+
TName extends NameType = NameType,
|
|
32
|
+
> extends Omit<SVGProps<SVGElement>, 'name'> {
|
|
33
|
+
type?: TooltipType;
|
|
34
|
+
color?: string;
|
|
35
|
+
formatter?: Formatter<TValue, TName>;
|
|
36
|
+
name?: TName;
|
|
37
|
+
value?: TValue;
|
|
38
|
+
unit?: ReactNode;
|
|
39
|
+
fill?: string;
|
|
40
|
+
dataKey?: DataKey<any>;
|
|
41
|
+
nameKey?: DataKey<any>;
|
|
42
|
+
payload?: any;
|
|
43
|
+
chartType?: string;
|
|
44
|
+
stroke?: string;
|
|
45
|
+
strokeDasharray?: string | number;
|
|
46
|
+
strokeWidth?: number | string;
|
|
47
|
+
className?: string;
|
|
48
|
+
hide?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The id of the graphical item that the data point belongs to
|
|
51
|
+
*/
|
|
52
|
+
graphicalItemId: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @inline
|
|
57
|
+
*/
|
|
58
|
+
export type TooltipItemSorter<
|
|
59
|
+
TValue extends ValueType = ValueType,
|
|
60
|
+
TName extends NameType = NameType,
|
|
61
|
+
> =
|
|
62
|
+
'dataKey' | 'value' | 'name' | ((item: Payload<TValue, TName>) => number | string | undefined);
|
|
63
|
+
|
|
64
|
+
export interface Props<TValue extends ValueType = ValueType, TName extends NameType = NameType> {
|
|
65
|
+
separator?: string;
|
|
66
|
+
wrapperClassName?: string;
|
|
67
|
+
labelClassName?: string;
|
|
68
|
+
formatter?: Formatter<TValue, TName>;
|
|
69
|
+
contentStyle?: CSSProperties;
|
|
70
|
+
itemStyle?: CSSProperties;
|
|
71
|
+
labelStyle?: CSSProperties;
|
|
72
|
+
labelFormatter?: (label: ReactNode, payload: ReadonlyArray<Payload<TValue, TName>>) => ReactNode;
|
|
73
|
+
label?: ReactNode;
|
|
74
|
+
payload?: ReadonlyArray<Payload<TValue, TName>>;
|
|
75
|
+
itemSorter?: TooltipItemSorter<TValue, TName>;
|
|
76
|
+
accessibilityLayer?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const defaultDefaultTooltipContentProps = {
|
|
80
|
+
separator: ' : ',
|
|
81
|
+
contentStyle: {
|
|
82
|
+
margin: 0,
|
|
83
|
+
padding: 10,
|
|
84
|
+
backgroundColor: '#fff',
|
|
85
|
+
border: '1px solid #ccc',
|
|
86
|
+
whiteSpace: 'nowrap',
|
|
87
|
+
},
|
|
88
|
+
itemStyle: {
|
|
89
|
+
display: 'block',
|
|
90
|
+
paddingTop: 4,
|
|
91
|
+
paddingBottom: 4,
|
|
92
|
+
color: '#000',
|
|
93
|
+
},
|
|
94
|
+
labelStyle: {},
|
|
95
|
+
accessibilityLayer: false,
|
|
96
|
+
} as const satisfies Partial<Props<any, any>>;
|
|
97
|
+
|
|
98
|
+
function lodashLikeSortBy<T>(
|
|
99
|
+
array: ReadonlyArray<T>,
|
|
100
|
+
itemSorter: TooltipItemSorter | undefined,
|
|
101
|
+
): ReadonlyArray<T> {
|
|
102
|
+
if (itemSorter == null) {
|
|
103
|
+
return array;
|
|
104
|
+
}
|
|
105
|
+
// @ts-expect-error sortBy types somehow are returning a number type.
|
|
106
|
+
return sortBy(array, itemSorter);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* This component is by default rendered inside the {@link Tooltip} component. You would not use it directly.
|
|
111
|
+
*
|
|
112
|
+
* You can use this component to customize the content of the tooltip,
|
|
113
|
+
* or you can provide your own completely independent content.
|
|
114
|
+
*/
|
|
115
|
+
export const DefaultTooltipContent = (props: Props) => {
|
|
116
|
+
const {
|
|
117
|
+
separator = defaultDefaultTooltipContentProps.separator,
|
|
118
|
+
contentStyle,
|
|
119
|
+
itemStyle,
|
|
120
|
+
labelStyle = defaultDefaultTooltipContentProps.labelStyle,
|
|
121
|
+
payload,
|
|
122
|
+
formatter,
|
|
123
|
+
itemSorter,
|
|
124
|
+
wrapperClassName,
|
|
125
|
+
labelClassName,
|
|
126
|
+
label,
|
|
127
|
+
labelFormatter,
|
|
128
|
+
accessibilityLayer = defaultDefaultTooltipContentProps.accessibilityLayer,
|
|
129
|
+
} = props;
|
|
130
|
+
|
|
131
|
+
const renderContent = () => {
|
|
132
|
+
if (payload && payload.length) {
|
|
133
|
+
const listStyle = { padding: 0, margin: 0 };
|
|
134
|
+
|
|
135
|
+
const sortedPayload: TooltipPayload = lodashLikeSortBy(payload, itemSorter);
|
|
136
|
+
const items = sortedPayload.map((entry: TooltipPayloadEntry, i: number) => {
|
|
137
|
+
if (!entry || entry.type === 'none') {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const finalFormatter: Formatter = entry.formatter || formatter || defaultFormatter;
|
|
142
|
+
const { value, name } = entry;
|
|
143
|
+
let finalValue: OctaneNode = value;
|
|
144
|
+
let finalName: OctaneNode = name;
|
|
145
|
+
if (finalFormatter) {
|
|
146
|
+
const formatted = finalFormatter(value, name, entry, i, payload);
|
|
147
|
+
if (Array.isArray(formatted)) {
|
|
148
|
+
[finalValue, finalName] = formatted;
|
|
149
|
+
} else if (formatted != null) {
|
|
150
|
+
finalValue = formatted;
|
|
151
|
+
} else {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const finalItemStyle = {
|
|
157
|
+
...defaultDefaultTooltipContentProps.itemStyle,
|
|
158
|
+
color: entry.color || defaultDefaultTooltipContentProps.itemStyle.color,
|
|
159
|
+
...itemStyle,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return <li
|
|
163
|
+
className="recharts-tooltip-item"
|
|
164
|
+
key={`tooltip-item-${i}`}
|
|
165
|
+
style={finalItemStyle}
|
|
166
|
+
>
|
|
167
|
+
{isNumOrStr(finalName)
|
|
168
|
+
? <span className="recharts-tooltip-item-name">{finalName}</span>
|
|
169
|
+
: null}
|
|
170
|
+
{isNumOrStr(finalName)
|
|
171
|
+
? <span className="recharts-tooltip-item-separator">{separator}</span>
|
|
172
|
+
: null}
|
|
173
|
+
<span className="recharts-tooltip-item-value">{finalValue}</span>
|
|
174
|
+
<span className="recharts-tooltip-item-unit">{entry.unit || ''}</span>
|
|
175
|
+
</li>;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return <ul className="recharts-tooltip-item-list" style={listStyle}>{items}</ul>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const finalStyle: React.CSSProperties = {
|
|
185
|
+
...defaultDefaultTooltipContentProps.contentStyle,
|
|
186
|
+
...contentStyle,
|
|
187
|
+
};
|
|
188
|
+
const finalLabelStyle = {
|
|
189
|
+
margin: 0,
|
|
190
|
+
...labelStyle,
|
|
191
|
+
};
|
|
192
|
+
const hasLabel = !isNullish(label);
|
|
193
|
+
let finalLabel: ReactNode = hasLabel ? label : '';
|
|
194
|
+
const wrapperCN = clsx('recharts-default-tooltip', wrapperClassName);
|
|
195
|
+
const labelCN = clsx('recharts-tooltip-label', labelClassName);
|
|
196
|
+
|
|
197
|
+
if (hasLabel && labelFormatter && payload !== undefined && payload !== null) {
|
|
198
|
+
finalLabel = labelFormatter(label, payload);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const accessibilityAttributes =
|
|
202
|
+
accessibilityLayer
|
|
203
|
+
? {
|
|
204
|
+
role: 'status',
|
|
205
|
+
'aria-live': 'assertive',
|
|
206
|
+
} as HTMLAttributes<HTMLDivElement>
|
|
207
|
+
: {};
|
|
208
|
+
|
|
209
|
+
return <div className={wrapperCN} style={finalStyle} {...accessibilityAttributes}>
|
|
210
|
+
<p className={labelCN} style={finalLabelStyle}>
|
|
211
|
+
{React.isValidElement(finalLabel) ? finalLabel : `${finalLabel}`}
|
|
212
|
+
</p>
|
|
213
|
+
{renderContent()}
|
|
214
|
+
</div>;
|
|
215
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview Default Tooltip Content
|
|
3
|
+
*/
|
|
4
|
+
import * as React from 'octane';
|
|
5
|
+
import { CSSProperties, ReactNode, SVGProps } from 'octane';
|
|
6
|
+
import { DataKey } from '../util/types';
|
|
7
|
+
export type TooltipType = 'none';
|
|
8
|
+
export type ValueType = number | string | ReadonlyArray<number | string>;
|
|
9
|
+
export type NameType = number | string;
|
|
10
|
+
export type Formatter<TValue extends ValueType = ValueType, TName extends NameType = NameType> = (
|
|
11
|
+
value: TValue | undefined,
|
|
12
|
+
name: TName | undefined,
|
|
13
|
+
item: Payload<TValue, TName>,
|
|
14
|
+
index: number,
|
|
15
|
+
payload: ReadonlyArray<Payload<TValue, TName>>,
|
|
16
|
+
) => [React.ReactNode, TName] | React.ReactNode;
|
|
17
|
+
export interface Payload<
|
|
18
|
+
TValue extends ValueType = ValueType,
|
|
19
|
+
TName extends NameType = NameType,
|
|
20
|
+
> extends Omit<SVGProps<SVGElement>, 'name'> {
|
|
21
|
+
type?: TooltipType;
|
|
22
|
+
color?: string;
|
|
23
|
+
formatter?: Formatter<TValue, TName>;
|
|
24
|
+
name?: TName;
|
|
25
|
+
value?: TValue;
|
|
26
|
+
unit?: ReactNode;
|
|
27
|
+
fill?: string;
|
|
28
|
+
dataKey?: DataKey<any>;
|
|
29
|
+
nameKey?: DataKey<any>;
|
|
30
|
+
payload?: any;
|
|
31
|
+
chartType?: string;
|
|
32
|
+
stroke?: string;
|
|
33
|
+
strokeDasharray?: string | number;
|
|
34
|
+
strokeWidth?: number | string;
|
|
35
|
+
className?: string;
|
|
36
|
+
hide?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The id of the graphical item that the data point belongs to
|
|
39
|
+
*/
|
|
40
|
+
graphicalItemId: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @inline
|
|
44
|
+
*/
|
|
45
|
+
export type TooltipItemSorter<
|
|
46
|
+
TValue extends ValueType = ValueType,
|
|
47
|
+
TName extends NameType = NameType,
|
|
48
|
+
> = 'dataKey' | 'value' | 'name' | ((item: Payload<TValue, TName>) => number | string | undefined);
|
|
49
|
+
export interface Props<TValue extends ValueType = ValueType, TName extends NameType = NameType> {
|
|
50
|
+
separator?: string;
|
|
51
|
+
wrapperClassName?: string;
|
|
52
|
+
labelClassName?: string;
|
|
53
|
+
formatter?: Formatter<TValue, TName>;
|
|
54
|
+
contentStyle?: CSSProperties;
|
|
55
|
+
itemStyle?: CSSProperties;
|
|
56
|
+
labelStyle?: CSSProperties;
|
|
57
|
+
labelFormatter?: (label: ReactNode, payload: ReadonlyArray<Payload<TValue, TName>>) => ReactNode;
|
|
58
|
+
label?: ReactNode;
|
|
59
|
+
payload?: ReadonlyArray<Payload<TValue, TName>>;
|
|
60
|
+
itemSorter?: TooltipItemSorter<TValue, TName>;
|
|
61
|
+
accessibilityLayer?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare const defaultDefaultTooltipContentProps: {
|
|
64
|
+
readonly separator: ' : ';
|
|
65
|
+
readonly contentStyle: {
|
|
66
|
+
readonly margin: 0;
|
|
67
|
+
readonly padding: 10;
|
|
68
|
+
readonly backgroundColor: '#fff';
|
|
69
|
+
readonly border: '1px solid #ccc';
|
|
70
|
+
readonly whiteSpace: 'nowrap';
|
|
71
|
+
};
|
|
72
|
+
readonly itemStyle: {
|
|
73
|
+
readonly display: 'block';
|
|
74
|
+
readonly paddingTop: 4;
|
|
75
|
+
readonly paddingBottom: 4;
|
|
76
|
+
readonly color: '#000';
|
|
77
|
+
};
|
|
78
|
+
readonly labelStyle: {};
|
|
79
|
+
readonly accessibilityLayer: false;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* This component is by default rendered inside the {@link Tooltip} component. You would not use it directly.
|
|
83
|
+
*
|
|
84
|
+
* You can use this component to customize the content of the tooltip,
|
|
85
|
+
* or you can provide your own completely independent content.
|
|
86
|
+
*/
|
|
87
|
+
export declare const DefaultTooltipContent: (props: Props) => React.JSX.Element;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as React from 'octane';
|
|
2
|
+
import { CSSProperties, useLayoutEffect } from 'octane';
|
|
3
|
+
import { createPortal } from 'octane';
|
|
4
|
+
import { useLegendPortal } from '../context/legendPortalContext';
|
|
5
|
+
import {
|
|
6
|
+
ContentType,
|
|
7
|
+
DefaultLegendContent,
|
|
8
|
+
LegendPayload,
|
|
9
|
+
Props as DefaultLegendContentProps,
|
|
10
|
+
VerticalAlignmentType,
|
|
11
|
+
} from './DefaultLegendContent';
|
|
12
|
+
|
|
13
|
+
import { LayoutType, Margin, Size } from '../util/types';
|
|
14
|
+
import { getUniqPayload, UniqueOption } from '../util/payload/getUniqPayload';
|
|
15
|
+
import { useLegendPayload } from '../context/legendPayloadContext';
|
|
16
|
+
import { ElementOffset, useElementOffset } from '../util/useElementOffset';
|
|
17
|
+
import { useChartHeight, useChartWidth, useMargin } from '../context/chartLayoutContext';
|
|
18
|
+
import { LegendSettings, setLegendSettings, setLegendSize } from '../state/legendSlice';
|
|
19
|
+
import { useAppDispatch } from '../state/hooks';
|
|
20
|
+
import { resolveDefaultProps } from '../util/resolveDefaultProps';
|
|
21
|
+
import { propsAreEqual } from '../util/propsAreEqual';
|
|
22
|
+
|
|
23
|
+
function defaultUniqBy(entry: LegendPayload) {
|
|
24
|
+
return entry.value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ContentProps =
|
|
28
|
+
Props & {
|
|
29
|
+
margin: Margin | undefined;
|
|
30
|
+
chartWidth: number;
|
|
31
|
+
chartHeight: number;
|
|
32
|
+
contextPayload: ReadonlyArray<LegendPayload>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function LegendContent(props: ContentProps) {
|
|
36
|
+
const { contextPayload, ...otherProps } = props;
|
|
37
|
+
const finalPayload = getUniqPayload(contextPayload, props.payloadUniqBy, defaultUniqBy);
|
|
38
|
+
const contentProps = {
|
|
39
|
+
...otherProps,
|
|
40
|
+
payload: finalPayload,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (React.isValidElement(props.content)) {
|
|
44
|
+
return React.cloneElement(props.content, contentProps);
|
|
45
|
+
}
|
|
46
|
+
if (typeof props.content === 'function') {
|
|
47
|
+
return React.createElement(props.content as any, contentProps);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return <DefaultLegendContent {...contentProps} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type PositionInput = {
|
|
54
|
+
layout?: Props['layout'];
|
|
55
|
+
align?: Props['align'];
|
|
56
|
+
verticalAlign?: Props['verticalAlign'];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function getDefaultPosition(
|
|
60
|
+
style: CSSProperties | undefined,
|
|
61
|
+
props: PositionInput,
|
|
62
|
+
margin: Margin | undefined,
|
|
63
|
+
chartWidth: number,
|
|
64
|
+
chartHeight: number,
|
|
65
|
+
box: ElementOffset,
|
|
66
|
+
) {
|
|
67
|
+
const { layout, align, verticalAlign } = props;
|
|
68
|
+
let hPos, vPos;
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
!style ||
|
|
72
|
+
(style.left === undefined || style.left === null) &&
|
|
73
|
+
(style.right === undefined || style.right === null)
|
|
74
|
+
) {
|
|
75
|
+
if (align === 'center' && layout === 'vertical') {
|
|
76
|
+
hPos = { left: ((chartWidth || 0) - box.width) / 2 };
|
|
77
|
+
} else {
|
|
78
|
+
hPos = align === 'right'
|
|
79
|
+
? { right: margin && margin.right || 0 }
|
|
80
|
+
: { left: margin && margin.left || 0 };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
!style ||
|
|
86
|
+
(style.top === undefined || style.top === null) &&
|
|
87
|
+
(style.bottom === undefined || style.bottom === null)
|
|
88
|
+
) {
|
|
89
|
+
if (verticalAlign === 'middle') {
|
|
90
|
+
vPos = { top: ((chartHeight || 0) - box.height) / 2 };
|
|
91
|
+
} else {
|
|
92
|
+
vPos = verticalAlign === 'bottom'
|
|
93
|
+
? { bottom: margin && margin.bottom || 0 }
|
|
94
|
+
: { top: margin && margin.top || 0 };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { ...hPos, ...vPos };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type LegendItemSorter = 'value' | 'dataKey' | ((item: LegendPayload) => number | string);
|
|
102
|
+
|
|
103
|
+
export type Props =
|
|
104
|
+
Omit<DefaultLegendContentProps, 'payload' | 'ref' | 'verticalAlign'> & {
|
|
105
|
+
/**
|
|
106
|
+
* Renders the content of the legend.
|
|
107
|
+
*
|
|
108
|
+
* This should return HTML elements, not SVG elements.
|
|
109
|
+
*
|
|
110
|
+
* - If not set, the {@link DefaultLegendContent} component is used.
|
|
111
|
+
* - If set to a React element, this element will be cloned and extra props will be passed in.
|
|
112
|
+
* - If set to a function, the function will be called and should return HTML elements.
|
|
113
|
+
*
|
|
114
|
+
* @example <Legend content={CustomizedLegend} />
|
|
115
|
+
* @example <Legend content={renderLegend} />
|
|
116
|
+
*/
|
|
117
|
+
content?: ContentType;
|
|
118
|
+
/**
|
|
119
|
+
* CSS styles to be applied to the wrapper `div` element.
|
|
120
|
+
*/
|
|
121
|
+
wrapperStyle?: CSSProperties;
|
|
122
|
+
/**
|
|
123
|
+
* Width of the legend.
|
|
124
|
+
* Accept CSS style string values like `100%` or `fit-content`, or number values like `400`.
|
|
125
|
+
*/
|
|
126
|
+
width?: number | string;
|
|
127
|
+
/**
|
|
128
|
+
* Height of the legend.
|
|
129
|
+
* Accept CSS style string values like `100%` or `fit-content`, or number values like `400`.
|
|
130
|
+
*/
|
|
131
|
+
height?: number | string;
|
|
132
|
+
payloadUniqBy?: UniqueOption<LegendPayload>;
|
|
133
|
+
onBBoxUpdate?: (box: ElementOffset | null) => void;
|
|
134
|
+
/**
|
|
135
|
+
* If portal is defined, then Legend will use this element as a target
|
|
136
|
+
* for rendering using React Portal.
|
|
137
|
+
*
|
|
138
|
+
* If this is undefined then Legend renders inside the recharts-wrapper element.
|
|
139
|
+
*
|
|
140
|
+
* @see {@link https://react.dev/reference/react-dom/createPortal}
|
|
141
|
+
*/
|
|
142
|
+
portal?: HTMLElement | null;
|
|
143
|
+
/**
|
|
144
|
+
* Sorts Legend items. Defaults to `value` which means it will sort alphabetically
|
|
145
|
+
* by the label.
|
|
146
|
+
*
|
|
147
|
+
* If `null` is provided then the payload is not sorted. Be aware that without sort,
|
|
148
|
+
* the order of items may change between renders!
|
|
149
|
+
*
|
|
150
|
+
* @defaultValue value
|
|
151
|
+
*/
|
|
152
|
+
itemSorter?: LegendItemSorter | null;
|
|
153
|
+
/**
|
|
154
|
+
* The alignment of the whole Legend container:
|
|
155
|
+
*
|
|
156
|
+
* - `bottom`: shows the Legend below chart, and chart height reduces automatically to make space for it.
|
|
157
|
+
* - `top`: shows the Legend above chart, and chart height reduces automatically.
|
|
158
|
+
* - `middle`: shows the Legend in the middle of chart, covering other content, and chart height remains unchanged.
|
|
159
|
+
* The exact behavior changes depending on `align` prop.
|
|
160
|
+
*
|
|
161
|
+
* @defaultValue bottom
|
|
162
|
+
*/
|
|
163
|
+
verticalAlign?: VerticalAlignmentType;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
function LegendSettingsDispatcher({
|
|
167
|
+
align,
|
|
168
|
+
layout,
|
|
169
|
+
verticalAlign,
|
|
170
|
+
itemSorter,
|
|
171
|
+
}: LegendSettings): null {
|
|
172
|
+
const dispatch = useAppDispatch();
|
|
173
|
+
useLayoutEffect(() => {
|
|
174
|
+
dispatch(setLegendSettings({ align, layout, verticalAlign, itemSorter }));
|
|
175
|
+
}, [dispatch, align, layout, verticalAlign, itemSorter]);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function LegendSizeDispatcher({ width, height }: Size): null {
|
|
180
|
+
const dispatch = useAppDispatch();
|
|
181
|
+
useLayoutEffect(() => {
|
|
182
|
+
dispatch(setLegendSize({ width, height }));
|
|
183
|
+
}, [dispatch, width, height]);
|
|
184
|
+
|
|
185
|
+
useLayoutEffect(() => {
|
|
186
|
+
return () => {
|
|
187
|
+
dispatch(setLegendSize({ width: 0, height: 0 }));
|
|
188
|
+
};
|
|
189
|
+
}, [dispatch]);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getWidthOrHeight(
|
|
194
|
+
layout: LayoutType | undefined,
|
|
195
|
+
height: number | string | undefined,
|
|
196
|
+
width: number | string | undefined,
|
|
197
|
+
maxWidth: number,
|
|
198
|
+
): null | { height?: number | string; width?: number | string } {
|
|
199
|
+
if (layout === 'vertical' && height != null) {
|
|
200
|
+
return {
|
|
201
|
+
height,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (layout === 'horizontal') {
|
|
205
|
+
return {
|
|
206
|
+
width: width || maxWidth,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const legendDefaultProps = {
|
|
214
|
+
align: 'center',
|
|
215
|
+
iconSize: 14,
|
|
216
|
+
inactiveColor: '#ccc',
|
|
217
|
+
itemSorter: 'value',
|
|
218
|
+
labelStyle: {},
|
|
219
|
+
layout: 'horizontal',
|
|
220
|
+
verticalAlign: 'bottom',
|
|
221
|
+
} as const satisfies Partial<Props>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @consumes CartesianChartContext
|
|
225
|
+
* @consumes PolarChartContext
|
|
226
|
+
*/
|
|
227
|
+
function LegendImpl(outsideProps: Props) {
|
|
228
|
+
const props = resolveDefaultProps(outsideProps, legendDefaultProps);
|
|
229
|
+
const contextPayload = useLegendPayload();
|
|
230
|
+
const legendPortalFromContext = useLegendPortal();
|
|
231
|
+
const margin = useMargin();
|
|
232
|
+
const {
|
|
233
|
+
width: widthFromProps,
|
|
234
|
+
height: heightFromProps,
|
|
235
|
+
wrapperStyle,
|
|
236
|
+
portal: portalFromProps,
|
|
237
|
+
} = props;
|
|
238
|
+
// The contextPayload is not used directly inside the hook, but we need the onBBoxUpdate call
|
|
239
|
+
// when the payload changes, therefore it's here as a dependency.
|
|
240
|
+
const [lastBoundingBox, updateBoundingBox] = useElementOffset([contextPayload]);
|
|
241
|
+
const chartWidth = useChartWidth();
|
|
242
|
+
const chartHeight = useChartHeight();
|
|
243
|
+
if (chartWidth == null || chartHeight == null) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const maxWidth = chartWidth - (margin?.left || 0) - (margin?.right || 0);
|
|
247
|
+
const widthOrHeight = getWidthOrHeight(props.layout, heightFromProps, widthFromProps, maxWidth);
|
|
248
|
+
// if the user supplies their own portal, only use their defined wrapper styles
|
|
249
|
+
const outerStyle: CSSProperties | undefined =
|
|
250
|
+
portalFromProps
|
|
251
|
+
? wrapperStyle
|
|
252
|
+
: {
|
|
253
|
+
position: 'absolute',
|
|
254
|
+
width: widthOrHeight?.width || widthFromProps || 'auto',
|
|
255
|
+
height: widthOrHeight?.height || heightFromProps || 'auto',
|
|
256
|
+
...getDefaultPosition(
|
|
257
|
+
wrapperStyle,
|
|
258
|
+
props,
|
|
259
|
+
margin,
|
|
260
|
+
chartWidth,
|
|
261
|
+
chartHeight,
|
|
262
|
+
lastBoundingBox,
|
|
263
|
+
),
|
|
264
|
+
...wrapperStyle,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const legendPortal = portalFromProps ?? legendPortalFromContext;
|
|
268
|
+
|
|
269
|
+
if (legendPortal == null || contextPayload == null) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const legendElement = <div
|
|
274
|
+
className="recharts-legend-wrapper"
|
|
275
|
+
style={outerStyle}
|
|
276
|
+
ref={updateBoundingBox}
|
|
277
|
+
>
|
|
278
|
+
<LegendSettingsDispatcher
|
|
279
|
+
layout={props.layout}
|
|
280
|
+
align={props.align}
|
|
281
|
+
verticalAlign={props.verticalAlign}
|
|
282
|
+
itemSorter={props.itemSorter}
|
|
283
|
+
/>
|
|
284
|
+
{/* if we have a portal from props nothing should need the size of the legend */}
|
|
285
|
+
{!portalFromProps &&
|
|
286
|
+
<LegendSizeDispatcher width={lastBoundingBox.width} height={lastBoundingBox.height} />}
|
|
287
|
+
<LegendContent
|
|
288
|
+
{...props}
|
|
289
|
+
{...widthOrHeight}
|
|
290
|
+
margin={margin}
|
|
291
|
+
chartWidth={chartWidth}
|
|
292
|
+
chartHeight={chartHeight}
|
|
293
|
+
contextPayload={contextPayload}
|
|
294
|
+
/>
|
|
295
|
+
</div>;
|
|
296
|
+
|
|
297
|
+
return createPortal(legendElement, legendPortal);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const Legend = React.memo(LegendImpl, propsAreEqual);
|
|
301
|
+
|
|
302
|
+
Legend.displayName = 'Legend';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from 'octane';
|
|
2
|
+
import { CSSProperties } from 'octane';
|
|
3
|
+
import {
|
|
4
|
+
ContentType,
|
|
5
|
+
LegendPayload,
|
|
6
|
+
Props as DefaultLegendContentProps,
|
|
7
|
+
VerticalAlignmentType,
|
|
8
|
+
} from './DefaultLegendContent';
|
|
9
|
+
import { UniqueOption } from '../util/payload/getUniqPayload';
|
|
10
|
+
import { ElementOffset } from '../util/useElementOffset';
|
|
11
|
+
export type LegendItemSorter = 'value' | 'dataKey' | ((item: LegendPayload) => number | string);
|
|
12
|
+
export type Props = Omit<DefaultLegendContentProps, 'payload' | 'ref' | 'verticalAlign'> & {
|
|
13
|
+
/**
|
|
14
|
+
* Renders the content of the legend.
|
|
15
|
+
*
|
|
16
|
+
* This should return HTML elements, not SVG elements.
|
|
17
|
+
*
|
|
18
|
+
* - If not set, the {@link DefaultLegendContent} component is used.
|
|
19
|
+
* - If set to a React element, this element will be cloned and extra props will be passed in.
|
|
20
|
+
* - If set to a function, the function will be called and should return HTML elements.
|
|
21
|
+
*
|
|
22
|
+
* @example <Legend content={CustomizedLegend} />
|
|
23
|
+
* @example <Legend content={renderLegend} />
|
|
24
|
+
*/
|
|
25
|
+
content?: ContentType;
|
|
26
|
+
/**
|
|
27
|
+
* CSS styles to be applied to the wrapper `div` element.
|
|
28
|
+
*/
|
|
29
|
+
wrapperStyle?: CSSProperties;
|
|
30
|
+
/**
|
|
31
|
+
* Width of the legend.
|
|
32
|
+
* Accept CSS style string values like `100%` or `fit-content`, or number values like `400`.
|
|
33
|
+
*/
|
|
34
|
+
width?: number | string;
|
|
35
|
+
/**
|
|
36
|
+
* Height of the legend.
|
|
37
|
+
* Accept CSS style string values like `100%` or `fit-content`, or number values like `400`.
|
|
38
|
+
*/
|
|
39
|
+
height?: number | string;
|
|
40
|
+
payloadUniqBy?: UniqueOption<LegendPayload>;
|
|
41
|
+
onBBoxUpdate?: (box: ElementOffset | null) => void;
|
|
42
|
+
/**
|
|
43
|
+
* If portal is defined, then Legend will use this element as a target
|
|
44
|
+
* for rendering using React Portal.
|
|
45
|
+
*
|
|
46
|
+
* If this is undefined then Legend renders inside the recharts-wrapper element.
|
|
47
|
+
*
|
|
48
|
+
* @see {@link https://react.dev/reference/react-dom/createPortal}
|
|
49
|
+
*/
|
|
50
|
+
portal?: HTMLElement | null;
|
|
51
|
+
/**
|
|
52
|
+
* Sorts Legend items. Defaults to `value` which means it will sort alphabetically
|
|
53
|
+
* by the label.
|
|
54
|
+
*
|
|
55
|
+
* If `null` is provided then the payload is not sorted. Be aware that without sort,
|
|
56
|
+
* the order of items may change between renders!
|
|
57
|
+
*
|
|
58
|
+
* @defaultValue value
|
|
59
|
+
*/
|
|
60
|
+
itemSorter?: LegendItemSorter | null;
|
|
61
|
+
/**
|
|
62
|
+
* The alignment of the whole Legend container:
|
|
63
|
+
*
|
|
64
|
+
* - `bottom`: shows the Legend below chart, and chart height reduces automatically to make space for it.
|
|
65
|
+
* - `top`: shows the Legend above chart, and chart height reduces automatically.
|
|
66
|
+
* - `middle`: shows the Legend in the middle of chart, covering other content, and chart height remains unchanged.
|
|
67
|
+
* The exact behavior changes depending on `align` prop.
|
|
68
|
+
*
|
|
69
|
+
* @defaultValue bottom
|
|
70
|
+
*/
|
|
71
|
+
verticalAlign?: VerticalAlignmentType;
|
|
72
|
+
};
|
|
73
|
+
export declare const legendDefaultProps: {
|
|
74
|
+
readonly align: 'center';
|
|
75
|
+
readonly iconSize: 14;
|
|
76
|
+
readonly inactiveColor: '#ccc';
|
|
77
|
+
readonly itemSorter: 'value';
|
|
78
|
+
readonly labelStyle: {};
|
|
79
|
+
readonly layout: 'horizontal';
|
|
80
|
+
readonly verticalAlign: 'bottom';
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* @consumes CartesianChartContext
|
|
84
|
+
* @consumes PolarChartContext
|
|
85
|
+
*/
|
|
86
|
+
declare function LegendImpl(outsideProps: Props): React.ReactPortal | null;
|
|
87
|
+
export declare const Legend: React.MemoExoticComponent<typeof LegendImpl>;
|
|
88
|
+
export {};
|