@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,833 @@
1
+ import * as React from 'octane';
2
+ import { MutableRefObject, ReactElement, ReactNode, useMemo, useRef } from 'octane';
3
+ import { shallowEqual } from '@octanejs/redux';
4
+ import omit from 'es-toolkit/compat/omit';
5
+
6
+ import { clsx } from 'clsx';
7
+ import { selectActiveIndex } from '../state/selectors/selectors';
8
+ import { useAppSelector } from '../state/hooks';
9
+ import { Layer } from '../container/Layer';
10
+ import { Props as TrapezoidProps } from '../shape/Trapezoid';
11
+ import {
12
+ CartesianLabelListContextProvider,
13
+ CartesianLabelListEntry,
14
+ ImplicitLabelListType,
15
+ LabelListFromLabelProp,
16
+ } from '../component/LabelList';
17
+ import { getPercentValue, interpolate } from '../util/DataUtils';
18
+ import { getValueByDataKey } from '../util/ChartUtils';
19
+ import {
20
+ ActiveShape,
21
+ adaptEventsOfChild,
22
+ AnimationDuration,
23
+ EasingInput,
24
+ CartesianViewBoxRequired,
25
+ ChartOffsetInternal,
26
+ Coordinate,
27
+ DataConsumer,
28
+ DataKey,
29
+ DataProvider,
30
+ LegendType,
31
+ PresentationAttributesAdaptChildEvent,
32
+ ShapeAnimationProps,
33
+ TooltipType,
34
+ TrapezoidViewBox,
35
+ CartesianLayout,
36
+ } from '../util/types';
37
+ import { defaultFunnelShape, FunnelTrapezoid, FunnelTrapezoidProps } from '../util/FunnelUtils';
38
+ import {
39
+ useMouseClickItemDispatch,
40
+ useMouseEnterItemDispatch,
41
+ useMouseLeaveItemDispatch,
42
+ } from '../context/tooltipContext';
43
+ import { TooltipPayload, TooltipPayloadConfiguration } from '../state/tooltipSlice';
44
+ import { SetTooltipEntrySettings } from '../state/SetTooltipEntrySettings';
45
+ import { Formatter } from '../component/DefaultTooltipContent';
46
+ import type { LegendPayload } from '../component/DefaultLegendContent';
47
+ import { ResolvedFunnelSettings, selectFunnelTrapezoids } from '../state/selectors/funnelSelectors';
48
+ import { RequiresDefaultProps, resolveDefaultProps } from '../util/resolveDefaultProps';
49
+ import { usePlotArea } from '../hooks';
50
+ import { svgPropertiesNoEvents } from '../util/svgPropertiesNoEvents';
51
+ import {
52
+ AnimatedItems,
53
+ AnimationInterpolateFn,
54
+ useAnimationCallbacks,
55
+ } from '../animation/AnimatedItems';
56
+ import { AnimationMatchByProp, matchAppend } from '../animation/matchBy';
57
+ import { GraphicalItemId } from '../state/graphicalItemsSlice';
58
+ import { RegisterGraphicalItemId } from '../context/RegisterGraphicalItemId';
59
+ import { WithIdRequired } from '../util/useUniqueId';
60
+ import { SetLegendPayload } from '../state/SetLegendPayload';
61
+ import { CellsContext, useCellRegistry } from '../context/CellsContext';
62
+ import { useCartesianChartLayout } from '../context/chartLayoutContext';
63
+
64
+ export type FunnelTrapezoidItem =
65
+ TrapezoidProps & TrapezoidViewBox & {
66
+ value?: number | string;
67
+ payload?: any;
68
+ tooltipPosition: Coordinate;
69
+ name: string;
70
+ labelViewBox: TrapezoidViewBox;
71
+ parentViewBox: CartesianViewBoxRequired;
72
+ val: number | ReadonlyArray<number>;
73
+ tooltipPayload: TooltipPayload;
74
+ };
75
+
76
+ /**
77
+ * Internal props, combination of external props + defaultProps + private Recharts state
78
+ */
79
+ type InternalFunnelProps =
80
+ RequiresDefaultProps<FunnelProps, typeof defaultFunnelProps> & {
81
+ id: GraphicalItemId;
82
+ trapezoids: ReadonlyArray<FunnelTrapezoidItem>;
83
+ };
84
+
85
+ /**
86
+ * External props, intended for end users to fill in
87
+ */
88
+ interface FunnelProps<DataPointType = any, DataValueType = any> extends DataProvider<
89
+ DataPointType
90
+ >, Required<DataConsumer<DataPointType, DataValueType>> {
91
+ /**
92
+ * This component is rendered when this graphical item is activated
93
+ * (could be by mouse hover, touch, keyboard, programmatically).
94
+ */
95
+ activeShape?: ActiveShape<FunnelTrapezoidItem, SVGPathElement>;
96
+ /**
97
+ * Specifies when the animation should begin, the unit of this option is ms.
98
+ * @defaultValue 400
99
+ */
100
+ animationBegin?: number;
101
+ /**
102
+ * Specifies the duration of animation, the unit of this option is ms.
103
+ * @defaultValue 1500
104
+ */
105
+ animationDuration?: AnimationDuration;
106
+ /**
107
+ * The type of easing function.
108
+ * @defaultValue ease
109
+ */
110
+ animationEasing?: EasingInput;
111
+ /**
112
+ * Custom animation function for interpolating data items.
113
+ * When provided, this replaces the default animation interpolation.
114
+ *
115
+ * @param prevItems The items from the previous animation frame, or null on first render
116
+ * @param nextItems The target items to animate towards
117
+ * @param animationElapsedTime A normalized time value (0 = start, 1 = end)
118
+ * @returns The interpolated items at time animationElapsedTime
119
+ *
120
+ * @since 3.9
121
+ * @see {@link https://recharts.github.io/en-US/guide/animations/ Animations guide}
122
+ */
123
+ animationInterpolateFn?: AnimationInterpolateFn<FunnelTrapezoidItem, CartesianLayout>;
124
+ /**
125
+ * Strategy for matching previous items to next items during animation.
126
+ * Determines how Recharts pairs old data points with new data points
127
+ * to create smooth transitions.
128
+ *
129
+ * - `matchAppend` (default): match sequentially by index and treat newly appended items as new
130
+ * - `matchByIndex`: match by array position with proportional stretching
131
+ * - `matchByDataKey('someKey')`: match by a data key from the payload
132
+ * - Custom function `(item, index) => key`: match by the returned key
133
+ *
134
+ * @defaultValue append
135
+ * @see matchByIndex
136
+ * @see matchByDataKey
137
+ * @see matchAppend
138
+ *
139
+ * @since 3.9
140
+ * @see {@link https://recharts.github.io/en-US/guide/animations/ Animations guide}
141
+ */
142
+ animationMatchBy?: AnimationMatchByProp<FunnelTrapezoidItem>;
143
+ className?: string;
144
+ /**
145
+ * Hides the whole graphical element when true.
146
+ *
147
+ * Hiding an element is different from removing it from the chart:
148
+ * Hidden graphical elements are still visible in Legend,
149
+ * and can be included in axis domain calculations,
150
+ * depending on `includeHidden` props of your XAxis/YAxis.
151
+ *
152
+ * @defaultValue false
153
+ */
154
+ hide?: boolean;
155
+ /**
156
+ * Unique identifier of this component.
157
+ * Used as an HTML attribute `id`, and also to identify this element internally.
158
+ *
159
+ * If undefined, Recharts will generate a unique ID automatically.
160
+ */
161
+ id?: string;
162
+ /**
163
+ * If set false, animation of funnel will be disabled.
164
+ * If set "auto", the animation will be disabled in SSR and will respect the user's prefers-reduced-motion system preference for accessibility.
165
+ * @defaultValue auto
166
+ */
167
+ isAnimationActive?: boolean | 'auto';
168
+ label?: ImplicitLabelListType;
169
+ /**
170
+ * @defaultValue triangle
171
+ */
172
+ lastShapeType?: 'triangle' | 'rectangle';
173
+ /**
174
+ * The type of icon in legend. If set to 'none', no legend item will be rendered.
175
+ * @defaultValue rect
176
+ */
177
+ legendType?: LegendType;
178
+ /**
179
+ * Name represents each sector in the tooltip.
180
+ * This allows you to extract the name from the data:
181
+ *
182
+ * - `string`: the name of the field in the data object;
183
+ * - `number`: the index of the field in the data;
184
+ * - `function`: a function that receives the data object and returns the name.
185
+ *
186
+ * @defaultValue name
187
+ */
188
+ nameKey?: DataKey<DataPointType, DataValueType>;
189
+ /**
190
+ * The customized event handler of animation end
191
+ */
192
+ onAnimationEnd?: () => void;
193
+ /**
194
+ * The customized event handler of animation start
195
+ */
196
+ onAnimationStart?: () => void;
197
+ reversed?: boolean;
198
+ /**
199
+ * If set a ReactElement, the shape of funnel can be customized.
200
+ * If set a function, the function will be called to render customized shape.
201
+ * During animations, the function shape also receives `animationElapsedTime`, `isAnimating`, and `isEntrance`.
202
+ * By default, renders a trapezoid.
203
+ */
204
+ shape?: ActiveShape<FunnelTrapezoidItem & ShapeAnimationProps, SVGPathElement>;
205
+ tooltipType?: TooltipType;
206
+ /**
207
+ * Formats the value displayed in the tooltip for this Funnel.
208
+ * When set, takes precedence over the `formatter` prop on the Tooltip component.
209
+ */
210
+ formatter?: Formatter;
211
+ /**
212
+ * The customized event handler of click on the area in this group
213
+ */
214
+ onClick?: (data: FunnelTrapezoidItem, index: number, e: React.MouseEvent<SVGPathElement>) => void;
215
+ /**
216
+ * The customized event handler of mousedown on the area in this group
217
+ */
218
+ onMouseDown?: (data: FunnelTrapezoidItem, index: number, e: React.MouseEvent<
219
+ SVGPathElement
220
+ >) => void;
221
+ /**
222
+ * The customized event handler of mouseup on the area in this group
223
+ */
224
+ onMouseUp?: (data: FunnelTrapezoidItem, index: number, e: React.MouseEvent<
225
+ SVGPathElement
226
+ >) => void;
227
+ /**
228
+ * The customized event handler of mousemove on the area in this group
229
+ */
230
+ onMouseMove?: (data: FunnelTrapezoidItem, index: number, e: React.MouseEvent<
231
+ SVGPathElement
232
+ >) => void;
233
+ /**
234
+ * The customized event handler of mouseover on the area in this group
235
+ */
236
+ onMouseOver?: (data: FunnelTrapezoidItem, index: number, e: React.MouseEvent<
237
+ SVGPathElement
238
+ >) => void;
239
+ /**
240
+ * The customized event handler of mouseout on the area in this group
241
+ */
242
+ onMouseOut?: (data: FunnelTrapezoidItem, index: number, e: React.MouseEvent<
243
+ SVGPathElement
244
+ >) => void;
245
+ /**
246
+ * The customized event handler of mouseenter on the area in this group
247
+ */
248
+ onMouseEnter?: (data: FunnelTrapezoidItem, index: number, e: React.MouseEvent<
249
+ SVGPathElement
250
+ >) => void;
251
+ /**
252
+ * The customized event handler of mouseleave on the area in this group
253
+ */
254
+ onMouseLeave?: (data: FunnelTrapezoidItem, index: number, e: React.MouseEvent<
255
+ SVGPathElement
256
+ >) => void;
257
+ }
258
+
259
+ type FunnelSvgProps =
260
+ Omit<PresentationAttributesAdaptChildEvent<FunnelTrapezoidItem, SVGPathElement>, 'ref'>;
261
+
262
+ type InternalProps = FunnelSvgProps & InternalFunnelProps;
263
+
264
+ export type Props<DataPointType = any, DataValueType = any> =
265
+ FunnelSvgProps & FunnelProps<DataPointType, DataValueType>;
266
+
267
+ type RealFunnelData = unknown;
268
+
269
+ type FunnelTrapezoidsProps =
270
+ ShapeAnimationProps & {
271
+ trapezoids: ReadonlyArray<FunnelTrapezoidItem>;
272
+ allOtherFunnelProps: InternalProps;
273
+ };
274
+
275
+ const SetFunnelTooltipEntrySettings = React.memo(({
276
+ dataKey,
277
+ nameKey,
278
+ stroke,
279
+ strokeWidth,
280
+ fill,
281
+ name,
282
+ hide,
283
+ tooltipType,
284
+ formatter,
285
+ data,
286
+ trapezoids,
287
+ id,
288
+ }: Pick<
289
+ InternalProps,
290
+ | 'dataKey'
291
+ | 'nameKey'
292
+ | 'stroke'
293
+ | 'strokeWidth'
294
+ | 'fill'
295
+ | 'name'
296
+ | 'hide'
297
+ | 'tooltipType'
298
+ | 'formatter'
299
+ | 'data'
300
+ | 'id'
301
+ > & {
302
+ trapezoids: ReadonlyArray<FunnelTrapezoidItem>;
303
+ }) => {
304
+ const tooltipEntrySettings: TooltipPayloadConfiguration = {
305
+ dataDefinedOnItem: data,
306
+ getPosition: (index) => trapezoids[Number(index)]?.tooltipPosition,
307
+ settings: {
308
+ stroke,
309
+ strokeWidth,
310
+ fill,
311
+ dataKey,
312
+ name,
313
+ nameKey,
314
+ hide,
315
+ type: tooltipType,
316
+ color: fill,
317
+ unit: '', // Funnel does not have unit, why?
318
+ formatter,
319
+ graphicalItemId: id,
320
+ },
321
+ };
322
+ return <SetTooltipEntrySettings tooltipEntrySettings={tooltipEntrySettings} />;
323
+ });
324
+
325
+ function FunnelLabelListProvider({
326
+ showLabels,
327
+ trapezoids,
328
+ children,
329
+ }: {
330
+ showLabels: boolean;
331
+ trapezoids: ReadonlyArray<FunnelTrapezoidItem> | undefined;
332
+ children: ReactNode;
333
+ }) {
334
+ const labelListEntries: ReadonlyArray<CartesianLabelListEntry> | undefined = useMemo(() => {
335
+ if (!showLabels) {
336
+ return undefined;
337
+ }
338
+ return trapezoids?.map((entry): CartesianLabelListEntry => {
339
+ const viewBox: TrapezoidViewBox = entry.labelViewBox;
340
+
341
+ return {
342
+ ...viewBox,
343
+ value: entry.name,
344
+ payload: entry.payload,
345
+ parentViewBox: entry.parentViewBox,
346
+ viewBox,
347
+ fill: entry.fill,
348
+ };
349
+ });
350
+ }, [showLabels, trapezoids]);
351
+
352
+ return <CartesianLabelListContextProvider
353
+ value={labelListEntries}
354
+ >{children}</CartesianLabelListContextProvider>;
355
+ }
356
+
357
+ function FunnelTrapezoids(props: FunnelTrapezoidsProps) {
358
+ const { trapezoids, allOtherFunnelProps, animationElapsedTime, isAnimating, isEntrance } = props;
359
+ const activeItemIndex = useAppSelector(
360
+ (state) => selectActiveIndex(state, 'item', state.tooltip.settings.trigger, undefined),
361
+ );
362
+ const {
363
+ onMouseEnter: onMouseEnterFromProps,
364
+ onClick: onItemClickFromProps,
365
+ onMouseLeave: onMouseLeaveFromProps,
366
+ shape,
367
+ activeShape,
368
+ ...restOfAllOtherProps
369
+ } = allOtherFunnelProps;
370
+
371
+ const onMouseEnterFromContext = useMouseEnterItemDispatch(
372
+ onMouseEnterFromProps,
373
+ allOtherFunnelProps.dataKey,
374
+ allOtherFunnelProps.id,
375
+ );
376
+ const onMouseLeaveFromContext = useMouseLeaveItemDispatch(onMouseLeaveFromProps);
377
+ const onClickFromContext = useMouseClickItemDispatch(
378
+ onItemClickFromProps,
379
+ allOtherFunnelProps.dataKey,
380
+ allOtherFunnelProps.id,
381
+ );
382
+
383
+ return <>
384
+ {trapezoids.map((entry: FunnelTrapezoidItem, i: number) => {
385
+ const isActiveIndex = Boolean(activeShape) && activeItemIndex === String(i);
386
+ const trapezoidOptions = isActiveIndex ? activeShape : shape;
387
+ const { id, ...trapezoidProps }: FunnelTrapezoidProps = {
388
+ ...entry,
389
+ option: trapezoidOptions,
390
+ isActive: isActiveIndex,
391
+ stroke: entry.stroke,
392
+ animationElapsedTime,
393
+ isAnimating,
394
+ isEntrance,
395
+ };
396
+
397
+ return <Layer
398
+ key={`trapezoid-${entry?.x}-${entry?.y}-${entry?.name}-${entry?.value}`}
399
+ className="recharts-funnel-trapezoid"
400
+ {...adaptEventsOfChild(restOfAllOtherProps, entry, i)}
401
+ onMouseEnter={onMouseEnterFromContext(entry, i)}
402
+ onMouseLeave={onMouseLeaveFromContext(entry, i)}
403
+ onClick={onClickFromContext(entry, i)}
404
+ >
405
+ <FunnelTrapezoid {...trapezoidProps} />
406
+ </Layer>;
407
+ })}
408
+ </>;
409
+ }
410
+
411
+ const defaultFunnelAnimateItems: AnimationInterpolateFn<FunnelTrapezoidItem, CartesianLayout> = (
412
+ items,
413
+ animationElapsedTime,
414
+ ) => {
415
+ if (items == null) return [];
416
+ if (animationElapsedTime === 1) {
417
+ return items.flatMap((item) => (item.status === 'removed' ? [] : [item.next]));
418
+ }
419
+ return items.flatMap((item) => {
420
+ if (item.status === 'removed') return [];
421
+ if (item.status === 'matched') {
422
+ return [
423
+ {
424
+ ...item.next,
425
+ x: interpolate(item.prev.x, item.next.x, animationElapsedTime),
426
+ y: interpolate(item.prev.y, item.next.y, animationElapsedTime),
427
+ upperWidth: interpolate(item.prev.upperWidth, item.next.upperWidth, animationElapsedTime),
428
+ lowerWidth: interpolate(item.prev.lowerWidth, item.next.lowerWidth, animationElapsedTime),
429
+ height: interpolate(item.prev.height, item.next.height, animationElapsedTime),
430
+ },
431
+ ];
432
+ }
433
+ // added
434
+ const { next } = item;
435
+ return [
436
+ {
437
+ ...next,
438
+ x: interpolate(next.x + next.upperWidth / 2, next.x, animationElapsedTime),
439
+ y: interpolate(next.y + next.height / 2, next.y, animationElapsedTime),
440
+ upperWidth: interpolate(0, next.upperWidth, animationElapsedTime),
441
+ lowerWidth: interpolate(0, next.lowerWidth, animationElapsedTime),
442
+ height: interpolate(0, next.height, animationElapsedTime),
443
+ },
444
+ ];
445
+ });
446
+ };
447
+
448
+ function TrapezoidsWithAnimation({
449
+ previousTrapezoidsRef,
450
+ props,
451
+ }: {
452
+ props: InternalProps;
453
+ previousTrapezoidsRef: MutableRefObject<ReadonlyArray<FunnelTrapezoidItem> | undefined>;
454
+ }) {
455
+ const {
456
+ trapezoids,
457
+ isAnimationActive,
458
+ animationBegin,
459
+ animationDuration,
460
+ animationEasing,
461
+ animationInterpolateFn,
462
+ } = props;
463
+ const layout = useCartesianChartLayout();
464
+
465
+ const { isAnimating, handleAnimationStart, handleAnimationEnd } = useAnimationCallbacks(
466
+ props.onAnimationStart,
467
+ props.onAnimationEnd,
468
+ );
469
+
470
+ if (layout == null) return null;
471
+
472
+ return <FunnelLabelListProvider showLabels={!isAnimating} trapezoids={trapezoids}>
473
+ <AnimatedItems
474
+ animationInput={trapezoids}
475
+ animationIdPrefix="recharts-funnel-"
476
+ items={trapezoids}
477
+ previousItemsRef={previousTrapezoidsRef}
478
+ isAnimationActive={isAnimationActive}
479
+ animationBegin={animationBegin}
480
+ animationDuration={animationDuration}
481
+ animationEasing={animationEasing}
482
+ onAnimationStart={handleAnimationStart}
483
+ onAnimationEnd={handleAnimationEnd}
484
+ animationInterpolateFn={animationInterpolateFn}
485
+ animationMatchBy={props.animationMatchBy}
486
+ layout={layout}
487
+ >
488
+ {(stepData, animationElapsedTime, isEntrance) => <Layer>
489
+ <FunnelTrapezoids
490
+ trapezoids={stepData}
491
+ allOtherFunnelProps={props}
492
+ animationElapsedTime={animationElapsedTime}
493
+ isAnimating={isAnimating || animationElapsedTime < 1}
494
+ isEntrance={isEntrance}
495
+ />
496
+ </Layer>}
497
+ </AnimatedItems>
498
+ <LabelListFromLabelProp label={props.label} />
499
+ {props.children}
500
+ </FunnelLabelListProvider>;
501
+ }
502
+
503
+ function RenderTrapezoids(props: InternalProps) {
504
+ const previousTrapezoidsRef = useRef<ReadonlyArray<FunnelTrapezoidItem> | undefined>(undefined);
505
+
506
+ return <TrapezoidsWithAnimation props={props} previousTrapezoidsRef={previousTrapezoidsRef} />;
507
+ }
508
+
509
+ const getRealWidthHeight = (
510
+ customWidth: number | string | undefined,
511
+ offset: ChartOffsetInternal,
512
+ ) => {
513
+ const { width, height, left, top } = offset;
514
+
515
+ const realWidth: number = getPercentValue(customWidth, width, width);
516
+
517
+ return {
518
+ realWidth,
519
+ realHeight: height,
520
+ offsetX: left,
521
+ offsetY: top,
522
+ };
523
+ };
524
+
525
+ export const defaultFunnelProps = {
526
+ animationBegin: 400,
527
+ animationDuration: 1500,
528
+ animationEasing: 'ease',
529
+ animationInterpolateFn: defaultFunnelAnimateItems,
530
+ animationMatchBy: matchAppend,
531
+ fill: '#808080',
532
+ hide: false,
533
+ isAnimationActive: 'auto',
534
+ lastShapeType: 'triangle',
535
+ legendType: 'rect',
536
+ nameKey: 'name',
537
+ reversed: false,
538
+ shape: defaultFunnelShape,
539
+ stroke: '#fff',
540
+ } as const satisfies Partial<Props>;
541
+
542
+ function FunnelImpl(props: WithIdRequired<RequiresDefaultProps<Props, typeof defaultFunnelProps>>) {
543
+ const plotArea = usePlotArea();
544
+
545
+ const {
546
+ stroke,
547
+ fill,
548
+ legendType,
549
+ hide,
550
+ isAnimationActive,
551
+ animationBegin,
552
+ animationDuration,
553
+ animationEasing,
554
+ nameKey,
555
+ lastShapeType,
556
+ id,
557
+ ...everythingElse
558
+ } = props;
559
+
560
+ const nextPresentationProps = svgPropertiesNoEvents(props);
561
+ const presentationPropsRef = useRef(nextPresentationProps);
562
+ if (!shallowEqual(presentationPropsRef.current, nextPresentationProps)) {
563
+ presentationPropsRef.current = nextPresentationProps;
564
+ }
565
+ const presentationProps = presentationPropsRef.current;
566
+ const [cells, cellRegistry] = useCellRegistry();
567
+
568
+ const funnelSettings: ResolvedFunnelSettings = useMemo(
569
+ () => ({
570
+ dataKey: props.dataKey,
571
+ nameKey,
572
+ data: props.data,
573
+ tooltipType: props.tooltipType,
574
+ lastShapeType,
575
+ reversed: props.reversed,
576
+ customWidth: props.width,
577
+ cells,
578
+ presentationProps,
579
+ id,
580
+ }),
581
+ [
582
+ props.dataKey,
583
+ nameKey,
584
+ props.data,
585
+ props.tooltipType,
586
+ lastShapeType,
587
+ props.reversed,
588
+ props.width,
589
+ cells,
590
+ presentationProps,
591
+ id,
592
+ ],
593
+ );
594
+
595
+ const trapezoids = useAppSelector((state) => selectFunnelTrapezoids(state, funnelSettings));
596
+ const legendPayload = useMemo<ReadonlyArray<LegendPayload>>(() => {
597
+ if (legendType === 'none' || !trapezoids) {
598
+ return [];
599
+ }
600
+ return trapezoids.map(
601
+ (entry) => ({
602
+ inactive: hide,
603
+ value: entry.name,
604
+ dataKey: props.dataKey,
605
+ color: typeof entry.fill === 'string' ? entry.fill : fill,
606
+ type: legendType,
607
+ payload: entry.payload != null && typeof entry.payload === 'object' ? entry.payload : entry,
608
+ }),
609
+ );
610
+ }, [fill, hide, legendType, props.dataKey, trapezoids]);
611
+
612
+ if (hide || !trapezoids || !trapezoids.length || !plotArea) {
613
+ return <CellsContext.Provider value={cellRegistry}>
614
+ <SetLegendPayload legendPayload={legendPayload} />
615
+ <SetFunnelTooltipEntrySettings
616
+ dataKey={props.dataKey}
617
+ nameKey={props.nameKey}
618
+ stroke={props.stroke}
619
+ strokeWidth={props.strokeWidth}
620
+ fill={props.fill}
621
+ name={props.name}
622
+ hide={props.hide}
623
+ tooltipType={props.tooltipType}
624
+ formatter={props.formatter}
625
+ data={props.data}
626
+ trapezoids={trapezoids}
627
+ id={id}
628
+ />
629
+ {props.children}
630
+ </CellsContext.Provider>;
631
+ }
632
+ const { height, width } = plotArea;
633
+
634
+ const layerClass = clsx('recharts-trapezoids', props.className);
635
+
636
+ return <CellsContext.Provider value={cellRegistry}>
637
+ <SetLegendPayload legendPayload={legendPayload} />
638
+ <SetFunnelTooltipEntrySettings
639
+ dataKey={props.dataKey}
640
+ nameKey={props.nameKey}
641
+ stroke={props.stroke}
642
+ strokeWidth={props.strokeWidth}
643
+ fill={props.fill}
644
+ name={props.name}
645
+ hide={props.hide}
646
+ tooltipType={props.tooltipType}
647
+ formatter={props.formatter}
648
+ data={props.data}
649
+ trapezoids={trapezoids}
650
+ id={id}
651
+ />
652
+ <Layer className={layerClass}>
653
+ <RenderTrapezoids
654
+ {...everythingElse}
655
+ id={id}
656
+ stroke={stroke}
657
+ fill={fill}
658
+ nameKey={nameKey}
659
+ lastShapeType={lastShapeType}
660
+ animationBegin={animationBegin}
661
+ animationDuration={animationDuration}
662
+ animationEasing={animationEasing}
663
+ isAnimationActive={isAnimationActive}
664
+ hide={hide}
665
+ legendType={legendType}
666
+ height={height}
667
+ width={width}
668
+ trapezoids={trapezoids}
669
+ />
670
+ </Layer>
671
+ </CellsContext.Provider>;
672
+ }
673
+
674
+ function normalizeFunnelValue(value: unknown): {
675
+ value: number;
676
+ rangedNextValue: number | undefined;
677
+ } {
678
+ if (typeof value === 'number' && Number.isFinite(value)) {
679
+ return { value, rangedNextValue: undefined };
680
+ }
681
+ if (Array.isArray(value)) {
682
+ const currentValue = value[0];
683
+ const nextValue = value[1];
684
+ return {
685
+ value: typeof currentValue === 'number' && Number.isFinite(currentValue) ? currentValue : 0,
686
+ rangedNextValue: typeof nextValue === 'number' && Number.isFinite(nextValue)
687
+ ? nextValue
688
+ : undefined,
689
+ };
690
+ }
691
+ return { value: 0, rangedNextValue: undefined };
692
+ }
693
+
694
+ export function computeFunnelTrapezoids({
695
+ dataKey,
696
+ nameKey,
697
+ displayedData,
698
+ tooltipType,
699
+ lastShapeType,
700
+ reversed,
701
+ offset,
702
+ customWidth,
703
+ graphicalItemId,
704
+ }: {
705
+ dataKey: Props['dataKey'];
706
+ nameKey: Props['nameKey'];
707
+ offset: ChartOffsetInternal;
708
+ displayedData: ReadonlyArray<RealFunnelData>;
709
+ tooltipType?: TooltipType;
710
+ lastShapeType?: Props['lastShapeType'];
711
+ reversed?: boolean;
712
+ customWidth: number | string | undefined;
713
+ graphicalItemId: GraphicalItemId;
714
+ }): ReadonlyArray<FunnelTrapezoidItem> {
715
+ const { realHeight, realWidth, offsetX, offsetY } = getRealWidthHeight(customWidth, offset);
716
+ const values = displayedData.map(
717
+ (entry: unknown) => normalizeFunnelValue(getValueByDataKey(entry, dataKey, 0)),
718
+ );
719
+ const maxValue = Math.max.apply(null, values.map((entry) => entry.value));
720
+ const len = displayedData.length;
721
+ const rowHeight = realHeight / len;
722
+ const parentViewBox = {
723
+ x: offset.left,
724
+ y: offset.top,
725
+ width: offset.width,
726
+ height: offset.height,
727
+ };
728
+
729
+ let trapezoids: ReadonlyArray<FunnelTrapezoidItem> = displayedData.map((
730
+ entry: unknown,
731
+ i: number,
732
+ ): FunnelTrapezoidItem => {
733
+ const name: string = String(getValueByDataKey(entry, nameKey, i));
734
+ const normalizedValue = values[i];
735
+ const val = normalizedValue.value;
736
+ let nextVal: number;
737
+
738
+ if (normalizedValue.rangedNextValue !== undefined) {
739
+ nextVal = normalizedValue.rangedNextValue;
740
+ } else if (i !== len - 1) {
741
+ nextVal = values[i + 1].value;
742
+ } else if (lastShapeType === 'rectangle') {
743
+ nextVal = val;
744
+ } else {
745
+ nextVal = 0;
746
+ }
747
+
748
+ const x =
749
+ maxValue === 0 ? offsetX : ((maxValue - val) * realWidth) / (2 * maxValue) + offsetX;
750
+ const y = rowHeight * i + offsetY;
751
+ const upperWidth =
752
+ maxValue === 0 ? 0 : (val / maxValue) * realWidth;
753
+ const lowerWidth =
754
+ maxValue === 0 ? 0 : (nextVal / maxValue) * realWidth;
755
+
756
+ const tooltipPayload: TooltipPayload = [
757
+ { name, value: val, payload: entry, dataKey, type: tooltipType, graphicalItemId },
758
+ ];
759
+ const tooltipPosition: Coordinate = {
760
+ x: x + upperWidth / 2,
761
+ y: y + rowHeight / 2,
762
+ };
763
+
764
+ const trapezoidViewBox: TrapezoidViewBox = {
765
+ x,
766
+ y,
767
+ upperWidth,
768
+ lowerWidth,
769
+ width: Math.max(upperWidth, lowerWidth),
770
+ height: rowHeight,
771
+ };
772
+
773
+ return {
774
+ ...(entry != null && typeof entry === 'object' ? omit(entry, ['width']) : {}),
775
+ ...trapezoidViewBox,
776
+ name,
777
+ val,
778
+ value: val,
779
+ tooltipPayload,
780
+ tooltipPosition,
781
+ payload: entry,
782
+ parentViewBox,
783
+ labelViewBox: trapezoidViewBox,
784
+ };
785
+ });
786
+
787
+ if (reversed) {
788
+ trapezoids = trapezoids.map((
789
+ entry: FunnelTrapezoidItem,
790
+ index: number,
791
+ ): FunnelTrapezoidItem => {
792
+ const reversedViewBox: TrapezoidViewBox = {
793
+ x: entry.x - (entry.lowerWidth - entry.upperWidth) / 2,
794
+ y: entry.y - index * rowHeight + (len - 1 - index) * rowHeight,
795
+ upperWidth: entry.lowerWidth,
796
+ lowerWidth: entry.upperWidth,
797
+ width: Math.max(entry.lowerWidth, entry.upperWidth),
798
+ height: rowHeight,
799
+ };
800
+
801
+ return {
802
+ ...entry,
803
+ ...reversedViewBox,
804
+ tooltipPosition: {
805
+ ...entry.tooltipPosition,
806
+ y: entry.y - index * rowHeight + (len - 1 - index) * rowHeight + rowHeight / 2,
807
+ },
808
+ labelViewBox: reversedViewBox,
809
+ };
810
+ });
811
+ }
812
+
813
+ return trapezoids;
814
+ }
815
+
816
+ /**
817
+ * @consumes CartesianViewBoxContext
818
+ * @provides LabelListContext
819
+ * @provides CellReader
820
+ */
821
+ function FunnelFn(outsideProps: Props) {
822
+ const { id: externalId, ...props } = resolveDefaultProps(outsideProps, defaultFunnelProps);
823
+ return <RegisterGraphicalItemId id={externalId} type="funnel">
824
+ {(id) => <FunnelImpl {...props} id={id} />}
825
+ </RegisterGraphicalItemId>;
826
+ }
827
+
828
+ export const Funnel = FunnelFn as (outsideProps: Props<
829
+ DataPointType,
830
+ DataValueType
831
+ >) => ReactElement;
832
+ // @ts-expect-error we need to set the displayName for debugging purposes
833
+ Funnel.displayName = 'Funnel';