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
83 changes: 83 additions & 0 deletions src/components/common/PriceSparkline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { cn } from '@/lib/utils';

interface PriceSparklineProps {
dataPoints: number[];
width?: number;
height?: number;
className?: string;
}

const POSITIVE_COLOR = '#34d399';
const NEUTRAL_COLOR = 'currentColor';

export function PriceSparkline({
dataPoints,
width = 120,
height = 40,
className,
}: PriceSparklineProps) {
if (dataPoints.length === 0) return null;

const lineColor =
dataPoints.length >= 2 && dataPoints[dataPoints.length - 1] > dataPoints[0]
? POSITIVE_COLOR
: NEUTRAL_COLOR;

const padding = 2;
const innerWidth = width - padding * 2;
const innerHeight = height - padding * 2;

if (dataPoints.length === 1) {
return (
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
className={cn('overflow-visible', className)}
role="img"
aria-label="Price sparkline"
>
<circle
cx={width / 2}
cy={height / 2}
r={2}
fill={lineColor}
/>
</svg>
);
}

const min = Math.min(...dataPoints);
const max = Math.max(...dataPoints);
const range = max - min || 1;

const buildPathD = () =>
dataPoints
.map((value, index) => {
const x = padding + (index / (dataPoints.length - 1)) * innerWidth;
const y =
padding + (1 - (value - min) / range) * innerHeight;
return `${index === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}`;
})
.join(' ');

return (
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
className={cn('overflow-visible', className)}
role="img"
aria-label="Price sparkline"
>
<path
d={buildPathD()}
fill="none"
stroke={lineColor}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
49 changes: 49 additions & 0 deletions src/components/common/__tests__/PriceSparkline.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { render } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { PriceSparkline } from '../PriceSparkline';

describe('PriceSparkline', () => {
it('renders an SVG path for 7 data points', () => {
const { container } = render(
<PriceSparkline
dataPoints={[10, 20, 15, 25, 30, 22, 35]}
/>
);

const svg = container.querySelector('svg');
expect(svg).toBeInTheDocument();

const path = container.querySelector('path');
expect(path).toBeInTheDocument();
expect(path).toHaveAttribute('d');
});

it('renders without error for 1 data point and does not produce a line path', () => {
const { container } = render(
<PriceSparkline dataPoints={[42]} />
);

const svg = container.querySelector('svg');
expect(svg).toBeInTheDocument();

const path = container.querySelector('path');
expect(path).not.toBeInTheDocument();
});

it('renders nothing for 0 data points', () => {
const { container } = render(
<PriceSparkline dataPoints={[]} />
);

expect(container.innerHTML).toBe('');
});

it('applies green line colour when the last value is higher than the first', () => {
const { container } = render(
<PriceSparkline dataPoints={[10, 20, 30]} />
);

const path = container.querySelector('path');
expect(path).toHaveAttribute('stroke', '#34d399');
});
});
Loading