Range Area Custom Animation

Uses range data ([low, high]) so the area has an internal baseline path. Swap datasets with animationInterpolateFn enabled to compare the point animation with the baseline animation.
import { useId } from 'react';
import type { AnimationInterpolateFn, AreaPointItem, AreaRevealShapeProps, CartesianLayout } from 'recharts';
import {
  Area,
  AreaChart,
  AreaProps,
  AreaRevealShape,
  CartesianGrid,
  interpolate,
  Tooltip,
  usePlotArea,
  XAxis,
  YAxis,
} from 'recharts';
import type { Lever } from '../../../components/Shared/levers/Levers.tsx';
import { createSelectLever } from '../../../components/Shared/levers/Levers.tsx';
import { animationDurationLever } from '../../../components/Shared/levers/gallery/animationDurationLever.tsx';
import { replayAnimationLever } from '../../../components/Shared/levers/gallery/replayAnimationLever.tsx';
import { swapDataSetLever } from '../../../components/Shared/levers/gallery/swapDataLever.tsx';

const animationOptions = {
  leftToRight: { label: 'Left to right (default)' },
  topToBottom: { label: 'Top to bottom (custom)' },
} as const;

type AnimationStyle = keyof typeof animationOptions;

type ControlsType = {
  animationStyle: AnimationStyle;
  animationDuration: number;
  replayKey: number;
  dataSet: 'a' | 'b';
};

export const rangeAreaChartCustomAnimationDefaultState: ControlsType = {
  animationStyle: 'topToBottom',
  animationDuration: 1600,
  replayKey: 0,
  dataSet: 'a',
};

type MyDataType = {
  name: string;
  range: [number, number];
};

const dataA: ReadonlyArray<MyDataType> = [
  { name: 'Jan', range: [800, 2600] },
  { name: 'Feb', range: [1100, 3300] },
  { name: 'Mar', range: [900, 2800] },
  { name: 'Apr', range: [1400, 3900] },
  { name: 'May', range: [1200, 3500] },
  { name: 'Jun', range: [1600, 5000] },
];

const dataB: ReadonlyArray<MyDataType> = [
  { name: 'Jan', range: [400, 1800] },
  { name: 'Feb', range: [1500, 4200] },
  { name: 'Mar', range: [700, 2100] },
  { name: 'Apr', range: [1800, 4700] },
  { name: 'May', range: [1000, 2400] },
  { name: 'Jun', range: [2200, 5000] },
];

function useAnimateFromPlotBottom(): AnimationInterpolateFn<AreaPointItem, CartesianLayout> {
  const plotArea = usePlotArea();
  const top = plotArea?.y ?? 0;

  return (items, animationElapsedTime) => {
    if (items == null) {
      return [];
    }
    if (animationElapsedTime === 1) {
      return items.flatMap(item => (item.status === 'removed' ? [] : [item.next]));
    }

    return items.flatMap(item => {
      if (item.status === 'removed') {
        return [];
      }

      /*
       * For demonstration here we always animate from the top, and we ignore the previous position (item.prev.y)
       * and so the chart always looks like a new animation, there is no continuity.
       * Here it depends on what exactly is your goal;
       * Customize your animations the way you want!
       */
      return [{ ...item.next, y: interpolate(top, item.next.y, animationElapsedTime) }];
    });
  };
}

function CurveWithoutEntranceAnimation(props: AreaRevealShapeProps) {
  return <AreaRevealShape {...props} isEntrance={false} />;
}

function CustomAnimationArea(
  props: AreaProps<MyDataType, number | [number, number]> & { animationStyle: AnimationStyle },
) {
  const { animationStyle } = props;
  const topToBottomAnimation = useAnimateFromPlotBottom();
  const animationInterpolateFn = animationStyle === 'topToBottom' ? topToBottomAnimation : undefined;
  const shape = animationStyle === 'topToBottom' ? CurveWithoutEntranceAnimation : AreaRevealShape;

  return <Area animationInterpolateFn={animationInterpolateFn} shape={shape} {...props} />;
}

export const rangeAreaChartCustomAnimationLevers = [
  replayAnimationLever<ControlsType>(),
  swapDataSetLever<ControlsType>(),
  createSelectLever<ControlsType, AnimationStyle>({
    key: 'animationInterpolateFn',
    label: 'animationInterpolateFn',
    options: Object.entries(animationOptions).map(([value, option]) => ({
      value: value as AnimationStyle,
      label: option.label,
    })),
    getValue: state => state.animationStyle,
    onChange: (animationStyle, state) => ({ ...state, animationStyle }),
  }),
  animationDurationLever<ControlsType>(),
] satisfies ReadonlyArray<Lever<ControlsType>>;

export default function RangeAreaChartCustomAnimation(props: Partial<ControlsType>) {
  const { animationStyle, animationDuration, replayKey, dataSet } = {
    ...rangeAreaChartCustomAnimationDefaultState,
    ...props,
  };
  const gradientId1 = useId();
  const gradientId2 = useId();
  return (
    <AreaChart<MyDataType>
      key={replayKey}
      style={{ width: '100%', maxWidth: 700, aspectRatio: 1.618 }}
      responsive
      data={dataSet === 'a' ? dataA : dataB}
      margin={{ top: 10, right: 16, left: 0, bottom: 0 }}
    >
      <defs>
        <linearGradient id={gradientId1} x1="0" y1="0" x2="0" y2="1">
          <stop offset="5%" stopColor="#8884d8" stopOpacity={0.45} />
          <stop offset="95%" stopColor="#8884d8" stopOpacity={0.1} />
        </linearGradient>
        <linearGradient id={gradientId2} x1="0" y1="0" x2="0" y2="1">
          <stop offset="5%" stopColor="#84d888" stopOpacity={0.45} />
          <stop offset="95%" stopColor="#84d888" stopOpacity={0.1} />
        </linearGradient>
      </defs>
      <CartesianGrid />
      <XAxis dataKey="name" />
      <YAxis width="auto" domain={[0, 5200]} />
      <Tooltip />
      <CustomAnimationArea
        type="linear"
        dataKey="range"
        stroke="#8884d8"
        strokeWidth={2}
        fill={`url(#${gradientId1})`}
        fillOpacity={1}
        isAnimationActive
        animationDuration={animationDuration}
        animationStyle={animationStyle}
      />
      <CustomAnimationArea
        type="linear"
        dataKey={p => p.range[1] * 2}
        baseValue="dataMax"
        stroke="#84d888"
        strokeWidth={2}
        fill={`url(#${gradientId2})`}
        fillOpacity={1}
        isAnimationActive
        animationDuration={animationDuration}
        animationStyle={animationStyle}
      />
    </AreaChart>
  );
}
1600