Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import AreaChart from './';
import { areaChartData } from './AreaChart.fixtures';

jest.unmock('./');

describe('AreaChart', () => {
it('renders chart with valid data', () => {
const { container } = render(
<AreaChart
data={areaChartData.data}
yAxisLabel={areaChartData.yAxisLabel}
/>
);

// Check for chart element by aria attributes (accessible name is the yAxisLabel)
expect(screen.getByRole('img', { name: areaChartData.yAxisLabel })).toBeInTheDocument();

// Verify chart container is present
expect(container.querySelector('svg')).toBeInTheDocument();
});

it('renders empty state when no data provided', () => {
render(<AreaChart data={null} />);

// Should show the no data message
expect(screen.getByText('No data available')).toBeInTheDocument();

// Should not render chart
expect(screen.queryByRole('img', { name: /area chart/i })).not.toBeInTheDocument();
});

it('renders empty state when empty data array provided', () => {
render(<AreaChart data={[]} />);

// Should show the no data message
expect(screen.getByText('No data available')).toBeInTheDocument();

// Should not render chart
expect(screen.queryByRole('img', { name: /area chart/i })).not.toBeInTheDocument();
});

it('displays custom noDataMsg', () => {
const customMsg = 'Custom no data message';
render(<AreaChart data={null} noDataMsg={customMsg} />);

// Should display the custom message
expect(screen.getByText(customMsg)).toBeInTheDocument();
});

it('applies custom size to chart', () => {
const customSize = { width: 500, height: 300 };
const { container } = render(
<AreaChart data={areaChartData.data} size={customSize} />
);

// Check the chart wrapper div has correct styles
const chartWrapper = container.querySelector('div[style*="height"]');
expect(chartWrapper).toHaveStyle({ height: '300px', width: '500px' });
});

it('renders with onclick callback', () => {
const onClickMock = jest.fn();
const { container } = render(
<AreaChart data={areaChartData.data} onclick={onClickMock} />
);

// Chart should render when onclick is provided
expect(container.querySelector('svg')).toBeInTheDocument();
});

it('renders all data series', () => {
const { container } = render(
<AreaChart
data={areaChartData.data}
yAxisLabel={areaChartData.yAxisLabel}
/>
);

// Verify the chart renders (contains SVG) - accessible name is the yAxisLabel
expect(screen.getByRole('img', { name: areaChartData.yAxisLabel })).toBeInTheDocument();

// The chart should be rendered with the data
expect(container.querySelector('svg')).toBeInTheDocument();
});

it('handles missing xAxisDataLabel gracefully', () => {
const dataWithoutTime = [
['nottime', 1614449768, 1614451500],
['CentOS 7.9', 3, 5],
];

render(
<AreaChart
data={dataWithoutTime}
xAxisDataLabel="time"
/>
);

// Should render empty state when time column is not found
expect(screen.getByText('No data available')).toBeInTheDocument();
});
});

Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React from 'react';
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { AreaChart as PfAreaChart } from 'patternfly-react';
import { getAreaChartConfig } from '../../../../../services/charts/AreaChartService';
import {
Chart,
ChartArea,
ChartAxis,
ChartGroup,
ChartStack,
ChartThemeColor,
ChartVoronoiContainer,
ChartLegend,
} from '@patternfly/react-charts';
import { noop } from '../../../../common/helpers';
import { translate as __ } from '../../../../common/I18n';
import MessageBox from '../../MessageBox';
Expand All @@ -11,35 +19,187 @@
onclick,
noDataMsg,
config,
unloadData,
unloadData, // eslint-disable-line no-unused-vars
xAxisDataLabel,
yAxisLabel,
size,
}) => {
const chartConfig = getAreaChartConfig({
data,
config,
onclick,
yAxisLabel,
xAxisDataLabel,
size,
});

if (chartConfig.data.columns.length) {
return <PfAreaChart {...chartConfig} unloadBeforeLoad={unloadData} />;
const chartData = useMemo(() => {
if (!data || data.length === 0) {
return null;
}

// Extract timestamps from first array
const timeColumn = data.find(col => col[0] === xAxisDataLabel);
if (!timeColumn) {
return null;
}

const timestamps = timeColumn.slice(1);
const dates = timestamps.map(epochSecs => new Date(epochSecs * 1000));

// Process other data columns
const series = data
.filter(col => col[0] !== xAxisDataLabel)
.map(col => {
const name = col[0];
const values = col.slice(1);
return {
name,
data: values.map((value, index) => ({
x: dates[index],
y: value,
name,
})),
};
});

return series.length > 0 ? series : null;
}, [data, xAxisDataLabel]);

const handleClick = useMemo(
() => (event, datum) => {
if (onclick && datum && datum.name) {
onclick({ id: datum.name });
}
},
[onclick]
);

if (!chartData) {
return <MessageBox msg={noDataMsg} icontype="info" />;
}
return <MessageBox msg={noDataMsg} icontype="info" />;

const chartHeight = size?.height || 250;
const chartWidth = size?.width || undefined;
const padding = { bottom: 75, left: 75, right: 50, top: 50 };

const legendData = chartData.map(series => ({ name: series.name }));

return (
<div style={{ height: chartHeight, width: chartWidth || '100%' }}>
<Chart
ariaDesc="Area chart"
ariaTitle={yAxisLabel || 'Area chart'}
containerComponent={
<ChartVoronoiContainer
labels={({ datum }) => {
const value = datum.y0 !== undefined ? datum.y - datum.y0 : datum.y;

Check failure on line 87 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Insert `⏎···············`

Check failure on line 87 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Insert `⏎···············`
const seriesName = datum.childName || datum.name;

Check failure on line 89 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Delete `··············`

Check failure on line 89 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Delete `··············`
// Show timestamp for the first series
const firstSeriesName = chartData[0]?.name;
const isFirstSeries = seriesName === firstSeriesName;

Check failure on line 93 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Delete `··············`

Check failure on line 93 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Delete `··············`
let label = '';

Check failure on line 95 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Delete `··············`

Check failure on line 95 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Delete `··············`
// Add timestamp as first line for first series only
if (isFirstSeries && datum.x) {
const date = datum.x instanceof Date ? datum.x : new Date(datum.x);

Check failure on line 98 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Insert `⏎·················`

Check failure on line 98 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Insert `⏎·················`
const dateStr = date.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' });

Check failure on line 99 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Replace `·month:·'numeric',·day:·'numeric'` with `⏎··················month:·'numeric',⏎··················day:·'numeric',⏎···············`

Check failure on line 99 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Replace `·month:·'numeric',·day:·'numeric'` with `⏎··················month:·'numeric',⏎··················day:·'numeric',⏎···············`
const timeStr = date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', hour12: true });

Check failure on line 100 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Replace `·hour:·'numeric',·minute:·'2-digit',·hour12:·true` with `⏎··················hour:·'numeric',⏎··················minute:·'2-digit',⏎··················hour12:·true,⏎···············`

Check failure on line 100 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Replace `·hour:·'numeric',·minute:·'2-digit',·hour12:·true` with `⏎··················hour:·'numeric',⏎··················minute:·'2-digit',⏎··················hour12:·true,⏎···············`
label = `${dateStr}, ${timeStr}\n`;
}

Check failure on line 103 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Delete `··············`

Check failure on line 103 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Delete `··············`
// Add series value if non-zero
if (value !== 0) {
const valueLine = seriesName ? `${seriesName}: ${Math.round(value)}` : `${Math.round(value)}`;

Check failure on line 106 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Replace `·?·`${seriesName}:·${Math.round(value)}`` with `⏎··················?·`${seriesName}:·${Math.round(value)}`⏎·················`

Check failure on line 106 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Replace `·?·`${seriesName}:·${Math.round(value)}`` with `⏎··················?·`${seriesName}:·${Math.round(value)}`⏎·················`
label += valueLine;
} else if (isFirstSeries) {
// Show first series even if zero to ensure timestamp displays
const valueLine = seriesName ? `${seriesName}: ${Math.round(value)}` : `${Math.round(value)}`;

Check failure on line 110 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 22)

Replace `·?·`${seriesName}:·${Math.round(value)}`` with `⏎··················?·`${seriesName}:·${Math.round(value)}`⏎·················`

Check failure on line 110 in webpack/assets/javascripts/react_app/components/common/charts/AreaChart/index.js

View workflow job for this annotation

GitHub Actions / test (13, 3.0, 18)

Replace `·?·`${seriesName}:·${Math.round(value)}`` with `⏎··················?·`${seriesName}:·${Math.round(value)}`⏎·················`
label += valueLine;
}

return label || null;
}}
constrainToVisibleArea
/>
}
height={chartHeight}
width={chartWidth}
padding={padding}
themeColor={ChartThemeColor.orange}
legendData={legendData}
legendOrientation="horizontal"
legendPosition="bottom"
legendComponent={<ChartLegend />}
events={[
{
target: 'data',
eventHandlers: {
onClick: () => [
{
target: 'data',
mutation: props => {
handleClick(null, props.datum);
return null;
},
},
],
},
},
]}
>
<ChartAxis
tickFormat={t => {
if (config === 'timeseries' && t instanceof Date) {
return t.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
});
}
return t;
}}
fixLabelOverlap
/>
<ChartAxis
dependentAxis
showGrid
label={yAxisLabel}
tickFormat={t => Math.round(t)}
/>
{config === 'timeseries' ? (
<ChartStack>
{chartData.map(series => (
<ChartArea key={series.name} data={series.data} name={series.name} />
))}
</ChartStack>
) : (
<ChartGroup>
{chartData.map(series => (
<ChartArea key={series.name} data={series.data} name={series.name} />
))}
</ChartGroup>
)}
</Chart>
</div>
);
};

AreaChart.propTypes = {
/** Array of data arrays. First element of each array is the label, rest are values.
* For timeseries, one array should have xAxisDataLabel as first element and timestamps as subsequent values.
* Example: [['time', 1614449768, 1614451500], ['CentOS 7.9', 3, 5], ['CentOS 7.6', 2, 1]]
*/
data: PropTypes.arrayOf(PropTypes.array),
/** Function called when clicking on data points. Receives object with 'id' property containing the series name. */
onclick: PropTypes.func,
/** Message to display when no data is available */
noDataMsg: PropTypes.string,
/** Chart configuration type. Only 'timeseries' is currently supported. */
config: PropTypes.oneOf(['timeseries']),
/** Legacy prop for compatibility - no longer used in PatternFly 5 */
unloadData: PropTypes.bool,
/** Label for the x-axis data column (must match first element of one data array) */
xAxisDataLabel: PropTypes.string,
/** Label for the y-axis */
yAxisLabel: PropTypes.string,
size: PropTypes.object,
/** Optional size object with width and/or height properties */
size: PropTypes.shape({
height: PropTypes.number,
width: PropTypes.number,
}),
};

AreaChart.defaultProps = {
Expand Down
Loading