Actually respect prettier configuration

This commit is contained in:
Ivan Malison 2024-02-03 20:23:31 -07:00
parent a6883a624a
commit 2276605e6d
37 changed files with 1629 additions and 1497 deletions

View File

@ -21,5 +21,7 @@ jobs:
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn run lint'
- name: typecheck
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn tsc --noEmit'
- name: prettier
run: nix develop --impure --command bash -c 'export HOME=$PWD; prettier . --check'
- name: test
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn run test --no-watchman'

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
/android
/ios
/react-native-vision-camera
flake.lock

View File

@ -1,44 +1,44 @@
{
"expo": {
"name": "Railbird",
"slug": "railbird-rn",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"plugins": [
"@react-native-firebase/app",
"@react-native-firebase/auth",
[
"expo-build-properties",
{
"ios": {
"useFrameworks": "static"
}
}
]
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "ai.railbird.railbird",
"googleServicesFile": "./GoogleService-Info.plist"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "android.railbird.app",
"googleServicesFile": "./google-services.json"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
"expo": {
"name": "Railbird",
"slug": "railbird-rn",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"plugins": [
"@react-native-firebase/app",
"@react-native-firebase/auth",
[
"expo-build-properties",
{
"ios": {
"useFrameworks": "static"
}
}
]
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "ai.railbird.railbird",
"googleServicesFile": "./GoogleService-Info.plist"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "android.railbird.app",
"googleServicesFile": "./google-services.json"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}

View File

@ -1,116 +1,137 @@
import React from 'react';
import * as scale from 'd3-scale';
import { View } from 'react-native';
import { BarChart, XAxis, YAxis } from 'react-native-svg-charts';
import React from "react";
import * as scale from "d3-scale";
import { View } from "react-native";
import { BarChart, XAxis, YAxis } from "react-native-svg-charts";
import { useGraphData } from '../use-graph-data';
import { GraphData, YAxisProps } from '../graph-types';
import { useGraphData } from "../use-graph-data";
import { GraphData, YAxisProps } from "../graph-types";
import { CustomBars } from '../custom-bars';
import { CustomGrid } from '../custom-grid';
import { graphStyles } from '../chart-styles';
import ChartView from '../chart-view';
import ChartLabel from '../chart-label/chart-label';
import { CustomBars } from "../custom-bars";
import { CustomGrid } from "../custom-grid";
import { graphStyles } from "../chart-styles";
import ChartView from "../chart-view";
import ChartLabel from "../chart-label/chart-label";
interface Props {
data: GraphData;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
testID?: string;
data: GraphData;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
testID?: string;
}
// TODO: separate PR will update useGraphData to take into account useCommonScale
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
export const BarGraph: React.FC<Props> = ({ data, useCommonScale = false, testID, ...props }) => {
if (!data) {
return null
} // TODO:#38
const {
xValues,
yData,
yAxisRightLabels,
yAxisLeftLabels,
defaultProps: {
height,
spacingInner,
spacingOuter,
contentInset,
min,
numberOfTicks,
barColors,
yAxisProps: {
maxLeftYAxisValue,
maxRightYAxisValue,
formatRightYAxisLabel,
formatLeftYAxisLabel
}
}
// Proper error/loading handling from useQueryHandler can work with this rule #38
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useGraphData(data, { includeColors: false, ...props});
export const BarGraph: React.FC<Props> = ({
data,
useCommonScale = false,
testID,
...props
}) => {
if (!data) {
return null;
} // TODO:#38
const {
xValues,
yData,
yAxisRightLabels,
yAxisLeftLabels,
defaultProps: {
height,
spacingInner,
spacingOuter,
contentInset,
min,
numberOfTicks,
barColors,
yAxisProps: {
maxLeftYAxisValue,
maxRightYAxisValue,
formatRightYAxisLabel,
formatLeftYAxisLabel,
},
},
// Proper error/loading handling from useQueryHandler can work with this rule #38
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useGraphData(data, { includeColors: false, ...props });
// TODO: Will come from BE & destructured / color assigned in useGraphData
const yLabels = [{ displayName: 'Shots Taken', axis: 'LEFT' as 'LEFT', color: barColors[0] }, { displayName:'Make Percentage', axis: 'RIGHT' as 'RIGHT', color: barColors[1] }]
const title = 'Shots Taken / Make Percentage by Cut Angle'
// TODO: Will come from BE & destructured / color assigned in useGraphData
const yLabels = [
{ displayName: "Shots Taken", axis: "LEFT" as "LEFT", color: barColors[0] },
{
displayName: "Make Percentage",
axis: "RIGHT" as "RIGHT",
color: barColors[1],
},
];
const title = "Shots Taken / Make Percentage by Cut Angle";
return (
<ChartView>
<ChartLabel title={title} yLabels={yLabels} />
<View style={[graphStyles.rowContainer, { height: height }]} testID={`bar-graph-${testID}`}>
<YAxis
data={yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={graphStyles.yAxisLeftPadding}
min={min}
max={maxLeftYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatLeftYAxisLabel}
/>
return (
<ChartView>
<ChartLabel title={title} yLabels={yLabels} />
<View
style={[graphStyles.rowContainer, { height: height }]}
testID={`bar-graph-${testID}`}
>
<YAxis
data={yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={graphStyles.yAxisLeftPadding}
min={min}
max={maxLeftYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatLeftYAxisLabel}
/>
<View style={graphStyles.flex}>
<BarChart
style={graphStyles.flex}
data={yData}
gridMin={min}
numberOfTicks={numberOfTicks} // rethink numberOfTicks, it should be determined automatically if we do our y axis scaling properly
yAccessor={({ item }) => (item as unknown as { value: number }).value}
contentInset={contentInset}
spacingInner={spacingInner}
spacingOuter={spacingOuter}
>
<CustomGrid />
<CustomBars barData={yData} xValues={xValues} barColors={barColors} />
</BarChart>
<XAxis
data={xValues.map((_, index) => index)}
formatLabel={(_, index) => xValues[index]}
style={graphStyles.xAxisMarginTop}
svg={graphStyles.xAxisFontStyle}
scale={scale.scaleBand}
/>
</View>
<View style={graphStyles.flex}>
<BarChart
style={graphStyles.flex}
data={yData}
gridMin={min}
numberOfTicks={numberOfTicks} // rethink numberOfTicks, it should be determined automatically if we do our y axis scaling properly
yAccessor={({ item }) =>
(item as unknown as { value: number }).value
}
contentInset={contentInset}
spacingInner={spacingInner}
spacingOuter={spacingOuter}
>
<CustomGrid />
<CustomBars
barData={yData}
xValues={xValues}
barColors={barColors}
/>
</BarChart>
<XAxis
data={xValues.map((_, index) => index)}
formatLabel={(_, index) => xValues[index]}
style={graphStyles.xAxisMarginTop}
svg={graphStyles.xAxisFontStyle}
scale={scale.scaleBand}
/>
</View>
<YAxis
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={graphStyles.yAxisRightPadding}
min={min}
max={maxRightYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatRightYAxisLabel}
/>
</View>
</ChartView>
);
<YAxis
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={graphStyles.yAxisRightPadding}
min={min}
max={maxRightYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatRightYAxisLabel}
/>
</View>
</ChartView>
);
};
export default BarGraph;

View File

@ -1,48 +1,48 @@
import React from 'react';
import { Path } from 'react-native-svg';
import React from "react";
import { Path } from "react-native-svg";
import { calculateBarOrigin, drawBarPath } from './custom-bar-utils';
import { ScaleBandType, ScaleLinearType, } from './graph-types';
import { calculateBarOrigin, drawBarPath } from "./custom-bar-utils";
import { ScaleBandType, ScaleLinearType } from "./graph-types";
interface BarProps {
scaleX: ScaleBandType;
scaleY: ScaleLinearType;
data: { value: number };
barNumber: number;
index: number;
fill: string;
barWidth: number;
gap: number;
roundedRadius: number;
scaleX: ScaleBandType;
scaleY: ScaleLinearType;
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
scaleX,
scaleY,
data,
barNumber,
index,
fill,
barWidth,
gap,
roundedRadius,
}) => {
const { xOrigin, yOrigin, height } = calculateBarOrigin({
scaleX,
scaleY,
index,
data,
barNumber,
barWidth,
gap
});
const { xOrigin, yOrigin, height } = calculateBarOrigin({
scaleX,
scaleY,
index,
data,
barNumber,
barWidth,
gap,
});
return (
<Path
key={`bar-path-${barNumber}-${index}`}
d={drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius)}
fill={fill}
testID={`bar-${barNumber}-${index}`}
/>
);
return (
<Path
key={`bar-path-${barNumber}-${index}`}
d={drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius)}
fill={fill}
testID={`bar-${barNumber}-${index}`}
/>
);
};

View File

@ -1,42 +1,44 @@
import React from 'react';
import { Text, View } from 'react-native';
import React from "react";
import { Text, View } from "react-native";
import { chartLabel } from '../chart-styles';
import { chartLabel } from "../chart-styles";
type Axis = 'RIGHT' | 'LEFT'
type Axis = "RIGHT" | "LEFT";
interface YLabel {
axis: Axis;
displayName: string;
color: string;
axis: Axis;
displayName: string;
color: string;
}
type ChartLabelProps = {
title: string;
yLabels: Array<YLabel>;
title: string;
yLabels: Array<YLabel>;
};
const renderLabels = (yLabels: Array<YLabel>) => {
return yLabels.map((label) => (
<View key={`${label.axis}-${label.displayName}`} style={chartLabel.labelInnerRow}>
<View
style={[chartLabel.labelColorBox, { backgroundColor: label.color }]}
/>
<View style={chartLabel.labelTextMargin}>
<Text style={chartLabel.labelText}>{label.displayName}</Text>
</View>
</View>
));
return yLabels.map((label) => (
<View
key={`${label.axis}-${label.displayName}`}
style={chartLabel.labelInnerRow}
>
<View
style={[chartLabel.labelColorBox, { backgroundColor: label.color }]}
/>
<View style={chartLabel.labelTextMargin}>
<Text style={chartLabel.labelText}>{label.displayName}</Text>
</View>
</View>
));
};
export default function ChartLabel({ title, yLabels }: ChartLabelProps) {
return (
<View>
<View style={chartLabel.titleRow}>
<Text style={chartLabel.titleText}>{title}</Text>
</View>
<View style={chartLabel.labelOuterRow}>{renderLabels(yLabels)}</View>
</View>
);
return (
<View>
<View style={chartLabel.titleRow}>
<Text style={chartLabel.titleText}>{title}</Text>
</View>
<View style={chartLabel.labelOuterRow}>{renderLabels(yLabels)}</View>
</View>
);
}

View File

@ -1,46 +1,46 @@
import { StyleSheet } from 'react-native';
import { StyleSheet } from "react-native";
import { colors, shadows } from '../../styles';
import { colors, shadows } from "../../styles";
export const graphStyles = StyleSheet.create({
container: {
backgroundColor: colors.panelWhite,
borderColor: 'black',
borderRadius: 5,
marginVertical: 10,
marginHorizontal: 15,
paddingTop: 15,
paddingHorizontal: 15,
...shadows.standard
},
rowContainer: { flexDirection: 'row', padding: 10 },
flex: { flex: 1 },
yAxisLeftPadding: { paddingRight: 3 },
yAxisRightPadding: { paddingLeft: 3 },
yAxisFontStyle: { fontSize: 10, fill: 'grey' },
xAxisFontStyle: { fontSize: 12, fill: 'black' },
xAxisMarginTop: { marginTop: -15 },
horizontalInset: { right: 10, left: 10 },
container: {
backgroundColor: colors.panelWhite,
borderColor: "black",
borderRadius: 5,
marginVertical: 10,
marginHorizontal: 15,
paddingTop: 15,
paddingHorizontal: 15,
...shadows.standard,
},
rowContainer: { flexDirection: "row", padding: 10 },
flex: { flex: 1 },
yAxisLeftPadding: { paddingRight: 3 },
yAxisRightPadding: { paddingLeft: 3 },
yAxisFontStyle: { fontSize: 10, fill: "grey" },
xAxisFontStyle: { fontSize: 12, fill: "black" },
xAxisMarginTop: { marginTop: -15 },
horizontalInset: { right: 10, left: 10 },
});
export const chartLabel = StyleSheet.create({
titleRow: {
flexDirection: 'row',
marginHorizontal: 10
},
titleText: { fontWeight: '500' },
labelOuterRow: {
flexDirection: 'row',
flexWrap: 'wrap',
marginHorizontal: 10
},
labelInnerRow: { flexDirection: 'row', alignItems: 'center', marginTop: 5 },
labelColorBox: {
height: 15,
width: 15,
borderRadius: 4,
marginRight: 2
},
labelTextMargin: { marginRight: 15 },
labelText: { fontSize: 12 }
titleRow: {
flexDirection: "row",
marginHorizontal: 10,
},
titleText: { fontWeight: "500" },
labelOuterRow: {
flexDirection: "row",
flexWrap: "wrap",
marginHorizontal: 10,
},
labelInnerRow: { flexDirection: "row", alignItems: "center", marginTop: 5 },
labelColorBox: {
height: 15,
width: 15,
borderRadius: 4,
marginRight: 2,
},
labelTextMargin: { marginRight: 15 },
labelText: { fontSize: 12 },
});

View File

@ -1,16 +1,20 @@
import React from 'react';
import { StyleProp, View, ViewStyle } from 'react-native';
import React from "react";
import { StyleProp, View, ViewStyle } from "react-native";
import { graphStyles } from './chart-styles';
import { graphStyles } from "./chart-styles";
interface ChartViewProps {
style?: StyleProp<ViewStyle>;
children: React.ReactNode;
testID?: string;
style?: StyleProp<ViewStyle>;
children: React.ReactNode;
testID?: string;
}
const ChartView: React.FC<ChartViewProps> = ({ children, style, testID }) => {
return <View style={[graphStyles.container, style]} testID={testID} >{children}</View>;
return (
<View style={[graphStyles.container, style]} testID={testID}>
{children}
</View>
);
};
export default ChartView;

View File

@ -1,59 +1,63 @@
import { path as d3 } from 'd3-path';
import { path as d3 } from "d3-path";
import { ScaleLinearType, ScaleBandType } from './graph-types';
import { ScaleLinearType, ScaleBandType } from "./graph-types";
type BarCalculationProps = {
scaleX: ScaleBandType;
scaleY: ScaleLinearType;
data: { value: number };
barNumber: number;
index: number;
barWidth: number;
gap: number;
scaleX: ScaleBandType;
scaleY: ScaleLinearType;
data: { value: number };
barNumber: number;
index: number;
barWidth: number;
gap: number;
};
export function calculateBarOrigin({
scaleX,
scaleY,
barWidth,
data,
index,
barNumber,
gap
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;
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 };
return { xOrigin, yOrigin, height };
}
export function drawBarPath(
xOrigin: number,
yOrigin: number,
barWidth: number,
height: number,
roundedRadius: number
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();
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();
return path.toString();
}
export const calculateBarWidth = (bandwidth: number, combinedDataLength: number, gap: number) =>
(bandwidth - gap * (combinedDataLength - 1)) / combinedDataLength;
export const calculateBarWidth = (
bandwidth: number,
combinedDataLength: number,
gap: number,
) => (bandwidth - gap * (combinedDataLength - 1)) / combinedDataLength;

View File

@ -1,54 +1,53 @@
import { Svg } from "react-native-svg";
import { Svg } from 'react-native-svg';
import { Bar } from './bar';
import { ScaleBandType, ScaleLinearType } from './graph-types';
import { calculateBarWidth } from './custom-bar-utils';
import { Bar } from "./bar";
import { ScaleBandType, ScaleLinearType } from "./graph-types";
import { calculateBarWidth } from "./custom-bar-utils";
export interface CombinedData {
data: { value: number }[];
svg: { fill: string };
data: { value: number }[];
svg: { fill: string };
}
interface CustomBarsProps {
x: ScaleBandType;
y: ScaleLinearType;
bandwidth: number;
barColors: string[];
xValues: unknown[]; // TODO: update this value when this data type is defined
barData: CombinedData[];
gap: number;
roundedRadius: number;
x: ScaleBandType;
y: ScaleLinearType;
bandwidth: number;
barColors: string[];
xValues: unknown[]; // TODO: update this value when this data type is defined
barData: CombinedData[];
gap: number;
roundedRadius: number;
}
export const CustomBars: React.FC<Partial<CustomBarsProps>> = ({
x: scaleX,
y: scaleY,
bandwidth,
barData,
xValues,
barColors,
gap = 2,
roundedRadius = 4
x: scaleX,
y: scaleY,
bandwidth,
barData,
xValues,
barColors,
gap = 2,
roundedRadius = 4,
}) => {
const barWidth = calculateBarWidth(bandwidth, barData.length, gap);
const barWidth = calculateBarWidth(bandwidth, barData.length, gap);
return xValues.map((_, index) => (
<Svg key={`group-${index}`} testID={`svg-${index}`}>
{barData.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>
));
return xValues.map((_, index) => (
<Svg key={`group-${index}`} testID={`svg-${index}`}>
{barData.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,14 +1,14 @@
import React from 'react';
import { G, Line } from 'react-native-svg';
import { colors } from '../../styles';
import { ScaleBandType, ScaleLinearType } from './graph-types';
import React from "react";
import { G, Line } from "react-native-svg";
import { colors } from "../../styles";
import { ScaleBandType, ScaleLinearType } from "./graph-types";
interface CustomGridProps {
x: ScaleBandType;
y: ScaleLinearType;
ticks: Array<number>;
includeVertical?: boolean;
xTicks?: Array<number | string>;
x: ScaleBandType;
y: ScaleLinearType;
ticks: Array<number>;
includeVertical?: boolean;
xTicks?: Array<number | string>;
}
/**
* CustomGrid Component
@ -34,55 +34,74 @@ interface CustomGridProps {
* Note: Use `includeVertical` cautiously; vertical lines are not fully developed.
*/
export const CustomGrid: React.FC<Partial<CustomGridProps>> = ({
x,
y,
ticks,
xTicks,
includeVertical = false
x,
y,
ticks,
xTicks,
includeVertical = false,
}) => {
const [firstTick, ...remainingTicks] = ticks;
const dashArray = [1, 3];
const strokeSolidWidth = 0.2;
const strokeSolidColor = colors.bgBlack;
const strokeDashWidth = 1;
const strokeDashColor = colors.lightGrey;
const [firstTick, ...remainingTicks] = ticks;
const dashArray = [1, 3];
const strokeSolidWidth = 0.2;
const strokeSolidColor = colors.bgBlack;
const strokeDashWidth = 1;
const strokeDashColor = colors.lightGrey;
const renderHorizontalLine = (tick: number, stroke: string, strokeWidth: number, dashArray?: number[]) => (
<Line
key={`line-${tick}`}
x1="0%"
x2="100%"
y1={y(tick)}
y2={y(tick)}
stroke={stroke}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
/>
);
const renderHorizontalLine = (
tick: number,
stroke: string,
strokeWidth: number,
dashArray?: number[],
) => (
<Line
key={`line-${tick}`}
x1="0%"
x2="100%"
y1={y(tick)}
y2={y(tick)}
stroke={stroke}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
/>
);
const topY = y(Math.max(...ticks));
const bottomY = y(Math.min(...ticks));
const renderVerticalLine = (tick: number, stroke: string, strokeWidth: number, dashArray?: number[]) => {
return (
<Line
key={`vertical-line-${tick}`}
x1={x(tick)}
x2={x(tick)}
y1={topY}
y2={bottomY}
stroke={stroke}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
/>
);
};
const topY = y(Math.max(...ticks));
const bottomY = y(Math.min(...ticks));
const renderVerticalLine = (
tick: number,
stroke: string,
strokeWidth: number,
dashArray?: number[],
) => {
return (
<Line
key={`vertical-line-${tick}`}
x1={x(tick)}
x2={x(tick)}
y1={topY}
y2={bottomY}
stroke={stroke}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
/>
);
};
return (
<G>
{renderHorizontalLine(firstTick, strokeSolidColor, strokeSolidWidth)}
{remainingTicks.map((tick) => renderHorizontalLine(tick, strokeDashColor, strokeDashWidth, dashArray))}
{includeVertical && xTicks.map((_, index) => renderVerticalLine(index, strokeDashColor, strokeDashWidth, dashArray))}
</G>
);
return (
<G>
{renderHorizontalLine(firstTick, strokeSolidColor, strokeSolidWidth)}
{remainingTicks.map((tick) =>
renderHorizontalLine(tick, strokeDashColor, strokeDashWidth, dashArray),
)}
{includeVertical &&
xTicks.map((_, index) =>
renderVerticalLine(
index,
strokeDashColor,
strokeDashWidth,
dashArray,
),
)}
</G>
);
};

View File

@ -1,13 +1,13 @@
// TODO: Style values should be moved to styles
// non-style config can live here as chartDefaults
export const chartDefaults = {
height: 300,
spacingInner: 0.3,
spacingOuter: 0.2,
contentInset: { top: 30, bottom: 30 },
numberOfTicks: 6,
min: 0,
barColors: ['#598EBB', '#F2D4BC', '#DB7878'],
includeColors: true,
lineStrokeWidth: 2
height: 300,
spacingInner: 0.3,
spacingOuter: 0.2,
contentInset: { top: 30, bottom: 30 },
numberOfTicks: 6,
min: 0,
barColors: ["#598EBB", "#F2D4BC", "#DB7878"],
includeColors: true,
lineStrokeWidth: 2,
};

View File

@ -1,50 +1,50 @@
import { ScaleBand, ScaleLinear } from 'd3-scale'
import { ScaleBand, ScaleLinear } from "d3-scale";
export type ScaleLinearType = ScaleLinear<number, number>;
export type ScaleBandType = ScaleBand<number | string>;
export interface YAxisData {
key: string; // string value for ChartLabel and useGraphData
values: Array<number>;
// including this code only for review --
// do we prefer the idea of passing label formatting from the data or in the component?
// generic function type, specific usage of value varies
// eslint-disable-next-line no-unused-vars
formatLabel?: (value: number) => string;
key: string; // string value for ChartLabel and useGraphData
values: Array<number>;
// including this code only for review --
// do we prefer the idea of passing label formatting from the data or in the component?
// generic function type, specific usage of value varies
// eslint-disable-next-line no-unused-vars
formatLabel?: (value: number) => string;
}
export type XValue = string | number
export type XValue = string | number;
export interface GraphData {
xValues: Array<XValue>;
yValues: Array<YAxisData>;
xValues: Array<XValue>;
yValues: Array<YAxisData>;
}
export interface YAxisProps {
maxLeftYAxisValue?: number;
maxRightYAxisValue?: number;
selectedLeftYAxisLabel?: string;
selectedRightYAxisLabel?: string;
// generic function type, specific usage of value varies
// eslint-disable-next-line no-unused-vars
formatRightYAxisLabel?: (value: string) => string;
// generic function type, specific usage of value varies
// eslint-disable-next-line no-unused-vars
formatLeftYAxisLabel?: (value: string) => string;
maxLeftYAxisValue?: number;
maxRightYAxisValue?: number;
selectedLeftYAxisLabel?: string;
selectedRightYAxisLabel?: string;
// generic function type, specific usage of value varies
// eslint-disable-next-line no-unused-vars
formatRightYAxisLabel?: (value: string) => string;
// generic function type, specific usage of value varies
// eslint-disable-next-line no-unused-vars
formatLeftYAxisLabel?: (value: string) => string;
}
export interface GraphProps {
data: GraphData;
includeColors?: boolean;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
lineStrokeWidth?: number;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
data: GraphData;
includeColors?: boolean;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
lineStrokeWidth?: number;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
}
/**
* Common properties for graph render components.
@ -63,16 +63,16 @@ export interface GraphProps {
* @property {string} [testID] - Optional. Identifier for testing purposes.
*/
export interface CommonProps {
data: GraphData;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
lineStrokeWidth?: number;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
testID?: string;
data: GraphData;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
lineStrokeWidth?: number;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
testID?: string;
}

View File

@ -1,43 +1,52 @@
import { GraphData } from "./graph-types";
export const convertToGraphData = (
graphData: GraphData,
options: {
selectedLeftYAxisLabel?: string;
selectedRightYAxisLabel?: string;
maxRightYAxisValue: number;
maxLeftYAxisValue: number;
includeColors: boolean;
barColors: Array<string>
}
graphData: GraphData,
options: {
selectedLeftYAxisLabel?: string;
selectedRightYAxisLabel?: string;
maxRightYAxisValue: number;
maxLeftYAxisValue: number;
includeColors: boolean;
barColors: Array<string>;
},
) => {
const xValues = graphData.xValues;
const leftAxisIndex = graphData.yValues.findIndex(
(y) => y.key === options.selectedLeftYAxisLabel
);
const rightAxisIndex = graphData.yValues.findIndex(
(y) => y.key === options.selectedRightYAxisLabel
);
const xValues = graphData.xValues;
const leftAxisIndex = graphData.yValues.findIndex(
(y) => y.key === options.selectedLeftYAxisLabel,
);
const rightAxisIndex = graphData.yValues.findIndex(
(y) => y.key === options.selectedRightYAxisLabel,
);
// scale data points according to max value
const yData = graphData.yValues.map((yAxis, index) => {
const maxValue = index === rightAxisIndex ? options.maxRightYAxisValue : options.maxLeftYAxisValue;
// scale data points according to max value
const yData = graphData.yValues.map((yAxis, index) => {
const maxValue =
index === rightAxisIndex
? options.maxRightYAxisValue
: options.maxLeftYAxisValue;
// scale values as a percentage of the max value
const scaledValues = yAxis.values.map((value) => (value / maxValue) * 100);
// scale values as a percentage of the max value
const scaledValues = yAxis.values.map((value) => (value / maxValue) * 100);
const strokeColor = options.includeColors && options.barColors ? options.barColors[index % options.barColors.length] : 'transparent';
const strokeColor =
options.includeColors && options.barColors
? options.barColors[index % options.barColors.length]
: "transparent";
const mappedData = scaledValues.map((scaledValue) => ({ value: scaledValue }));
const mappedData = scaledValues.map((scaledValue) => ({
value: scaledValue,
}));
return {
data: mappedData,
svg: { fill: 'transparent', stroke: strokeColor } // Apply the stroke color here
};
});
const yAxisLeftLabels = leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null;
const yAxisRightLabels = rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined;
return {
data: mappedData,
svg: { fill: "transparent", stroke: strokeColor }, // Apply the stroke color here
};
});
const yAxisLeftLabels =
leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null;
const yAxisRightLabels =
rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined;
return { yData, yAxisLeftLabels, yAxisRightLabels, xValues };
return { yData, yAxisLeftLabels, yAxisRightLabels, xValues };
};

View File

@ -1,90 +1,103 @@
import React from 'react';
import * as shape from 'd3-shape';
import { View } from 'react-native';
import { LineChart, XAxis, YAxis } from 'react-native-svg-charts';
import React from "react";
import * as shape from "d3-shape";
import { View } from "react-native";
import { LineChart, XAxis, YAxis } from "react-native-svg-charts";
import { useGraphData } from '../use-graph-data';
import { CustomGrid } from '../custom-grid';
import { graphStyles } from '../chart-styles';
import ChartView from '../chart-view';
import { CommonProps } from '../graph-types';
import { useGraphData } from "../use-graph-data";
import { CustomGrid } from "../custom-grid";
import { graphStyles } from "../chart-styles";
import ChartView from "../chart-view";
import { CommonProps } from "../graph-types";
// TODO: separate PR will update useGraphData to take into account useCommonScale
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const LineGraph: React.FC<CommonProps> = ({ data, useCommonScale = false, testID, ...props }) => {
if (!data || typeof data !== 'object') {
return null
}; // TODO:#38
const {
xValues,
yData,
yAxisLeftLabels,
yAxisRightLabels,
defaultProps: {
height,
contentInset,
min,
numberOfTicks,
lineStrokeWidth,
yAxisProps: {
maxLeftYAxisValue,
maxRightYAxisValue,
formatLeftYAxisLabel,
formatRightYAxisLabel
}
}
// Proper error/loading handling from useQueryHandler can work with this rule #38
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useGraphData(data, props);
const LineGraph: React.FC<CommonProps> = ({
data,
useCommonScale = false,
testID,
...props
}) => {
if (!data || typeof data !== "object") {
return null;
} // TODO:#38
const {
xValues,
yData,
yAxisLeftLabels,
yAxisRightLabels,
defaultProps: {
height,
contentInset,
min,
numberOfTicks,
lineStrokeWidth,
yAxisProps: {
maxLeftYAxisValue,
maxRightYAxisValue,
formatLeftYAxisLabel,
formatRightYAxisLabel,
},
},
// Proper error/loading handling from useQueryHandler can work with this rule #38
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useGraphData(data, props);
return (
<ChartView>
<View style={[graphStyles.rowContainer, { height: height }]} testID={`line-graph-${testID}`}>
<YAxis
data={yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={graphStyles.yAxisLeftPadding}
min={min}
max={maxLeftYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatLeftYAxisLabel}
/>
<View style={graphStyles.flex}>
<LineChart
style={graphStyles.flex}
data={yData}
curve={shape.curveNatural}
svg={{ strokeWidth: lineStrokeWidth}}
yAccessor={({ item }) => (item as unknown as { value: number }).value}
xAccessor={({ index }) => xValues[index] as number}
gridMin={min}
contentInset={contentInset}
numberOfTicks={numberOfTicks}
>
<CustomGrid />
</LineChart>
<XAxis
data={xValues.map((_, index: number) => index)} // TODO: update when useGraphHook returns explicit display values
style={[graphStyles.xAxisMarginTop]}
svg={graphStyles.xAxisFontStyle}
numberOfTicks={numberOfTicks}
contentInset={graphStyles.horizontalInset}
/>
</View>
<YAxis
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={[graphStyles.yAxisRightPadding, { height: useCommonScale ? 0 : 'auto' }]}
min={min}
max={maxRightYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatRightYAxisLabel} // formatRightYAxisLabel formatting could come from yAxisRightLabels object
/>
</View>
</ChartView>
);
return (
<ChartView>
<View
style={[graphStyles.rowContainer, { height: height }]}
testID={`line-graph-${testID}`}
>
<YAxis
data={yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={graphStyles.yAxisLeftPadding}
min={min}
max={maxLeftYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatLeftYAxisLabel}
/>
<View style={graphStyles.flex}>
<LineChart
style={graphStyles.flex}
data={yData}
curve={shape.curveNatural}
svg={{ strokeWidth: lineStrokeWidth }}
yAccessor={({ item }) =>
(item as unknown as { value: number }).value
}
xAccessor={({ index }) => xValues[index] as number}
gridMin={min}
contentInset={contentInset}
numberOfTicks={numberOfTicks}
>
<CustomGrid />
</LineChart>
<XAxis
data={xValues.map((_, index: number) => index)} // TODO: update when useGraphHook returns explicit display values
style={[graphStyles.xAxisMarginTop]}
svg={graphStyles.xAxisFontStyle}
numberOfTicks={numberOfTicks}
contentInset={graphStyles.horizontalInset}
/>
</View>
<YAxis
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={[
graphStyles.yAxisRightPadding,
{ height: useCommonScale ? 0 : "auto" },
]}
min={min}
max={maxRightYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatRightYAxisLabel} // formatRightYAxisLabel formatting could come from yAxisRightLabels object
/>
</View>
</ChartView>
);
};
export default LineGraph;

View File

@ -1,57 +1,70 @@
import { useMemo } from 'react';
import { convertToGraphData } from './graph-utils';
import { chartDefaults } from './graph-config';
import { GraphData, GraphProps, XValue, YAxisData } from './graph-types';
import { useMemo } from "react";
import { convertToGraphData } from "./graph-utils";
import { chartDefaults } from "./graph-config";
import { GraphData, GraphProps, XValue, YAxisData } from "./graph-types";
interface useGraphDataInterface {
xValues: Array<XValue>;
yData: {
data: {
value: number;
}[];
svg: {
fill: string;
};
}[];
yAxisLeftLabels: YAxisData;
yAxisRightLabels: YAxisData;
defaultProps: Partial<GraphProps>;
xValues: Array<XValue>;
yData: {
data: {
value: number;
}[];
svg: {
fill: string;
};
}[];
yAxisLeftLabels: YAxisData;
yAxisRightLabels: YAxisData;
defaultProps: Partial<GraphProps>;
}
// this version assumes string values for X, this isn't necessarily the case
// convertToGraphData is specifically tailored to bar/group bar graphs
// ultimately this component could be used by any x & y axis graph types (line/bar/scatter)
export const useGraphData = (graphData: GraphData, props: Partial<GraphProps>): useGraphDataInterface => {
const { yAxisProps = {}, ...otherProps } = props;
const defaultProps = {
...chartDefaults,
...otherProps,
// assign default values for yAxisProps + spread to override with values coming from props
yAxisProps: {
maxLeftYAxisValue: Math.max(...(graphData.yValues[0]?.values ?? [0])),
maxRightYAxisValue:
graphData.yValues.length > 1 ? Math.max(...graphData.yValues[1]?.values) : undefined,
formatRightYAxisLabel: yAxisProps.formatRightYAxisLabel,
formatLeftYAxisLabel: yAxisProps.formatLeftYAxisLabel,
selectedLeftYAxisLabel: graphData.yValues[0]?.key,
selectedRightYAxisLabel: graphData.yValues[1]?.key,
...yAxisProps
}
};
export const useGraphData = (
graphData: GraphData,
props: Partial<GraphProps>,
): useGraphDataInterface => {
const { yAxisProps = {}, ...otherProps } = props;
const defaultProps = {
...chartDefaults,
...otherProps,
// assign default values for yAxisProps + spread to override with values coming from props
yAxisProps: {
maxLeftYAxisValue: Math.max(...(graphData.yValues[0]?.values ?? [0])),
maxRightYAxisValue:
graphData.yValues.length > 1
? Math.max(...graphData.yValues[1]?.values)
: undefined,
formatRightYAxisLabel: yAxisProps.formatRightYAxisLabel,
formatLeftYAxisLabel: yAxisProps.formatLeftYAxisLabel,
selectedLeftYAxisLabel: graphData.yValues[0]?.key,
selectedRightYAxisLabel: graphData.yValues[1]?.key,
...yAxisProps,
},
};
const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo(
() => convertToGraphData(graphData, { ...defaultProps.yAxisProps, includeColors: defaultProps.includeColors, barColors: defaultProps.barColors}),
[graphData, defaultProps.yAxisProps, defaultProps.includeColors, defaultProps.barColors]
);
const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo(
() =>
convertToGraphData(graphData, {
...defaultProps.yAxisProps,
includeColors: defaultProps.includeColors,
barColors: defaultProps.barColors,
}),
[
graphData,
defaultProps.yAxisProps,
defaultProps.includeColors,
defaultProps.barColors,
],
);
return {
xValues,
yData,
yAxisLeftLabels,
yAxisRightLabels,
defaultProps
};
return {
xValues,
yData,
yAxisLeftLabels,
yAxisRightLabels,
defaultProps,
};
};

View File

@ -1,169 +1,177 @@
import React, { useCallback, useRef, useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
import {
Camera,
useCameraPermission,
useCameraDevice,
useCameraFormat,
PhotoFile,
VideoFile,
CameraRuntimeError,
Orientation,
// @ts-ignore
Camera,
useCameraPermission,
useCameraDevice,
useCameraFormat,
PhotoFile,
VideoFile,
CameraRuntimeError,
Orientation,
// @ts-ignore
} from "react-native-vision-camera";
import { RecordingButton } from "./capture-button";
import { useIsForeground } from "./is-foreground";
import { useIsFocused } from "@react-navigation/native";
export default function CameraScreen({ route, navigation }): React.ReactElement {
// TODO: #73 Does this need to be passed to Camera component?
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const { gameType, tableSize, tags, location } = route.params
// LOG for params -- Remove when no longer needed
// Note: camelCased value being passed, change on record.tsx if you want a different value format
console.log(gameType, tableSize, tags, location)
export default function CameraScreen({
route,
navigation,
}): React.ReactElement {
// TODO: #73 Does this need to be passed to Camera component?
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const { gameType, tableSize, tags, location } = route.params;
// LOG for params -- Remove when no longer needed
// Note: camelCased value being passed, change on record.tsx if you want a different value format
console.log(gameType, tableSize, tags, location);
const camera = useRef<Camera>(null);
const { hasPermission, requestPermission } = useCameraPermission();
const [isCameraInitialized, setIsCameraInitialized] =
useState<boolean>(false);
const camera = useRef<Camera>(null);
const { hasPermission, requestPermission } = useCameraPermission();
const [isCameraInitialized, setIsCameraInitialized] =
useState<boolean>(false);
const isForeground = useIsForeground();
const isFocused = useIsFocused();
const isActive = isForeground && isFocused;
const isForeground = useIsForeground();
const isFocused = useIsFocused();
const isActive = isForeground && isFocused;
const onError = useCallback((error: CameraRuntimeError) => {
console.error(error);
}, []);
const onError = useCallback((error: CameraRuntimeError) => {
console.error(error);
}, []);
const onInitialized = useCallback(() => {
console.log("Camera initialized!");
setIsCameraInitialized(true);
}, []);
const onInitialized = useCallback(() => {
console.log("Camera initialized!");
setIsCameraInitialized(true);
}, []);
const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => {
console.log(`Media captured! ${JSON.stringify(media)}`);
}, []);
const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => {
console.log(`Media captured! ${JSON.stringify(media)}`);
}, []);
const onVideoChunkReady = useCallback((event) => {
console.log(`Chunk ready in react-native`, event.nativeEvent);
}, []);
const onVideoChunkReady = useCallback((event) => {
console.log(`Chunk ready in react-native`, event.nativeEvent);
}, []);
if (!hasPermission) {
requestPermission();
// Error handling in case they refuse to give permission
}
if (!hasPermission) {
requestPermission();
// Error handling in case they refuse to give permission
}
const device = useCameraDevice("back");
const format = useCameraFormat(device, [
{ videoResolution: { width: 3048, height: 2160 } },
{ fps: 60 },
]); // this sets as a target
const device = useCameraDevice("back");
const format = useCameraFormat(device, [
{ videoResolution: { width: 3048, height: 2160 } },
{ fps: 60 },
]); // this sets as a target
//Orientation detection
const [orientation, setOrientation] = useState<Orientation>("portrait");
//Orientation detection
const [orientation, setOrientation] = useState<Orientation>("portrait");
const toggleOrientation = () => {
setOrientation(
(currentOrientation) =>
currentOrientation === "landscape-left" ? "portrait" : "landscape-left", // Can adjust this and the type to match what we want
);
};
const toggleOrientation = () => {
setOrientation(
(currentOrientation) =>
currentOrientation === "landscape-left" ? "portrait" : "landscape-left", // Can adjust this and the type to match what we want
);
};
// Replace with error handling
if (device === null) {
console.log(device);
return (
<Text>
Camera not available. Does user have permissions: {hasPermission}
</Text>
);
}
return (
hasPermission && (
<View style={styles.container}>
<Camera
ref={camera}
style={StyleSheet.absoluteFill}
device={device}
format={format}
onInitialized={onInitialized}
onError={onError}
onVideoChunkReady={onVideoChunkReady}
video={true}
orientation={orientation} // TODO: #60
isActive={isActive}
/>
<View style={orientation === "portrait" ? styles.goBackPortrait : styles.goBackLandscape}>
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
<RecordingButton
style={[
styles.captureButton,
orientation === "portrait" ? styles.portrait : styles.landscape,
]}
camera={camera}
onMediaCaptured={onMediaCaptured}
enabled={isCameraInitialized}
/>
<View
style={[
styles.button,
orientation === "portrait"
? styles.togglePortrait
: styles.toggleLandscape,
]}
>
<Button
title="Toggle Orientation"
onPress={toggleOrientation}
color="#841584"
accessibilityLabel="Toggle camera orientation"
/>
</View>
</View>
)
);
// Replace with error handling
if (device === null) {
console.log(device);
return (
<Text>
Camera not available. Does user have permissions: {hasPermission}
</Text>
);
}
return (
hasPermission && (
<View style={styles.container}>
<Camera
ref={camera}
style={StyleSheet.absoluteFill}
device={device}
format={format}
onInitialized={onInitialized}
onError={onError}
onVideoChunkReady={onVideoChunkReady}
video={true}
orientation={orientation} // TODO: #60
isActive={isActive}
/>
<View
style={
orientation === "portrait"
? styles.goBackPortrait
: styles.goBackLandscape
}
>
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
<RecordingButton
style={[
styles.captureButton,
orientation === "portrait" ? styles.portrait : styles.landscape,
]}
camera={camera}
onMediaCaptured={onMediaCaptured}
enabled={isCameraInitialized}
/>
<View
style={[
styles.button,
orientation === "portrait"
? styles.togglePortrait
: styles.toggleLandscape,
]}
>
<Button
title="Toggle Orientation"
onPress={toggleOrientation}
color="#841584"
accessibilityLabel="Toggle camera orientation"
/>
</View>
</View>
)
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "black",
},
captureButton: {
position: "absolute",
alignSelf: "center",
},
button: {
position: "absolute",
alignSelf: "center",
},
togglePortrait: {
bottom: 110, // needs refined
},
toggleLandscape: {
transform: [{ rotate: "90deg" }],
bottom: "43%", // Should come from SafeAreaProvider, hardcoded right now, should roughly appear above the button
left: 50, // needs refined
},
portrait: {
bottom: 20, // needs refined
},
landscape: {
bottom: "40%", // Should come from SafeAreaProvider
left: 20, // needs refined
},
goBackPortrait: {
position: 'absolute',
top: 20, // or wherever you want the button to be positioned in portrait
left: 20, // or wherever you want the button to be positioned in portrait
},
goBackLandscape: {
position: 'absolute',
top: 40,
right: 20,
transform: [{ rotate: '90deg' }],
},
container: {
flex: 1,
backgroundColor: "black",
},
captureButton: {
position: "absolute",
alignSelf: "center",
},
button: {
position: "absolute",
alignSelf: "center",
},
togglePortrait: {
bottom: 110, // needs refined
},
toggleLandscape: {
transform: [{ rotate: "90deg" }],
bottom: "43%", // Should come from SafeAreaProvider, hardcoded right now, should roughly appear above the button
left: 50, // needs refined
},
portrait: {
bottom: 20, // needs refined
},
landscape: {
bottom: "40%", // Should come from SafeAreaProvider
left: 20, // needs refined
},
goBackPortrait: {
position: "absolute",
top: 20, // or wherever you want the button to be positioned in portrait
left: 20, // or wherever you want the button to be positioned in portrait
},
goBackLandscape: {
position: "absolute",
top: 40,
right: 20,
transform: [{ rotate: "90deg" }],
},
});

View File

@ -1,31 +1,32 @@
import { Dimensions, Platform } from 'react-native'
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets'
import { Dimensions, Platform } from "react-native";
import StaticSafeAreaInsets from "react-native-static-safe-area-insets";
export const CONTENT_SPACING = 15
export const CONTENT_SPACING = 15;
const SAFE_BOTTOM =
Platform.select({
ios: StaticSafeAreaInsets.safeAreaInsetsBottom,
}) ?? 0
Platform.select({
ios: StaticSafeAreaInsets.safeAreaInsetsBottom,
}) ?? 0;
export const SAFE_AREA_PADDING = {
paddingLeft: StaticSafeAreaInsets.safeAreaInsetsLeft + CONTENT_SPACING,
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + CONTENT_SPACING,
paddingRight: StaticSafeAreaInsets.safeAreaInsetsRight + CONTENT_SPACING,
paddingBottom: SAFE_BOTTOM + CONTENT_SPACING,
}
paddingLeft: StaticSafeAreaInsets.safeAreaInsetsLeft + CONTENT_SPACING,
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + CONTENT_SPACING,
paddingRight: StaticSafeAreaInsets.safeAreaInsetsRight + CONTENT_SPACING,
paddingBottom: SAFE_BOTTOM + CONTENT_SPACING,
};
// The maximum zoom _factor_ you should be able to zoom in
export const MAX_ZOOM_FACTOR = 10
export const MAX_ZOOM_FACTOR = 10;
export const SCREEN_WIDTH = Dimensions.get('window').width
export const SCREEN_WIDTH = Dimensions.get("window").width;
export const SCREEN_HEIGHT = Platform.select<number>({
android: Dimensions.get('screen').height - StaticSafeAreaInsets.safeAreaInsetsBottom,
ios: Dimensions.get('window').height,
}) as number
android:
Dimensions.get("screen").height - StaticSafeAreaInsets.safeAreaInsetsBottom,
ios: Dimensions.get("window").height,
}) as number;
// Capture Button
export const CAPTURE_BUTTON_SIZE = 78
export const CAPTURE_BUTTON_SIZE = 78;
// Control Button like Flash
export const CONTROL_BUTTON_SIZE = 40
export const CONTROL_BUTTON_SIZE = 40;

View File

@ -1,16 +1,16 @@
import { useState, useEffect } from 'react'
import { AppState, AppStateStatus } from 'react-native'
import { useState, useEffect } from "react";
import { AppState, AppStateStatus } from "react-native";
export const useIsForeground = (): boolean => {
const [isForeground, setIsForeground] = useState(true)
const [isForeground, setIsForeground] = useState(true);
useEffect(() => {
const onChange = (state: AppStateStatus): void => {
setIsForeground(state === 'active')
}
const listener = AppState.addEventListener('change', onChange)
return () => listener.remove()
}, [setIsForeground])
useEffect(() => {
const onChange = (state: AppStateStatus): void => {
setIsForeground(state === "active");
};
const listener = AppState.addEventListener("change", onChange);
return () => listener.remove();
}, [setIsForeground]);
return isForeground
}
return isForeground;
};

View File

@ -1,47 +1,47 @@
{
"project_info": {
"project_number": "735905563616",
"project_id": "railbird-infra",
"storage_bucket": "railbird-infra.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:735905563616:android:7eefd99f68d2f7db702185",
"android_client_info": {
"package_name": "android.railbird.app"
}
},
"oauth_client": [
{
"client_id": "735905563616-v12rcdm7pkm1r2t5v09th5bs9j3ah1e4.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyA5o4LpiDFl8Q8AaA2eGjbgdS7tMfupCWg"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "735905563616-v12rcdm7pkm1r2t5v09th5bs9j3ah1e4.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "735905563616-ncd8794ocn2f25qmnaascn88upfgokp0.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "ai.railbird.railbird",
"app_store_id": "6469274937"
}
}
]
}
}
}
],
"configuration_version": "1"
"project_info": {
"project_number": "735905563616",
"project_id": "railbird-infra",
"storage_bucket": "railbird-infra.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:735905563616:android:7eefd99f68d2f7db702185",
"android_client_info": {
"package_name": "android.railbird.app"
}
},
"oauth_client": [
{
"client_id": "735905563616-v12rcdm7pkm1r2t5v09th5bs9j3ah1e4.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyA5o4LpiDFl8Q8AaA2eGjbgdS7tMfupCWg"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "735905563616-v12rcdm7pkm1r2t5v09th5bs9j3ah1e4.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "735905563616-ncd8794ocn2f25qmnaascn88upfgokp0.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "ai.railbird.railbird",
"app_store_id": "6469274937"
}
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -1,6 +1,6 @@
import { registerRootComponent } from 'expo';
import { registerRootComponent } from "expo";
import App from './App';
import App from "./App";
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,

View File

@ -1,64 +1,64 @@
export const graph_data_one_measures = {
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
yValues: [
{
key: 'measure_1',
values: [100, 140, 90, 80, 40]
}
]
xValues: ["x_1", "x_2", "x_3", "x_4", "x_5"],
yValues: [
{
key: "measure_1",
values: [100, 140, 90, 80, 40],
},
],
};
export const graph_data_two_measures = {
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
yValues: [
{
key: 'measure_1',
values: [100, 140, 90, 80, 40]
},
{
key: 'measure_2',
values: [78, 82, 73, 56, 61],
formatLabel: (value: number) => `${value}%`
}
]
xValues: ["x_1", "x_2", "x_3", "x_4", "x_5"],
yValues: [
{
key: "measure_1",
values: [100, 140, 90, 80, 40],
},
{
key: "measure_2",
values: [78, 82, 73, 56, 61],
formatLabel: (value: number) => `${value}%`,
},
],
};
export const graph_data_three_measures = {
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
yValues: [
{
key: 'measure_1',
values: [100, 140, 90, 80, 40]
},
{
key: 'measure_2',
values: [78, 82, 73, 56, 61],
formatLabel: (value: number) => `${value}%`
},
{
key: 'measure_3',
values: [77, 32, 45, 65, 50]
}
]
xValues: ["x_1", "x_2", "x_3", "x_4", "x_5"],
yValues: [
{
key: "measure_1",
values: [100, 140, 90, 80, 40],
},
{
key: "measure_2",
values: [78, 82, 73, 56, 61],
formatLabel: (value: number) => `${value}%`,
},
{
key: "measure_3",
values: [77, 32, 45, 65, 50],
},
],
};
export const line_chart_one_y_data = {
xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
yValues: [
{
key: 'measure_1',
values: [100, 140, 90, 80, 40, 20, 70, 20, 30, 30]
}
]
xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
yValues: [
{
key: "measure_1",
values: [100, 140, 90, 80, 40, 20, 70, 20, 30, 30],
},
],
};
export const line_chart_two_y_data = {
xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
yValues: [
{
key: 'measure_1',
values: [100, 140, 90, 80, 40, 20, 70, 20, 30, 30]
},
{
key: 'measure_2',
values: [50, 67, 123, 140, 156, 147, 126, 180, 123, 87]
}
]
xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
yValues: [
{
key: "measure_1",
values: [100, 140, 90, 80, 40, 20, 70, 20, 30, 30],
},
{
key: "measure_2",
values: [50, 67, 123, 140, 156, 147, 126, 180, 123, 87],
},
],
};

View File

@ -1,26 +1,30 @@
import { useColorScheme } from 'react-native';
import { NavigationContainer, DarkTheme, DefaultTheme } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { useColorScheme } from "react-native";
import {
NavigationContainer,
DarkTheme,
DefaultTheme,
} from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Tabs from './tab-navigator';
import Login from '../screens/login';
import Tabs from "./tab-navigator";
import Login from "../screens/login";
const Stack = createNativeStackNavigator()
const Stack = createNativeStackNavigator();
const ScreensStack = () => (
<Stack.Navigator>
<Stack.Screen
name="Tabs"
component={Tabs}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Login"
component={Login}
options={{ headerShown: false }}
/>
</Stack.Navigator>
)
<Stack.Navigator>
<Stack.Screen
name="Tabs"
component={Tabs}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Login"
component={Login}
options={{ headerShown: false }}
/>
</Stack.Navigator>
);
/**
* Functional component for app navigation. Configures a navigation container with a stack navigator.
* Dynamically selects between dark and light themes based on the device's color scheme.
@ -29,15 +33,14 @@ const ScreensStack = () => (
* @returns {React.ComponentType} A NavigationContainer wrapping a Stack.Navigator for app screens.
*/
export default function AppNavigator(): React.JSX.Element {
// useColorScheme get's the theme from device settings
const scheme = useColorScheme();
// useColorScheme get's the theme from device settings
const scheme = useColorScheme();
return (
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="App" component={ScreensStack} />
</Stack.Navigator>
</NavigationContainer>
)
return (
<NavigationContainer theme={scheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="App" component={ScreensStack} />
</Stack.Navigator>
</NavigationContainer>
);
}

View File

@ -1,35 +1,29 @@
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Image } from 'react-native';
import CameraScreen from '../component/video/camera';
import Session from '../screens/session';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import RecordScreen from '../screens/video-stack/record';
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Image } from "react-native";
import CameraScreen from "../component/video/camera";
import Session from "../screens/session";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import RecordScreen from "../screens/video-stack/record";
// TODO: add ts support for assets folder to use imports
const Icon = require('../assets/favicon.png')
const Icon = require("../assets/favicon.png");
const Tab = createBottomTabNavigator();
const RecordStack = createNativeStackNavigator();
// tabBarIcon configuration should live on separate file and contain all logic/icons/rendering for the Tabs
const tabIcons = {
'Session': <Image source={Icon} style={{ width: 20, height: 20 }} />,
'VideoStack': <Image source={Icon} style={{ width: 20, height: 20 }} />,
Session: <Image source={Icon} style={{ width: 20, height: 20 }} />,
VideoStack: <Image source={Icon} style={{ width: 20, height: 20 }} />,
};
function VideoTabStack() {
return (
<RecordStack.Navigator screenOptions={{ headerShown: false }}>
<RecordStack.Screen
name="Record"
component={RecordScreen}
/>
<RecordStack.Screen
name="Camera"
component={CameraScreen}
/>
</RecordStack.Navigator>
);
return (
<RecordStack.Navigator screenOptions={{ headerShown: false }}>
<RecordStack.Screen name="Record" component={RecordScreen} />
<RecordStack.Screen name="Camera" component={CameraScreen} />
</RecordStack.Navigator>
);
}
/**
@ -41,19 +35,27 @@ function VideoTabStack() {
*/
export default function Tabs(): React.JSX.Element {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
tabBarIcon: () => {
return tabIcons[route.name];
}
})}
>
<Tab.Screen name="Session" component={Session} options={{ tabBarLabel: 'Session' }} />
<Tab.Screen name="VideoStack" component={VideoTabStack} options={{ tabBarLabel: 'Record' }} />
</Tab.Navigator>
);
return (
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarActiveTintColor: "tomato",
tabBarInactiveTintColor: "gray",
tabBarIcon: () => {
return tabIcons[route.name];
},
})}
>
<Tab.Screen
name="Session"
component={Session}
options={{ tabBarLabel: "Session" }}
/>
<Tab.Screen
name="VideoStack"
component={VideoTabStack}
options={{ tabBarLabel: "Record" }}
/>
</Tab.Navigator>
);
}

