diff --git a/component/bar.tsx b/component/bar.tsx new file mode 100644 index 0000000..97cdc41 --- /dev/null +++ b/component/bar.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Path } from 'react-native-svg'; + +import { calculateBarOrigin, 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 = ({ + scaleX, + scaleY, + data, + barNumber, + index, + fill, + barWidth, + gap, + roundedRadius +}) => { + const { xOrigin, yOrigin, height } = calculateBarOrigin({ + scaleX, + scaleY, + index, + data, + barNumber, + barWidth, + gap + }); + + return ( + + ); +}; diff --git a/component/custom-bar-utils.ts b/component/custom-bar-utils.ts new file mode 100644 index 0000000..8e50593 --- /dev/null +++ b/component/custom-bar-utils.ts @@ -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 calculateBarOrigin({ + 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; \ No newline at end of file diff --git a/component/custom-bars.tsx b/component/custom-bars.tsx new file mode 100644 index 0000000..36e2b52 --- /dev/null +++ b/component/custom-bars.tsx @@ -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 = ({ + x: scaleX, + y: scaleY, + bandwidth, + combinedData, + rawData, + barColors, + gap = 2, + roundedRadius = 4 +}) => { + const barWidth = calculateBarWidth(bandwidth, combinedData.length, gap); + + return rawData.map((_, index) => ( + + {combinedData.map((item, i) => ( + + ))} + + )); +}; \ No newline at end of file diff --git a/component/custom-grid.tsx b/component/custom-grid.tsx index 453f749..dfb6fb8 100644 --- a/component/custom-grid.tsx +++ b/component/custom-grid.tsx @@ -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; +import { ScaleFunction } from './graph-types'; interface CustomGridProps { - y: scaleFunction; + y: ScaleFunction; ticks: Array; } diff --git a/component/graph-types.ts b/component/graph-types.ts new file mode 100644 index 0000000..345312a --- /dev/null +++ b/component/graph-types.ts @@ -0,0 +1,3 @@ +import { ScaleLinear } from 'd3-scale' + +export type ScaleFunction = ScaleLinear; \ No newline at end of file diff --git a/package.json b/package.json index 02b63a4..831368d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "jest": { "preset": "jest-expo", "transformIgnorePatterns": [ - "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)" + "node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|d3-path)/)" ] }, "dependencies": { @@ -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" }, diff --git a/test/component/custom-bars.test.tsx b/test/component/custom-bars.test.tsx new file mode 100644 index 0000000..fe62815 --- /dev/null +++ b/test/component/custom-bars.test.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import "@testing-library/jest-native/extend-expect"; +import * as scale from 'd3-scale' + +import { CustomBars } from '../../component/custom-bars'; +import { calculateBarOrigin, drawBarPath } from '../../component/custom-bar-utils'; + + +const mockYScaleFunction = scale.scaleLinear(); +const mockXScaleFunction = scale.scaleBand(); +const mockBandwidth = 100; +const mockCombinedData = [ + { + data: [{ value: 10 }, { value: 20 }], + svg: { fill: 'red' }, + }, +]; +const mockRawData = [ + { label: 'Full ball', shotsTaken: 10, makePercentage: 20 }, +]; +const mockBarColors = ['red', 'blue']; +const mockGap = 2; +const mockRoundedRadius = 4; +describe('CustomBars Component Tests', () => { + it('should render correct number of Svg and Bar components', () => { + const { getAllByTestId } = render( + + ); + + const svgs = getAllByTestId(/svg-/); + const bars = getAllByTestId(/bar-/); + + expect(svgs.length).toBe(mockRawData.length); + expect(bars.length).toBe(mockCombinedData.length * mockRawData.length); + }); + +}); + +describe('Bar utility functions', () => { + describe('calculateBarOrigin', () => { + it('calculates properties correctly', () => { + const mockData = { value: 10 }; + const mockIndex = 1; + const mockBarNumber = 2; + const mockBarWidth = 20; + const mockGap = 5; + + const result = calculateBarOrigin({ + scaleX: mockXScaleFunction, + scaleY: mockYScaleFunction, + data: mockData, + index: mockIndex, + barNumber: mockBarNumber, + barWidth: mockBarWidth, + gap: mockGap + }); + expect(result).toEqual({ + xOrigin: expect.any(Number), + yOrigin: expect.any(Number), + height: expect.any(Number) + }); + }); + }); + + describe('drawBarPath', () => { + it('generates a correct SVG path string', () => { + const xOrigin = 50; + const yOrigin = 100; + const barWidth = 20; + const height = 60; + const roundedRadius = 10; + + const path = drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius); + const expectedPath = 'M50,160L50,110A10,10,0,0,1,60,100L60,100A10,10,0,0,1,70,110L70,160L50,160Z' + + expect(path).toBe(expectedPath); + }); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 54d9078..621786f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"