add custom-grid and associated files + test - test needs plugin config to work with d3-path and jest

This commit is contained in:
Loewy 2024-01-10 12:30:46 -08:00
parent 92258f4925
commit 4dda9eb24c
8 changed files with 261 additions and 4 deletions

48
component/bar.tsx Normal file
View File

@ -0,0 +1,48 @@
import React from 'react';
import { Path } from 'react-native-svg';
import { calculateBarProps, drawBarPath } from './custom-bar-utils';
import { ScaleFunction } from './graph-types';
interface BarProps {
scaleX: ScaleFunction;
scaleY: ScaleFunction;
data: { value: number };
barNumber: number;
index: number;
fill: string;
barWidth: number;
gap: number;
roundedRadius: number;
}
export const Bar: React.FC<BarProps> = ({
scaleX,
scaleY,
data,
barNumber,
index,
fill,
barWidth,
gap,
roundedRadius
}) => {
const { xOrigin, yOrigin, height } = calculateBarProps({
scaleX,
scaleY,
index,
data,
barNumber,
barWidth,
gap
});
return (
<Path
key={`bar-${barNumber}-${index}`}
d={drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius)}
fill={fill}
testID={`bar-${barNumber}-${index}`}
/>
);
};

View File

@ -0,0 +1,59 @@
import { path as d3 } from 'd3-path';
import { ScaleFunction } from './graph-types';
type BarCalculationProps = {
scaleX: ScaleFunction;
scaleY: ScaleFunction;
data: { value: number };
barNumber: number;
index: number;
barWidth: number;
gap: number;
};
export function calculateBarProps({
scaleX,
scaleY,
barWidth,
data,
index,
barNumber,
gap
}: BarCalculationProps): { xOrigin: number; yOrigin: number; height: number } {
const firstBar = barNumber === 0;
const xOrigin = scaleX(index) + (firstBar ? 0 : barWidth * barNumber + gap * barNumber);
const yOrigin = scaleY(data.value);
const height = scaleY(0) - yOrigin;
return { xOrigin, yOrigin, height };
}
export function drawBarPath(
xOrigin: number,
yOrigin: number,
barWidth: number,
height: number,
roundedRadius: number
): string {
const path = d3();
path.moveTo(xOrigin, yOrigin + height);
path.lineTo(xOrigin, yOrigin + roundedRadius);
path.arcTo(xOrigin, yOrigin, xOrigin + roundedRadius, yOrigin, roundedRadius);
path.lineTo(xOrigin + barWidth - roundedRadius, yOrigin);
path.arcTo(
xOrigin + barWidth,
yOrigin,
xOrigin + barWidth,
yOrigin + roundedRadius,
roundedRadius
);
path.lineTo(xOrigin + barWidth, yOrigin + height);
path.lineTo(xOrigin, yOrigin + height);
path.closePath();
return path.toString();
}
export const calculateBarWidth = (bandwidth: number, combinedDataLength: number, gap: number) =>
(bandwidth - gap * (combinedDataLength - 1)) / combinedDataLength;

54
component/custom-bars.tsx Normal file
View File

@ -0,0 +1,54 @@
import { Svg } from 'react-native-svg';
import { Bar } from './bar';
import { ScaleFunction } from './graph-types';
import { calculateBarWidth } from './custom-bar-utils';
export interface CombinedData {
data: { value: number }[];
svg: { fill: string };
}
interface CustomBarsProps {
x: ScaleFunction;
y: ScaleFunction;
bandwidth: number;
barColors: string[];
rawData: unknown[]; // TODO: update this value when this data type is defined
combinedData: CombinedData[];
gap: number;
roundedRadius: number;
}
export const CustomBars: React.FC<Partial<CustomBarsProps>> = ({
x: scaleX,
y: scaleY,
bandwidth,
combinedData,
rawData,
barColors,
gap = 2,
roundedRadius = 4
}) => {
const barWidth = calculateBarWidth(bandwidth, combinedData.length, gap);
return rawData.map((_, index) => (
<Svg key={`group-${index}`} testID={`svg-${index}`}>
{combinedData.map((item, i) => (
<Bar
key={`bar-${i}-${index}`}
scaleX={scaleX}
scaleY={scaleY}
data={item.data[index]}
barNumber={i}
index={index}
fill={barColors[i]}
barWidth={barWidth}
gap={gap}
roundedRadius={roundedRadius}
/>
))}
</Svg>
));
};