View File

@ -1,12 +1,12 @@
import React, { useState, useEffect } from "react";
import {
Alert,
Button,
View,
Text,
TextInput,
TouchableWithoutFeedback,
Keyboard,
Alert,
Button,
View,
Text,
TextInput,
TouchableWithoutFeedback,
Keyboard,
} from "react-native";
import auth, { FirebaseAuthTypes } from "@react-native-firebase/auth";
@ -15,105 +15,105 @@ import auth, { FirebaseAuthTypes } from "@react-native-firebase/auth";
// Currently working for Android builds, iOS has open issue #56
export default function Login() {
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [code, setCode] = useState<string>("");
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [code, setCode] = useState<string>("");
const [user, setUser] = useState(null);
const [confirm, setConfirm] =
useState<FirebaseAuthTypes.ConfirmationResult | null>(null);
const [user, setUser] = useState(null);
const [confirm, setConfirm] =
useState<FirebaseAuthTypes.ConfirmationResult | null>(null);
async function onAuthStateChanged(user: any) {
setUser(user);
if (user) {
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const token = await auth().currentUser?.getIdToken();
// To debug/check token & user return, use these logs
// console.log(token) // token log
// console.log(user) // user log
}
}
async function onAuthStateChanged(user: any) {
setUser(user);
if (user) {
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const token = await auth().currentUser?.getIdToken();
// To debug/check token & user return, use these logs
// console.log(token) // token log
// console.log(user) // user log
}
}
async function signInWithPhoneNumber(phoneNumber: string) {
if (!phoneNumber) {
return Alert.alert(
"Please enter a valid phone number with a country code",
);
}
try {
const confirmation = await auth().signInWithPhoneNumber(phoneNumber);
setConfirm(confirmation);
} catch (err) {
// TODO: implement more robust error handling by parsing err message
console.warn(err);
Alert.alert(
"There was an error. Make sure you are using a country code (ex: +1 for US)",
);
}
}
async function signInWithPhoneNumber(phoneNumber: string) {
if (!phoneNumber) {
return Alert.alert(
"Please enter a valid phone number with a country code",
);
}
try {
const confirmation = await auth().signInWithPhoneNumber(phoneNumber);
setConfirm(confirmation);
} catch (err) {
// TODO: implement more robust error handling by parsing err message
console.warn(err);
Alert.alert(
"There was an error. Make sure you are using a country code (ex: +1 for US)",
);
}
}
async function confirmCode() {
try {
await confirm?.confirm(code);
} catch {
Alert.alert("Invalid code, please try again.");
}
}
async function confirmCode() {
try {
await confirm?.confirm(code);
} catch {
Alert.alert("Invalid code, please try again.");
}
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber;
}, []);
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber;
}, []);
return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={{ alignItems: "center" }}>
<TextInput
style={{
width: "50%",
height: 30,
borderWidth: 1,
borderColor: "black",
marginBottom: 20,
}}
placeholder="Phone"
textContentType="telephoneNumber"
keyboardType="phone-pad"
autoCapitalize="none"
value={phoneNumber}
onChangeText={(value) => setPhoneNumber(value)}
/>
{confirm && (
<TextInput
style={{
width: "50%",
height: 30,
borderWidth: 1,
borderColor: "black",
marginBottom: 20,
}}
placeholder="Code"
keyboardType="number-pad"
textContentType="oneTimeCode"
autoCapitalize="none"
value={code}
onChangeText={(value) => setCode(value)}
/>
)}
<Button
title={!confirm ? "Receive code" : "Confirm code"}
onPress={() =>
!confirm ? signInWithPhoneNumber(phoneNumber) : confirmCode()
}
/>
{user && (
<>
<Text style={{ marginTop: 10 }}>
Display name: {user?.displayName}
</Text>
<Text>Phone number: {user?.phoneNumber}</Text>
</>
)}
</View>
</TouchableWithoutFeedback>
);
return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={{ alignItems: "center" }}>
<TextInput
style={{
width: "50%",
height: 30,
borderWidth: 1,
borderColor: "black",
marginBottom: 20,
}}
placeholder="Phone"
textContentType="telephoneNumber"
keyboardType="phone-pad"
autoCapitalize="none"
value={phoneNumber}
onChangeText={(value) => setPhoneNumber(value)}
/>
{confirm && (
<TextInput
style={{
width: "50%",
height: 30,
borderWidth: 1,
borderColor: "black",
marginBottom: 20,
}}
placeholder="Code"
keyboardType="number-pad"
textContentType="oneTimeCode"
autoCapitalize="none"
value={code}
onChangeText={(value) => setCode(value)}
/>
)}
<Button
title={!confirm ? "Receive code" : "Confirm code"}
onPress={() =>
!confirm ? signInWithPhoneNumber(phoneNumber) : confirmCode()
}
/>
{user && (
<>
<Text style={{ marginTop: 10 }}>
Display name: {user?.displayName}
</Text>
<Text>Phone number: {user?.phoneNumber}</Text>
</>
)}
</View>
</TouchableWithoutFeedback>
);
}

