@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.
Files changed (88) hide show
  1. package/package.json +4 -4
  2. package/src/animation/CSSTransitionAnimate.tsrx +142 -0
  3. package/src/cartesian/Area.tsrx +1096 -0
  4. package/src/cartesian/Area.tsrx.d.ts +340 -0
  5. package/src/cartesian/AreaRevealShape.tsrx +202 -0
  6. package/src/cartesian/CartesianGrid.tsrx +643 -0
  7. package/src/cartesian/CartesianGrid.tsrx.d.ts +219 -0
  8. package/src/cartesian/ErrorBar.tsrx +322 -0
  9. package/src/cartesian/ErrorBar.tsrx.d.ts +131 -0
  10. package/src/cartesian/Funnel.tsrx +833 -0
  11. package/src/cartesian/Funnel.tsrx.d.ts +274 -0
  12. package/src/cartesian/ReferenceLine.tsrx +452 -0
  13. package/src/cartesian/ReferenceLine.tsrx.d.ts +178 -0
  14. package/src/cartesian/Scatter.tsrx +1076 -0
  15. package/src/cartesian/Scatter.tsrx.d.ts +351 -0
  16. package/src/cartesian/ZAxis.tsrx +148 -0
  17. package/src/cartesian/ZAxis.tsrx.d.ts +102 -0
  18. package/src/chart/AreaChart.tsrx +22 -0
  19. package/src/chart/AreaChart.tsrx.d.ts +12 -0
  20. package/src/chart/ComposedChart.tsrx +22 -0
  21. package/src/chart/ComposedChart.tsrx.d.ts +12 -0
  22. package/src/chart/FunnelChart.tsrx +22 -0
  23. package/src/chart/FunnelChart.tsrx.d.ts +12 -0
  24. package/src/chart/PieChart.tsrx +31 -0
  25. package/src/chart/PieChart.tsrx.d.ts +31 -0
  26. package/src/chart/PolarChart.tsrx +121 -0
  27. package/src/chart/RadarChart.tsrx +53 -0
  28. package/src/chart/RadarChart.tsrx.d.ts +31 -0
  29. package/src/chart/RadialBarChart.tsrx +31 -0
  30. package/src/chart/RadialBarChart.tsrx.d.ts +31 -0
  31. package/src/chart/Sankey.tsrx +1542 -0
  32. package/src/chart/Sankey.tsrx.d.ts +253 -0
  33. package/src/chart/ScatterChart.tsrx +22 -0
  34. package/src/chart/ScatterChart.tsrx.d.ts +12 -0
  35. package/src/chart/SunburstChart.tsrx +495 -0
  36. package/src/chart/SunburstChart.tsrx.d.ts +146 -0
  37. package/src/chart/types.ts +19 -0
  38. package/src/component/Cursor.tsrx +157 -0
  39. package/src/component/DefaultLegendContent.tsrx +288 -0
  40. package/src/component/DefaultLegendContent.tsrx.d.ts +125 -0
  41. package/src/component/DefaultTooltipContent.tsrx +215 -0
  42. package/src/component/DefaultTooltipContent.tsrx.d.ts +87 -0
  43. package/src/component/Legend.tsrx +302 -0
  44. package/src/component/Legend.tsrx.d.ts +88 -0
  45. package/src/component/ResponsiveContainer.ts +1 -22
  46. package/src/component/ResponsiveContainer.tsrx +171 -0
  47. package/src/component/ResponsiveContainer.tsrx.d.ts +81 -0
  48. package/src/component/Tooltip.tsrx +425 -0
  49. package/src/component/Tooltip.tsrx.d.ts +246 -0
  50. package/src/component/TooltipBoundingBox.tsrx +135 -0
  51. package/src/context/CellsContext.ts +12 -0
  52. package/src/context/legendPayloadContext.tsrx +11 -0
  53. package/src/context/useTooltipAxis.ts +20 -0
  54. package/src/index.d.ts +236 -0
  55. package/src/index.ts +67 -0
  56. package/src/polar/Pie.tsrx +1224 -0
  57. package/src/polar/Pie.tsrx.d.ts +436 -0
  58. package/src/polar/PolarAngleAxis.tsrx +554 -0
  59. package/src/polar/PolarAngleAxis.tsrx.d.ts +215 -0
  60. package/src/polar/PolarGrid.tsrx +317 -0
  61. package/src/polar/PolarGrid.tsrx.d.ts +117 -0
  62. package/src/polar/PolarRadiusAxis.tsrx +463 -0
  63. package/src/polar/PolarRadiusAxis.tsrx.d.ts +209 -0
  64. package/src/polar/Radar.tsrx +696 -0
  65. package/src/polar/Radar.tsrx.d.ts +215 -0
  66. package/src/polar/RadialBar.tsrx +887 -0
  67. package/src/polar/RadialBar.tsrx.d.ts +340 -0
  68. package/src/state/ReportPolarOptions.tsrx +11 -0
  69. package/src/state/selectors/areaSelectors.ts +257 -0
  70. package/src/state/selectors/funnelSelectors.ts +86 -0
  71. package/src/state/selectors/pieSelectors.ts +134 -0
  72. package/src/state/selectors/polarGridSelectors.ts +43 -0
  73. package/src/state/selectors/polarScaleSelectors.ts +159 -0
  74. package/src/state/selectors/polarSelectors.ts +198 -0
  75. package/src/state/selectors/radarSelectors.ts +228 -0
  76. package/src/state/selectors/radialBarSelectors.ts +511 -0
  77. package/src/state/selectors/scatterSelectors.ts +168 -0
  78. package/src/util/FunnelUtils.tsrx +19 -0
  79. package/src/util/RadialBarUtils.tsrx +29 -0
  80. package/src/util/ScatterUtils.tsrx +34 -0
  81. package/src/util/cursor/getCursorPoints.ts +45 -0
  82. package/src/util/cursor/getCursorRectangle.ts +28 -0
  83. package/src/util/cursor/getRadialCursorPoints.ts +31 -0
  84. package/src/util/payload/getUniqPayload.ts +28 -0
  85. package/src/util/scale/CartesianScaleHelper.ts +71 -0
  86. package/src/util/tooltip/translate.ts +168 -0
  87. package/src/util/types.ts +3 -0
  88. 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 {};