Skip to content

Commit

Permalink
Add animation for ErrorBar (#4311)
Browse files Browse the repository at this point in the history
## Description

Added animation for ErrorBar component

## Related Issue

[ErrorBar does not
animate](#4055)

## Motivation and Context

Previously the ErrorBar didn't have animation, now animation has been
introduced to make it have a smoother visual effect

## How Has This Been Tested?

It was tested locally with Storybook and validated through unit tests

## Screenshots (if appropriate):


https://github.com/recharts/recharts/assets/41566276/32d7f85b-3e10-47de-87a8-ac5edd018072

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->

- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [ ] I have added a storybook story or extended an existing story to
show my changes
  • Loading branch information
ForestLinSen committed Mar 21, 2024
1 parent d2a0344 commit e7338c4
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 5 deletions.
47 changes: 42 additions & 5 deletions src/cartesian/ErrorBar.tsx
Expand Up @@ -3,10 +3,11 @@
*/
import React, { SVGProps } from 'react';
import invariant from 'tiny-invariant';
import Animate from 'react-smooth';
import { Layer } from '../container/Layer';
import { Props as XAxisProps } from './XAxis';
import { Props as YAxisProps } from './YAxis';
import { D3Scale, DataKey } from '../util/types';
import { AnimationTiming, D3Scale, DataKey } from '../util/types';
import { filterProps } from '../util/ReactUtils';
import { BarRectangleItem } from './Bar';
import { LinePointItem } from './Line';
Expand Down Expand Up @@ -43,12 +44,30 @@ interface ErrorBarProps extends InternalErrorBarProps {
* Only accepts a value of "x" or "y" and makes the error bars lie in that direction.
*/
direction?: 'x' | 'y';
isAnimationActive?: boolean;
animationBegin?: number;
animationDuration?: number;
animationEasing?: AnimationTiming;
}

export type Props = SVGProps<SVGLineElement> & ErrorBarProps;

export function ErrorBar(props: Props) {
const { offset, layout, width, dataKey, data, dataPointFormatter, xAxis, yAxis, ...others } = props;
const {
offset,
layout,
width,
dataKey,
data,
dataPointFormatter,
xAxis,
yAxis,
isAnimationActive,
animationBegin,
animationDuration,
animationEasing,
...others
} = props;
const svgProps = filterProps(others, false);

invariant(
Expand Down Expand Up @@ -114,9 +133,23 @@ export function ErrorBar(props: Props) {
key={`bar-${lineCoordinates.map(c => `${c.x1}-${c.x2}-${c.y1}-${c.y2}`)}`}
{...svgProps}
>
{lineCoordinates.map(coordinates => (
<line {...coordinates} key={`line-${coordinates.x1}-${coordinates.x2}-${coordinates.y1}-${coordinates.y2}`} />
))}
{lineCoordinates.map(coordinates => {
const lineStyle = isAnimationActive ? { transformOrigin: `${coordinates.x1 - 5}px` } : undefined;
return (
<Animate
from="scale(0, 1)"
to="scale(1, 1)"
attributeName="transform"
begin={animationBegin}
easing={animationEasing}
isActive={isAnimationActive}
duration={animationDuration}
key={`line-${coordinates.x1}-${coordinates.x2}-${coordinates.y1}-${coordinates.y2}`}
>
<line {...coordinates} style={lineStyle} />
</Animate>
);
})}
</Layer>
);
});
Expand All @@ -130,5 +163,9 @@ ErrorBar.defaultProps = {
width: 5,
offset: 0,
layout: 'horizontal',
isAnimationActive: true,
animationBegin: 0,
animationDuration: 200,
animationEasing: 'ease-in-out',
};
ErrorBar.displayName = 'ErrorBar';
100 changes: 100 additions & 0 deletions test/cartesian/ErrorBar.spec.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { Bar, BarChart, Line, LineChart, ErrorBar, XAxis, YAxis } from '../../src';
import { mockAnimation, cleanupMockAnimation } from '../helper/animation-frame-helper';

// asserts an error bar has both a start and end position
const assertErrorBars = (container: HTMLElement, barsExpected: number) => {
Expand All @@ -20,6 +21,27 @@ const assertErrorBars = (container: HTMLElement, barsExpected: number) => {
});
};

function assertAnimationStyles(
container: HTMLElement,
animation: boolean = true,
expectedStyles: { [key: string]: string } = {},
) {
const errorBars = container.querySelectorAll('.recharts-errorBar');
errorBars.forEach(bar => {
const lineElements = bar.querySelectorAll('line');
lineElements.forEach(line => {
if (animation) {
const style = line.getAttribute('style');
Object.entries(expectedStyles).forEach(([key, value]) => {
expect(style).toContain(`${key}: ${value}`);

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 18.x

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms ease-in-o…' - Expected + Received - transition: transform 200ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:147:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 18.x

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation delay

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms ease-in-o…' - Expected + Received - transition: transform 200ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:173:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 18.x

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation duration

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 400ms ease-in-o…' - Expected + Received - transition: transform 400ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:191:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 18.x

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation easing

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms linear' - Expected + Received - transition: transform 200ms linear + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:204:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Upload code coverage

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms ease-in-o…' - Expected + Received - transition: transform 200ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:147:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Upload code coverage

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation delay

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms ease-in-o…' - Expected + Received - transition: transform 200ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:173:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Upload code coverage

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation duration

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 400ms ease-in-o…' - Expected + Received - transition: transform 400ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:191:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Upload code coverage

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation easing

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms linear' - Expected + Received - transition: transform 200ms linear + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:204:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 20.x

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms ease-in-o…' - Expected + Received - transition: transform 200ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:147:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 20.x

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation delay

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms ease-in-o…' - Expected + Received - transition: transform 200ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:173:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 20.x

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation duration

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 400ms ease-in-o…' - Expected + Received - transition: transform 400ms ease-in-out + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:191:5

Check failure on line 36 in test/cartesian/ErrorBar.spec.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 20.x

test/cartesian/ErrorBar.spec.tsx > <ErrorBar /> > Renders Error Bars with animation easing

AssertionError: expected 'transform-origin: 56.25px; transform:…' to contain 'transition: transform 200ms linear' - Expected + Received - transition: transform 200ms linear + transform-origin: 56.25px; transform: scale(0, 1); ❯ test/cartesian/ErrorBar.spec.tsx:36:25 ❯ test/cartesian/ErrorBar.spec.tsx:35:40 ❯ test/cartesian/ErrorBar.spec.tsx:32:18 ❯ assertAnimationStyles test/cartesian/ErrorBar.spec.tsx:30:13 ❯ test/cartesian/ErrorBar.spec.tsx:204:5
});
} else {
expect(line.getAttribute('style')).toBeNull();
}
});
});
}

describe('<ErrorBar />', () => {
const barData = [
{ name: 'food', uv: 2000, pv: 2013, time: 1, uvError: [100, 50], pvError: [110, 20] },
Expand All @@ -28,6 +50,14 @@ describe('<ErrorBar />', () => {
{ name: 'digital', uv: 2800, pv: 2800, time: 4, uvError: [100, 200], pvError: 30 },
];

beforeAll(() => {
mockAnimation();
});

afterAll(() => {
cleanupMockAnimation();
});

test('Renders Error Bars in Bar', () => {
const { container } = render(
<BarChart data={barData} width={500} height={500}>
Expand Down Expand Up @@ -103,4 +133,74 @@ describe('<ErrorBar />', () => {

expect(container.querySelectorAll('.recharts-errorBar')).toHaveLength(10);
});

test('Renders Error Bars with animation', async () => {
const { container } = render(
<BarChart data={barData} width={500} height={500}>
<Bar isAnimationActive={false} dataKey="uv">
<ErrorBar isAnimationActive dataKey="uvError" />
</Bar>
</BarChart>,
);

assertErrorBars(container, 4);
assertAnimationStyles(container, true, { transition: 'transform 200ms ease-in-out' });
});

test('Renders Error Bars without animation', () => {
const { container } = render(
<BarChart data={barData} width={500} height={500}>
<Bar isAnimationActive={false} dataKey="uv">
<ErrorBar isAnimationActive={false} dataKey="uvError" />
</Bar>
</BarChart>,
);

assertErrorBars(container, 4);
assertAnimationStyles(container, false);
});

test('Renders Error Bars with animation delay', () => {
const { container } = render(
<BarChart data={barData} width={500} height={500}>
<Bar isAnimationActive={false} dataKey="uv">
<ErrorBar isAnimationActive begin={200} dataKey="uvError" />
</Bar>
</BarChart>,
);

assertErrorBars(container, 4);
assertAnimationStyles(container, true, { transition: 'transform 200ms ease-in-out' });

const errorBars = container.querySelectorAll('.recharts-errorBar');
errorBars.forEach(bar => {
expect(bar.getAttribute('begin')).toBe('200');
});
});

test('Renders Error Bars with animation duration', () => {
const { container } = render(
<BarChart data={barData} width={500} height={500}>
<Bar isAnimationActive={false} dataKey="uv">
<ErrorBar isAnimationActive animationDuration={400} dataKey="uvError" />
</Bar>
</BarChart>,
);

assertErrorBars(container, 4);
assertAnimationStyles(container, true, { transition: 'transform 400ms ease-in-out' });
});

test('Renders Error Bars with animation easing', () => {
const { container } = render(
<BarChart data={barData} width={500} height={500}>
<Bar isAnimationActive={false} dataKey="uv">
<ErrorBar isAnimationActive animationEasing="linear" dataKey="uvError" />
</Bar>
</BarChart>,
);

assertErrorBars(container, 4);
assertAnimationStyles(container, true, { transition: 'transform 200ms linear' });
});
});

0 comments on commit e7338c4

Please sign in to comment.