View File

@ -1,15 +1,14 @@
import React from 'react'
import React from "react";
import { View, StyleSheet } from "react-native";
import BarGraph from '../component/charts/bar-graph/bar-graph';
import { graph_data_two_measures } from '../mock/charts/mock-data';
import BarGraph from "../component/charts/bar-graph/bar-graph";
import { graph_data_two_measures } from "../mock/charts/mock-data";
// Session Mock - can be used for session summary screen using a query handler component
// BarGraph component using mocked data currently
export default function SessionScreen() {
return (
<View style={StyleSheet.absoluteFill}>
<BarGraph data={graph_data_two_measures} />
</View>
)
return (
<View style={StyleSheet.absoluteFill}>
<BarGraph data={graph_data_two_measures} />
</View>
);
}

View File

@ -1,145 +1,150 @@
import React, { useCallback, useState } from 'react'
import { View, TextInput, TouchableWithoutFeedback, Text, TouchableOpacity, Keyboard } from 'react-native';
import DropDownPicker from 'react-native-dropdown-picker';
import { recordStyles as styles } from './styles';
import React, { useCallback, useState } from "react";
import {
View,
TextInput,
TouchableWithoutFeedback,
Text,
TouchableOpacity,
Keyboard,
} from "react-native";
import DropDownPicker from "react-native-dropdown-picker";
import { recordStyles as styles } from "./styles";
interface CameraScreenParams {
gameType: string;
tableSize: string;
tags: Array<string>;
location: string;
gameType: string;
tableSize: string;
tags: Array<string>;
location: string;
}
// Record Screen
// Precedes Camera.tsx
// Can be made into Modal when ready
export default function RecordScreen({ navigation }): React.JSX.Element {
// Game type dropdown
const [gameTypeOpen, setGameTypeOpen] = useState<boolean>(false);
const [gameType, setGameType] = useState<string | null>(null); // This is a dropdown
const [gameTypes, setGameTypes] = useState([
{ label: "Free Play", value: "freePlay" },
{ label: "Straight Pool", value: "straightPool" },
{ label: "Nine Ball", value: "nineBall" },
]);
const onGameTypeOpen = useCallback(() => {
setTableSizeOpen(false);
setTagsOpen(false);
}, []);
// Game type dropdown
const [gameTypeOpen, setGameTypeOpen] = useState<boolean>(false)
const [gameType, setGameType] = useState<string | null>(null) // This is a dropdown
const [gameTypes, setGameTypes] = useState([
{ label: 'Free Play', value: 'freePlay' },
{ label: 'Straight Pool', value: 'straightPool' },
{ label: 'Nine Ball', value: 'nineBall' }
]);
const onGameTypeOpen = useCallback(() => {
setTableSizeOpen(false);
setTagsOpen(false);
}, []);
// Table size dropdown
const [tableSizeOpen, setTableSizeOpen] = useState<boolean>(false);
const [tableSize, setTableSize] = useState<string>("");
const [tableSizes, setTableSizes] = useState([
{ label: `9'`, value: "nineFoot" },
{ label: `8'`, value: "eightFoot" },
{ label: "7", value: "sevenFoot" },
]);
const onTableSizeOpen = useCallback(() => {
setGameTypeOpen(false);
setTagsOpen(false);
}, []);
// Table size dropdown
const [tableSizeOpen, setTableSizeOpen] = useState<boolean>(false)
const [tableSize, setTableSize] = useState<string>('')
const [tableSizes, setTableSizes] = useState([
{ label: `9'`, value: 'nineFoot' },
{ label: `8'`, value: 'eightFoot' },
{ label: '7', value: 'sevenFoot' }
]);
const onTableSizeOpen = useCallback(() => {
setGameTypeOpen(false);
setTagsOpen(false);
}, []);
// Tags multi-select dropdown
const [tagsOpen, setTagsOpen] = useState<boolean>(false);
const [tags, setTags] = useState<Array<string>>([]);
const [tagsList, setTagsList] = useState([
{ label: `Tag1`, value: "tag1" },
{ label: `Tag2`, value: "tag2" },
{ label: "Tag3", value: "tag3" },
]);
const onTagsOpen = useCallback(() => {
setTableSizeOpen(false);
setGameTypeOpen(false);
}, []);
// Tags multi-select dropdown
const [tagsOpen, setTagsOpen] = useState<boolean>(false)
const [tags, setTags] = useState<Array<string>>([])
const [tagsList, setTagsList] = useState([
{ label: `Tag1`, value: 'tag1' },
{ label: `Tag2`, value: 'tag2' },
{ label: 'Tag3', value: 'tag3' }
]);
const onTagsOpen = useCallback(() => {
setTableSizeOpen(false);
setGameTypeOpen(false);
}, []);
// Location
const [location, setLocation] = useState<string>("");
// Location
const [location, setLocation] = useState<string>('')
const handleSubmit = () => {
// needs to pass info as params or store in a context/state provider
const params: CameraScreenParams = {
gameType: gameType,
tableSize: tableSize,
tags: tags,
location: location,
};
navigation.push("Camera", params);
};
const handleSubmit = () => {
// needs to pass info as params or store in a context/state provider
const params: CameraScreenParams = {
gameType: gameType,
tableSize: tableSize,
tags: tags,
location: location
};
navigation.push('Camera', params)
}
const dropDownStyles = {
style: styles.dropdownStyle,
};
const dropDownStyles = {
style: styles.dropdownStyle,
};
return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={styles.container}>
<View style={styles.dropdownContainer}>
<Text style={styles.dropdownTitle}>Game Type</Text>
<DropDownPicker
zIndex={3000}
zIndexInverse={1000}
open={gameTypeOpen}
value={gameType}
items={gameTypes}
setOpen={setGameTypeOpen}
setValue={setGameType}
setItems={setGameTypes}
onOpen={onGameTypeOpen}
{...dropDownStyles}
/>
<Text style={styles.dropdownTitle}>Table size</Text>
<DropDownPicker
zIndex={2000}
zIndexInverse={2000}
open={tableSizeOpen}
value={tableSize}
items={tableSizes}
setOpen={setTableSizeOpen}
setValue={setTableSize}
setItems={setTableSizes}
onOpen={onTableSizeOpen}
{...dropDownStyles}
/>
<Text style={styles.dropdownTitle}>Tags</Text>
<DropDownPicker
zIndex={1000}
zIndexInverse={3000}
multiple
min={0}
max={5}
open={tagsOpen}
value={tags}
items={tagsList}
setOpen={setTagsOpen}
setValue={setTags}
setItems={setTagsList}
onOpen={onTagsOpen}
{...dropDownStyles}
/>
</View>
<Text style={styles.dropdownTitle}>Location</Text>
<TextInput
style={styles.input}
placeholder="Location"
value={location}
onChangeText={(value) => setLocation(value)}
/>
return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={styles.container}>
<View style={styles.dropdownContainer}>
<Text style={styles.dropdownTitle}>Game Type</Text>
<DropDownPicker
zIndex={3000}
zIndexInverse={1000}
open={gameTypeOpen}
value={gameType}
items={gameTypes}
setOpen={setGameTypeOpen}
setValue={setGameType}
setItems={setGameTypes}
onOpen={onGameTypeOpen}
{...dropDownStyles}
/>
<Text style={styles.dropdownTitle}>Table size</Text>
<DropDownPicker
zIndex={2000}
zIndexInverse={2000}
open={tableSizeOpen}
value={tableSize}
items={tableSizes}
setOpen={setTableSizeOpen}
setValue={setTableSize}
setItems={setTableSizes}
onOpen={onTableSizeOpen}
{...dropDownStyles}
/>
<Text style={styles.dropdownTitle}>Tags</Text>
<DropDownPicker
zIndex={1000}
zIndexInverse={3000}
multiple
min={0}
max={5}
open={tagsOpen}
value={tags}
items={tagsList}
setOpen={setTagsOpen}
setValue={setTags}
setItems={setTagsList}
onOpen={onTagsOpen}
{...dropDownStyles}
/>
</View>
<Text style={styles.dropdownTitle}>Location</Text>
<TextInput
style={styles.input}
placeholder='Location'
value={location}
onChangeText={(value) => setLocation(value)}
/>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.buttonStyle} onPress={() => navigation.goBack()}>
<Text style={styles.buttonText}>Back</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.buttonStyle} onPress={handleSubmit}>
<Text style={styles.buttonText}>Next</Text>
</TouchableOpacity>
</View>
</View>
</TouchableWithoutFeedback>
)
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.buttonStyle}
onPress={() => navigation.goBack()}
>
<Text style={styles.buttonText}>Back</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.buttonStyle} onPress={handleSubmit}>
<Text style={styles.buttonText}>Next</Text>
</TouchableOpacity>
</View>
</View>
</TouchableWithoutFeedback>
);
}

