Skip to content
Merged
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
21 changes: 20 additions & 1 deletion src/components/Chart.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import React, { useRef, useState } from 'react';
import './Chart.css';
import Button from './Button.jsx';
import EmptyState from './EmptyState.jsx';

export default function Chart({ data, title, formatValue }) {
export default function Chart({
data,
title,
emptyStateIcon = '📊',
emptyStateTitle = 'No chart data',
emptyStateMessage = 'Add some data to visualize.'
}) {
const chartRef = useRef(null);
const [hoveredIndex, setHoveredIndex] = useState(null);

Expand All @@ -21,6 +28,18 @@ export default function Chart({ data, title, formatValue }) {
URL.revokeObjectURL(url);
};

if (!data || data.length === 0) {
return (
<div className="chart-container">
<EmptyState
icon={emptyStateIcon}
title={emptyStateTitle}
message={emptyStateMessage}
/>
</div>
);
}

const maxValue = Math.max(...data.map(d => d.value), 1);
const barCount = data.length;

Expand Down
42 changes: 15 additions & 27 deletions src/components/Chart.stories.jsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,38 @@
import Chart from './Chart.jsx'
import { formatAmount } from '../utils/format.js'

export default {
title: 'Components/Chart',
component: Chart,
tags: ['autodocs'],
argTypes: {
title: { control: 'text' },
data: { control: 'object' },
formatValue: { control: false }
title: { control: 'text' },
emptyStateIcon: { control: 'text' },
emptyStateTitle: { control: 'text' },
emptyStateMessage: { control: 'text' }
}
}

const sampleData = [
{ value: 200, label: 'amina@example.com', currency: 'USD' },
{ value: 120, label: 'GBQAZ7Z3X7...', currency: 'USD' },
{ value: 450, label: 'chidi@example.com', currency: 'USD' },
{ value: 80, label: 'devi@example.com', currency: 'USD' },
{ value: 310, label: 'emeka@example.com', currency: 'USD' }
]

export const Default = {
args: {
title: 'Recent Transfer Amounts',
data: [
{ value: 200 },
{ value: 120 },
{ value: 450 },
{ value: 80 },
{ value: 310 }
]
data: [{ value: 10 }, { value: 20 }, { value: 15 }, { value: 30 }],
title: 'Monthly Transfers'
}
}

export const WithFormattedValues = {
export const Empty = {
args: {
title: 'Recent Transfer Amounts',
data: sampleData,
formatValue: (d) => formatAmount(d.value, d.currency)
data: [],
title: 'Monthly Transfers'
}
}

export const SingleBar = {
export const CustomEmptyState = {
args: {
title: 'Single Transfer',
data: [{ value: 250, label: 'juan@example.com', currency: 'USD' }],
formatValue: (d) => formatAmount(d.value, d.currency)
data: [],
title: 'Monthly Transfers',
emptyStateIcon: '💸',
emptyStateTitle: 'No transfers yet',
emptyStateMessage: 'Your first transfer will appear here.'
}
}
69 changes: 21 additions & 48 deletions test/components/Chart.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,40 @@ import { describe, expect, it } from 'vitest'
import Chart from '../../src/components/Chart.jsx'

describe('Chart component', () => {
it('renders title', () => {
it('renders correctly with data', () => {
const data = [{ value: 10 }, { value: 20 }]
render(<Chart data={data} title="Test Chart" />)
expect(screen.getByText('Test Chart')).toBeInTheDocument()
})

it('renders the correct number of bars', () => {
const data = [{ value: 10 }, { value: 20 }, { value: 30 }]
const { container } = render(<Chart data={data} title="Test" />)
const bars = container.querySelectorAll('.chart-bar')
expect(bars).toHaveLength(3)
it('shows empty state when data is an empty array', () => {
render(<Chart data={[]} title="Test Chart" />)
expect(screen.getByText('No chart data')).toBeInTheDocument()
expect(screen.getByText('Add some data to visualize.')).toBeInTheDocument()
expect(screen.queryByText('Download SVG')).not.toBeInTheDocument()
})

it('shows tooltip on bar hover', async () => {
const user = userEvent.setup()
const data = [{ value: 10 }, { value: 20 }]
const { container } = render(<Chart data={data} title="Test" />)
const bars = container.querySelectorAll('.chart-bar')

await user.hover(bars[0])
expect(screen.getByRole('tooltip')).toBeInTheDocument()

await user.unhover(bars[0])
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
it('shows empty state when data is null', () => {
render(<Chart data={null} title="Test Chart" />)
expect(screen.getByText('No chart data')).toBeInTheDocument()
})

it('displays raw value in tooltip by default', async () => {
const user = userEvent.setup()
const data = [{ value: 42 }, { value: 20 }]
const { container } = render(<Chart data={data} title="Test" />)
const bars = container.querySelectorAll('.chart-bar')

await user.hover(bars[0])
expect(screen.getByRole('tooltip')).toHaveTextContent('42')
it('shows empty state when data is undefined', () => {
render(<Chart title="Test Chart" />)
expect(screen.getByText('No chart data')).toBeInTheDocument()
})

it('displays formatted value when formatValue is provided', async () => {
const user = userEvent.setup()
const data = [{ value: 42, currency: 'USD' }, { value: 20 }]
const { container } = render(
it('uses custom empty state props when provided', () => {
render(
<Chart
data={data}
title="Test"
formatValue={(d) => `$${d.value.toFixed(2)} ${d.currency || ''}`.trim()}
data={[]}
title="Test Chart"
emptyStateIcon="💸"
emptyStateTitle="Nothing to show"
emptyStateMessage="Please add some data."
/>
)
const bars = container.querySelectorAll('.chart-bar')

await user.hover(bars[0])
expect(screen.getByRole('tooltip')).toHaveTextContent('$42.00 USD')
})

it('displays label in tooltip when data items have a label', async () => {
const user = userEvent.setup()
const data = [{ value: 100, label: 'alice@example.com' }, { value: 200 }]
const { container } = render(<Chart data={data} title="Test" />)
const bars = container.querySelectorAll('.chart-bar')

await user.hover(bars[0])
const tooltip = screen.getByRole('tooltip')
expect(tooltip).toHaveTextContent('100')
expect(tooltip).toHaveTextContent('alice@example.com')
expect(screen.getByText('Nothing to show')).toBeInTheDocument()
expect(screen.getByText('Please add some data.')).toBeInTheDocument()
})
})
Loading