Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chart Library: add bar chart component tests + improve data validation #41296

Merged
merged 9 commits into from
Jan 29, 2025
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Charts: adds tests and fixes to bar chart component
35 changes: 26 additions & 9 deletions projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ const BarChart: FC< BarChartProps > = ( {
) => {
const coords = localPoint( event );
if ( ! coords ) return;

showTooltip( {
tooltipData: { value, xLabel, yLabel, seriesIndex },
tooltipLeft: coords.x,
Expand All @@ -58,12 +57,25 @@ const BarChart: FC< BarChartProps > = ( {
[ showTooltip ]
);

const handleMouseLeave = useCallback( () => {
hideTooltip();
}, [ hideTooltip ] );

// Check for empty data
if ( ! data?.length ) {
return <div className={ clsx( 'bar-chart-empty', styles[ 'bat-chart-empty' ] ) }>Empty...</div>;
return <div className={ clsx( styles[ 'bar-chart-empty' ] ) }>No data available</div>;
}

// Add date validation to hasInvalidData check
const hasInvalidData = data.some( series =>
series.data.some(
d =>
d.value === null ||
d.value === undefined ||
isNaN( d.value ) ||
! d.label ||
( d.date && isNaN( d.date.getTime() ) ) // Add date validation
)
);

if ( hasInvalidData ) {
return <div className={ clsx( styles[ 'bar-chart-error' ] ) }>Invalid data</div>;
}

const margins = margin;
Expand Down Expand Up @@ -102,7 +114,12 @@ const BarChart: FC< BarChartProps > = ( {
} ) );

return (
<div className={ clsx( 'bar-chart', className, styles[ 'bar-chart' ] ) }>
annacmc marked this conversation as resolved.
Show resolved Hide resolved
<div
className={ clsx( 'bar-chart', styles[ 'bar-chart' ], className ) }
data-testid="bar-chart"
role="img"
aria-label="bar chart"
>
<svg width={ width } height={ height }>
<Group left={ margins.left } top={ margins.top }>
<GridControl
Expand Down Expand Up @@ -133,7 +150,7 @@ const BarChart: FC< BarChartProps > = ( {
height={ yMax - ( yScale( d.value ) ?? 0 ) }
fill={ theme.colors[ seriesIndex % theme.colors.length ] }
onMouseMove={ withTooltips ? handleBarMouseMove : undefined }
onMouseLeave={ withTooltips ? handleMouseLeave : undefined }
onMouseLeave={ withTooltips ? hideTooltip : undefined }
/>
);
} ) }
Expand All @@ -159,7 +176,7 @@ const BarChart: FC< BarChartProps > = ( {
<Legend
items={ legendItems }
orientation={ legendOrientation }
className={ styles[ 'bar-chart-legend' ] }
className={ styles[ 'bar-chart__legend' ] }
/>
) }
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,43 @@ export const FixedDimensions: Story = {
},
},
};

export const ErrorStates: StoryObj< typeof BarChart > = {
render: () => (
<div style={ { display: 'grid', gap: '20px' } }>
<div>
<h3>Empty Data</h3>
<div style={ { width: '400px', height: '300px' } }>
<BarChart data={ [] } />
</div>
</div>

<div>
<h3>Invalid Data</h3>
<div style={ { width: '400px', height: '300px' } }>
<BarChart
data={ [
{
label: 'Invalid Series',
data: [
{ date: new Date( 'invalid' ), value: 10, label: 'Invalid Date' },
{ date: new Date( '2024-01-02' ), value: null, label: 'Null Value' },
],
options: {},
},
] }
/>
</div>
</div>
</div>
),
};

ErrorStates.parameters = {
docs: {
description: {
story:
'Examples of how the bar chart handles various error states including empty data and invalid data.',
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* @jest-environment jsdom
*/

import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '../../../providers/theme';
import BarChart from '../bar-chart';

describe( 'BarChart', () => {
const defaultProps = {
width: 500,
height: 300,
data: [
{
label: 'Series A',
data: [
{ date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' },
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
],
options: {},
},
],
};

const renderWithTheme = ( props = {} ) => {
return render(
<ThemeProvider>
<BarChart { ...defaultProps } { ...props } />
</ThemeProvider>
);
};

describe( 'Data Validation', () => {
test( 'handles empty data array', () => {
renderWithTheme( { data: [] } );
expect( screen.getByText( /no data available/i ) ).toBeInTheDocument();
} );

test( 'handles single data point', () => {
renderWithTheme( {
data: [
{
label: 'Series A',
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
options: {},
},
],
} );
expect( screen.getByRole( 'img', { name: /bar chart/i } ) ).toBeInTheDocument();
} );

test( 'handles negative values', () => {
renderWithTheme( {
data: [
{
label: 'Series A',
data: [
{ date: new Date( '2024-01-01' ), value: -10, label: 'Jan 1' },
{ date: new Date( '2024-01-02' ), value: -20, label: 'Jan 2' },
],
options: {},
},
],
} );
expect( screen.getByRole( 'img', { name: /bar chart/i } ) ).toBeInTheDocument();
} );

test( 'handles null or undefined values', () => {
renderWithTheme( {
data: [
{
label: 'Series A',
data: [
{ date: new Date( '2024-01-01' ), value: null as number | null, label: 'Jan 1' },
{
date: new Date( '2024-01-02' ),
value: undefined as number | undefined,
label: 'Jan 2',
},
],
options: {},
},
],
} );
expect( screen.getByText( /invalid data/i ) ).toBeInTheDocument();
} );

test( 'handles invalid date values', () => {
renderWithTheme( {
data: [
{
label: 'Series A',
data: [
{ date: new Date( 'invalid' ), value: 10, label: 'Jan 1' },
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
],
options: {},
},
],
} );
expect( screen.getByText( /invalid data/i ) ).toBeInTheDocument();
} );

test( 'handles invalid label values', () => {
renderWithTheme( {
data: [
{
label: 'Series A',
data: [
{ label: '', value: 10 }, // Empty label
{ label: 'Label 2', value: 20 },
],
options: {},
},
],
} );
expect( screen.getByText( /invalid data/i ) ).toBeInTheDocument();
} );
} );

describe( 'Legend', () => {
test( 'shows legend when showLegend is true', () => {
renderWithTheme( {
showLegend: true,
data: [
{
label: 'Series A',
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
options: {},
},
{
label: 'Series B',
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
options: {},
},
],
} );
expect( screen.getByText( 'Series A' ) ).toBeInTheDocument();
expect( screen.getByText( 'Series B' ) ).toBeInTheDocument();
} );

test( 'hides legend when showLegend is false', () => {
renderWithTheme( {
showLegend: false,
data: [
{
label: 'Series A',
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
options: {},
},
],
} );
expect( screen.queryByText( 'Series A' ) ).not.toBeInTheDocument();
} );
} );

describe( 'Grid Visibility', () => {
test( 'renders with different grid visibility options', () => {
const { rerender } = renderWithTheme( { gridVisibility: 'x' } );
expect( screen.getByRole( 'img', { name: /bar chart/i } ) ).toBeInTheDocument();

rerender(
<ThemeProvider>
<BarChart { ...defaultProps } gridVisibility="y" />
</ThemeProvider>
);
expect( screen.getByRole( 'img', { name: /bar chart/i } ) ).toBeInTheDocument();

rerender(
<ThemeProvider>
<BarChart { ...defaultProps } gridVisibility="xy" />
</ThemeProvider>
);
expect( screen.getByRole( 'img', { name: /bar chart/i } ) ).toBeInTheDocument();
} );
} );
} );