View File

@ -1,57 +1,57 @@
import { StyleSheet } from 'react-native'
import { StyleSheet } from "react-native";
export const recordStyles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
dropdownContainer: {
width: '100%',
marginBottom: 20,
zIndex: 50
},
dropdownTitle: {
fontSize: 16,
fontWeight: '500',
marginBottom: 5,
alignSelf: 'flex-start'
},
input: {
width: '100%',
marginBottom: 20,
borderWidth: 1,
borderColor: 'grey',
borderRadius: 5,
padding: 10,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
},
buttonStyle: {
backgroundColor: 'lightblue',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 20,
margin: 10,
},
buttonText: {
color: 'white',
textAlign: 'center',
},
dropdownStyle: {
backgroundColor: '#ffffff',
borderColor: '#D1D1D1',
borderWidth: 1,
borderRadius: 4,
},
dropdownContainerStyle: {
marginBottom: 10,
borderColor: '#D1D1D1',
borderWidth: 1,
borderRadius: 4,
},
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
dropdownContainer: {
width: "100%",
marginBottom: 20,
zIndex: 50,
},
dropdownTitle: {
fontSize: 16,
fontWeight: "500",
marginBottom: 5,
alignSelf: "flex-start",
},
input: {
width: "100%",
marginBottom: 20,
borderWidth: 1,
borderColor: "grey",
borderRadius: 5,
padding: 10,
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
},
buttonStyle: {
backgroundColor: "lightblue",
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 20,
margin: 10,
},
buttonText: {
color: "white",
textAlign: "center",
},
dropdownStyle: {
backgroundColor: "#ffffff",
borderColor: "#D1D1D1",
borderWidth: 1,
borderRadius: 4,
},
dropdownContainerStyle: {
marginBottom: 10,
borderColor: "#D1D1D1",
borderWidth: 1,
borderRadius: 4,
},
});