View File

@ -1,12 +1,10 @@
import React from 'react';
import { G, Line } from 'react-native-svg';
import { ScaleLinear } from 'd3-scale'
import { colors } from '../styles';
type scaleFunction = ScaleLinear<number, number>;
import { ScaleFunction } from './graph-types';
interface CustomGridProps {
y: scaleFunction;
y: ScaleFunction;
ticks: Array<number>;
}

3
component/graph-types.ts Normal file
View File

@ -0,0 +1,3 @@
import { ScaleLinear } from 'd3-scale'
export type ScaleFunction = ScaleLinear<number, number>;

View File

@ -23,6 +23,7 @@
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"babel-plugin-inline-dotenv": "^1.7.0",
"d3-path": "^3.1.0",
"d3-scale": "1.0.6",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
@ -43,6 +44,7 @@
"@babel/core": "^7.20.0",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^12.4.3",
"@types/d3-path": "^3.0.2",
"@types/jest": "^29.5.11",
"eslint-config-prettier": "^9.1.0"
},

View File

@ -0,0 +1,83 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import "@testing-library/jest-native/extend-expect";
import { CustomBars } from '../../component/custom-bars';
describe('CustomBars Component Tests', () => {
const mockScaleFunction = jest.fn();
const mockCombinedData = [
{
data: [{ value: 10 }],
svg: { fill: 'red' },
},
{
data: [{ value: 20 }],
svg: { fill: 'blue' },
},
];
const mockRawData = [{}, {}];
const mockBarColors = ['red', 'blue'];
const mockBandwidth = 100;
const mockGap = 2;
const mockRoundedRadius = 4;
it('should render correct number of Svg and Bar components', () => {
const { getAllByTestId } = render(
<CustomBars
x={mockScaleFunction}
y={mockScaleFunction}
bandwidth={mockBandwidth}
combinedData={mockCombinedData}
rawData={mockRawData}
barColors={mockBarColors}
gap={mockGap}
roundedRadius={mockRoundedRadius}
/>
);
const svgs = getAllByTestId(/svg-/);
const bars = getAllByTestId(/bar-/);
expect(svgs.length).toHaveLength(mockRawData.length);
expect(bars.length).toHaveLength(mockCombinedData.length * mockRawData.length);
});
it('should pass correct props to Bar components', () => {
const { getAllByTestId } = render(
<CustomBars
x={mockScaleFunction}
y={mockScaleFunction}
bandwidth={mockBandwidth}
combinedData={mockCombinedData}
rawData={mockRawData}
barColors={mockBarColors}
gap={mockGap}
roundedRadius={mockRoundedRadius}
/>
);
const bars = getAllByTestId(/bar-/); // Regex pattern to match testIDs of Bar components
bars.forEach((bar) => {
// Extract indices from the testID
const testId = bar.props.testID;
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const [_, barNumber, index] = testId.split('-').map(Number);
const item = mockCombinedData[barNumber];
const data = item.data[index];
expect(bar.props).toEqual({
scaleX: mockScaleFunction,
scaleY: mockScaleFunction,
data: data,
barNumber: barNumber,
index: index,
fill: mockBarColors[barNumber],
barWidth: expect.any(Number),
gap: mockGap,
roundedRadius: mockRoundedRadius,
});
});
});
});

View File

@ -2296,6 +2296,11 @@
dependencies:
"@babel/types" "^7.20.7"
"@types/d3-path@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.2.tgz#4327f4a05d475cf9be46a93fc2e0f8d23380805a"
integrity sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==
"@types/graceful-fs@^4.1.3":
version "4.1.9"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4"
@ -3683,6 +3688,11 @@ d3-interpolate@1:
dependencies:
d3-color "1"
d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-scale@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.6.tgz#bce19da80d3a0cf422c9543ae3322086220b34ed"