View File

@ -2,25 +2,25 @@
// COLORS:
// can be made more granular to specify utility (ex: fontColors vs backgroundColors)
export const colors = {
bgBlack: '#121212',
lightGrey: '#BFC2C8',
themeBrown: '#D9AA84',
panelWhite: '#F2FBFE',
tournamentBlue: '#50a6c2',
blueCloth: '#539dc2',
buttonBlue: '#1987ff',
textWhite: '#ffffff'
bgBlack: "#121212",
lightGrey: "#BFC2C8",
themeBrown: "#D9AA84",
panelWhite: "#F2FBFE",
tournamentBlue: "#50a6c2",
blueCloth: "#539dc2",
buttonBlue: "#1987ff",
textWhite: "#ffffff",
};
export const shadows = {
standard: {
shadowColor: '#000000',
shadowOffset: {
width: 0,
height: 3
},
shadowOpacity: 0.1,
shadowRadius: 4.65,
elevation: 3
},
standard: {
shadowColor: "#000000",
shadowOffset: {
width: 0,
height: 3,
},
shadowOpacity: 0.1,
shadowRadius: 4.65,
elevation: 3,
},
};

View File

@ -1,20 +1,20 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import BarGraph from '../../component/charts/bar-graph/bar-graph';
import { graph_data_two_measures } from '../../mock/charts/mock-data';
import React from "react";
import { render } from "@testing-library/react-native";
import BarGraph from "../../component/charts/bar-graph/bar-graph";
import { graph_data_two_measures } from "../../mock/charts/mock-data";
describe('BarGraph Component Tests', () => {
it('renders correctly with data', () => {
const { getByTestId } = render(<BarGraph data={graph_data_two_measures} testID='1'/>);
expect(getByTestId(`bar-graph-1`)).toBeTruthy();
});
it('does not render without data', () => {
// Have to ts-ignore to test null data conditions
// @ts-ignore
const { queryByTestId } = render(<BarGraph testID='2'/>);
expect(queryByTestId(`bar-graph-2`)).toBeNull();
});
describe("BarGraph Component Tests", () => {
it("renders correctly with data", () => {
const { getByTestId } = render(
<BarGraph data={graph_data_two_measures} testID="1" />,
);
expect(getByTestId(`bar-graph-1`)).toBeTruthy();
});
it("does not render without data", () => {
// Have to ts-ignore to test null data conditions
// @ts-ignore
const { queryByTestId } = render(<BarGraph testID="2" />);
expect(queryByTestId(`bar-graph-2`)).toBeNull();
});
});

View File

@ -1,38 +1,42 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import React from "react";
import { render } from "@testing-library/react-native";
import "@testing-library/jest-native/extend-expect";
import ChartLabel from '../../component/charts/chart-label/chart-label';
import ChartLabel from "../../component/charts/chart-label/chart-label";
describe('ChartLabel Component Tests', () => {
const mockData = {
yLabels: [
{ displayName: 'Shots Taken', axis: 'LEFT' as 'LEFT', color: '#598EBB' },
{ displayName:'Make Percentage', axis: 'RIGHT' as 'RIGHT', color: '#F2D4BC'}
],
title: 'Shots Taken / Make Percentage by Cut Angle'
};
describe("ChartLabel Component Tests", () => {
const mockData = {
yLabels: [
{ displayName: "Shots Taken", axis: "LEFT" as "LEFT", color: "#598EBB" },
{
displayName: "Make Percentage",
axis: "RIGHT" as "RIGHT",
color: "#F2D4BC",
},
],
title: "Shots Taken / Make Percentage by Cut Angle",
};
it('should render the correct labels given yLabels', () => {
const { getByText } = render(
<ChartLabel title={mockData.title} yLabels={mockData.yLabels} />
);
it("should render the correct labels given yLabels", () => {
const { getByText } = render(
<ChartLabel title={mockData.title} yLabels={mockData.yLabels} />,
);
mockData.yLabels.forEach(label => {
expect(getByText(label.displayName)).toBeTruthy();
});
});
mockData.yLabels.forEach((label) => {
expect(getByText(label.displayName)).toBeTruthy();
});
});
it('should render the correct number of label boxes', () => {
const { getAllByText } = render(
<ChartLabel title={mockData.title} yLabels={mockData.yLabels} />
);
it("should render the correct number of label boxes", () => {
const { getAllByText } = render(
<ChartLabel title={mockData.title} yLabels={mockData.yLabels} />,
);
// Assuming displayName is unique and used only for labels
const labelElements = mockData.yLabels.map(label =>
getAllByText(label.displayName)
).flat();
// Assuming displayName is unique and used only for labels
const labelElements = mockData.yLabels
.map((label) => getAllByText(label.displayName))
.flat();
expect(labelElements.length).toBe(mockData.yLabels.length);
});
expect(labelElements.length).toBe(mockData.yLabels.length);
});
});

View File

@ -1,31 +1,31 @@
import React from 'react';
import { Text } from 'react-native'
import { render } from '@testing-library/react-native';
import React from "react";
import { Text } from "react-native";
import { render } from "@testing-library/react-native";
import "@testing-library/jest-native/extend-expect";
import ChartView from '../../component/charts/chart-view';
import ChartView from "../../component/charts/chart-view";
describe('ChartView Component Tests', () => {
it('applies the passed style prop correctly', () => {
const testStyle = { backgroundColor: 'blue', padding: 10 };
const { getByTestId } = render(
<ChartView style={testStyle} testID="chart-view">
<Text>Test Child</Text>
</ChartView>
);
describe("ChartView Component Tests", () => {
it("applies the passed style prop correctly", () => {
const testStyle = { backgroundColor: "blue", padding: 10 };
const { getByTestId } = render(
<ChartView style={testStyle} testID="chart-view">
<Text>Test Child</Text>
</ChartView>,
);
const chartView = getByTestId('chart-view');
expect(chartView.props.style).toEqual(expect.arrayContaining([testStyle]));
});
const chartView = getByTestId("chart-view");
expect(chartView.props.style).toEqual(expect.arrayContaining([testStyle]));
});
it('renders children correctly', () => {
const { getByText } = render(
<ChartView>
<Text testID="child-text">Child Component</Text>
</ChartView>
);
it("renders children correctly", () => {
const { getByText } = render(
<ChartView>
<Text testID="child-text">Child Component</Text>
</ChartView>,
);
const childText = getByText('Child Component');
expect(childText).toBeTruthy();
});
const childText = getByText("Child Component");
expect(childText).toBeTruthy();
});
});

View File

@ -1,89 +1,97 @@
import React from 'react';
import { render } from '@testing-library/react-native';
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/charts/custom-bars';
import { calculateBarOrigin, drawBarPath } from '../../component/charts/custom-bar-utils';
import * as scale from "d3-scale";
import { CustomBars } from "../../component/charts/custom-bars";
import {
calculateBarOrigin,
drawBarPath,
} from "../../component/charts/custom-bar-utils";
const mockYScaleFunction = scale.scaleLinear();
const mockXScaleFunction = scale.scaleBand();
const mockBandwidth = 100;
const mockCombinedData = [
{
data: [{ value: 10 }, { value: 20 }],
svg: { fill: 'red' },
},
{
data: [{ value: 10 }, { value: 20 }],
svg: { fill: "red" },
},
];
const mockRawData = [
{ label: 'Full ball', shotsTaken: 10, makePercentage: 20 },
{ label: "Full ball", shotsTaken: 10, makePercentage: 20 },
];
const mockBarColors = ['red', 'blue'];
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(
<CustomBars
x={mockXScaleFunction}
y={mockYScaleFunction}
bandwidth={mockBandwidth}
barData={mockCombinedData}
xValues={mockRawData}
barColors={mockBarColors}
gap={mockGap}
roundedRadius={mockRoundedRadius}
/>
);
describe("CustomBars Component Tests", () => {
it("should render correct number of Svg and Bar components", () => {
const { getAllByTestId } = render(
<CustomBars
x={mockXScaleFunction}
y={mockYScaleFunction}
bandwidth={mockBandwidth}
barData={mockCombinedData}
xValues={mockRawData}
barColors={mockBarColors}
gap={mockGap}
roundedRadius={mockRoundedRadius}
/>,
);
const svgs = getAllByTestId(/svg-/);
const bars = getAllByTestId(/bar-/);
expect(svgs.length).toBe(mockRawData.length);
expect(bars.length).toBe(mockCombinedData.length * mockRawData.length);
});
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;
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)
});
});
});
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;
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'
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);
});
});
expect(path).toBe(expectedPath);
});
});
});

View File

@ -1,21 +1,20 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import LineGraph from '../../component/charts/line-graph/line-graph';
import { line_chart_two_y_data } from '../../mock/charts/mock-data';
import React from "react";
import { render } from "@testing-library/react-native";
import LineGraph from "../../component/charts/line-graph/line-graph";
import { line_chart_two_y_data } from "../../mock/charts/mock-data";
describe('LineGraph Component Tests', () => {
it('renders correctly with data', () => {
const { getByTestId } = render(<LineGraph data={line_chart_two_y_data} testID='1'/>);
expect(getByTestId(`line-graph-1`)).toBeTruthy();
});
it('does not render without data', () => {
// Have to ts-ignore to test null data conditions
// @ts-ignore
const { queryByTestId } = render(<LineGraph testID='2'/>);
expect(queryByTestId(`line-graph-2`)).toBeNull();
});
describe("LineGraph Component Tests", () => {
it("renders correctly with data", () => {
const { getByTestId } = render(
<LineGraph data={line_chart_two_y_data} testID="1" />,
);
expect(getByTestId(`line-graph-1`)).toBeTruthy();
});
it("does not render without data", () => {
// Have to ts-ignore to test null data conditions
// @ts-ignore
const { queryByTestId } = render(<LineGraph testID="2" />);
expect(queryByTestId(`line-graph-2`)).toBeNull();
});
});

View File

@ -1,58 +1,66 @@
import { renderHook } from '@testing-library/react-native';
import { useGraphData } from '../../component/charts/use-graph-data';
import { GraphData, GraphProps } from '../../component/charts/graph-types';
import { renderHook } from "@testing-library/react-native";
import { useGraphData } from "../../component/charts/use-graph-data";
import { GraphData, GraphProps } from "../../component/charts/graph-types";
describe('useGraphData', () => {
it('should return correctly processed data from convertToGraphData', () => {
// mock values
const mockGraphData: GraphData = {
xValues: ['full hit', '3/4 ball ball', '1/2 ball'],
yValues: [
{ key: 'left', values: [10, 20, 30] },
{ key: 'right', values: [40, 50, 60] }
]
};
describe("useGraphData", () => {
it("should return correctly processed data from convertToGraphData", () => {
// mock values
const mockGraphData: GraphData = {
xValues: ["full hit", "3/4 ball ball", "1/2 ball"],
yValues: [
{ key: "left", values: [10, 20, 30] },
{ key: "right", values: [40, 50, 60] },
],
};
const mockProps: Partial<GraphProps> = {
yAxisProps: {
maxLeftYAxisValue: 30,
maxRightYAxisValue: 60,
selectedLeftYAxisLabel: 'left',
selectedRightYAxisLabel: 'right',
formatRightYAxisLabel: (value) => `${value}%`,
formatLeftYAxisLabel: (value) => `${value}%`
}
};
const mockProps: Partial<GraphProps> = {
yAxisProps: {
maxLeftYAxisValue: 30,
maxRightYAxisValue: 60,
selectedLeftYAxisLabel: "left",
selectedRightYAxisLabel: "right",
formatRightYAxisLabel: (value) => `${value}%`,
formatLeftYAxisLabel: (value) => `${value}%`,
},
};
const { result } = renderHook(() => useGraphData(mockGraphData, mockProps));
// values expected
const expectedYData = [
{
data: [{ value: 33.33 }, { value: 66.67 }, { value: 100 }],
svg: { fill: "transparent" },
},
{
data: [{ value: 66.67 }, { value: 83.33 }, { value: 100 }],
svg: { fill: "transparent" },
},
];
const expectedLeftLabels = { key: "left", values: [10, 20, 30] };
const expectedRightLabels = { key: "right", values: [40, 50, 60] };
const { result } = renderHook(() => useGraphData(mockGraphData, mockProps));
// values expected
const expectedYData = [
{
data: [{ value: 33.33 }, { value: 66.67 }, { value: 100 }],
svg: { fill: 'transparent' },
},
{
data: [{ value: 66.67 }, { value: 83.33 }, { value: 100 }],
svg: { fill: 'transparent' },
},
];
const expectedLeftLabels = { key: 'left', values: [10, 20, 30] };
const expectedRightLabels = { key: 'right', values: [40, 50, 60] };
expect(result.current).toBeDefined();
expect(result.current.xValues).toEqual(['full hit', '3/4 ball ball', '1/2 ball']);
result.current.yData.forEach((yDataItem, index) => {
yDataItem.data.forEach((dataItem, dataIndex) => {
expect(dataItem.value).toBeCloseTo(expectedYData[index].data[dataIndex].value, 2);
});
});
expect(result.current.yAxisLeftLabels).toEqual(expectedLeftLabels);
expect(result.current.yAxisRightLabels).toEqual(expectedRightLabels);
expect(result.current.defaultProps.yAxisProps.selectedLeftYAxisLabel).toEqual('left');
expect(result.current.defaultProps.yAxisProps.selectedRightYAxisLabel).toEqual('right');
expect(result.current.defaultProps).toBeDefined();
});
expect(result.current).toBeDefined();
expect(result.current.xValues).toEqual([
"full hit",
"3/4 ball ball",
"1/2 ball",
]);
result.current.yData.forEach((yDataItem, index) => {
yDataItem.data.forEach((dataItem, dataIndex) => {
expect(dataItem.value).toBeCloseTo(
expectedYData[index].data[dataIndex].value,
2,
);
});
});
expect(result.current.yAxisLeftLabels).toEqual(expectedLeftLabels);
expect(result.current.yAxisRightLabels).toEqual(expectedRightLabels);
expect(
result.current.defaultProps.yAxisProps.selectedLeftYAxisLabel,
).toEqual("left");
expect(
result.current.defaultProps.yAxisProps.selectedRightYAxisLabel,
).toEqual("right");
expect(result.current.defaultProps).toBeDefined();
});
});

View File

@ -1,5 +1,5 @@
{
"include": ["."],
"exclude": ["node_modules", "./react-native-vision-camera/package"],
"extends": ["expo/tsconfig.base"]
"include": ["."],
"exclude": ["node_modules", "./react-native-vision-camera/package"],
"extends": ["expo/tsconfig.base"]
}

View File

@ -9513,6 +9513,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-plugin-organize-imports@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e"
integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==
pretty-bytes@5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"