diff --git a/src/component/charts/container/chart-container.tsx b/src/component/charts/container/chart-container.tsx index 94aa5f4..e1f5174 100644 --- a/src/component/charts/container/chart-container.tsx +++ b/src/component/charts/container/chart-container.tsx @@ -4,8 +4,10 @@ import { View } from "react-native"; import { XAxis, YAxis } from "react-native-svg-charts"; import { graphStyles } from "../chart-styles"; import ChartView from "../chart-view"; +import { chartDefaults } from "../graph-config"; import { ChartContainerProps } from "../graph-types"; -import { useGraphData } from "../use-graph-data"; +import { computeYAxisConfig, useGraphData } from "../use-graph-data"; +computeYAxisConfig; // TODO: #43 #31 separate PR will update useGraphData to take into account useCommonScale // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars @@ -19,62 +21,49 @@ const ChartContainer: React.FC = ({ if (!data || typeof data !== "object") { return null; } // TODO:#38 + const yAxisProps = computeYAxisConfig(data, props.yAxisProps || {}); + const { xValues, yData, yAxisLeftLabels, yAxisRightLabels, - defaultProps: { - height, - contentInset, - min, - numberOfTicks, - lineStrokeWidth, - spacingInner, - spacingOuter, - barColors, - 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, { + } = useGraphData(data, yAxisProps, { includeColors: ChartComponent.chartType === "bar" ? false : true, ...props, }); // TODO: #31 This could be done by the hook since it already handles spreading props. - // Depending on if we want a context provider for Charts, that could be another way to handle it + // Depiending on if we want a context pr}}ovider for Charts, that could be another way to handle it const chartComponentProps = { yData, xValues, - lineStrokeWidth, - min, - contentInset, - numberOfTicks, - spacingInner, - spacingOuter, - barColors, + lineStrokeWidth: chartDefaults.lineStrokeWidth, + min: chartDefaults.min, + contentInset: chartDefaults.contentInset, + numberOfTicks: chartDefaults.numberOfTicks, + spacingInner: chartDefaults.spacingInner, + spacingOuter: chartDefaults.spacingOuter, + barColors: chartDefaults.barColors, }; + return ( @@ -87,23 +76,23 @@ const ChartContainer: React.FC = ({ scale: scale.scaleBand, })} {...(ChartComponent.chartType === "line" && { - numberOfTicks: numberOfTicks, + numberOfTicks: chartDefaults.numberOfTicks, contentInset: graphStyles.horizontalInset, })} /> diff --git a/src/component/charts/graph-types.ts b/src/component/charts/graph-types.ts index ce7ec65..80b7571 100644 --- a/src/component/charts/graph-types.ts +++ b/src/component/charts/graph-types.ts @@ -15,7 +15,7 @@ export interface YAxisData { export type XValue = string | number; -export interface GraphData { +export interface XYValues { xValues: Array; yValues: Array; } @@ -32,8 +32,15 @@ export interface YAxisProps { // eslint-disable-next-line no-unused-vars formatLeftYAxisLabel?: (value: string) => string; } + +export interface ScaledAndStyledYDataItem { + data: { value: number }[]; + svg: { fill: string; stroke: string }; +} +export type ScaledAndStyledYData = ScaledAndStyledYDataItem[]; + export interface GraphProps { - data: GraphData; + data: XYValues; includeColors?: boolean; height?: number; spacingInner?: number; @@ -90,7 +97,7 @@ type ChartComponentWithStatic = React.ComponentType & { */ export interface ChartContainerProps extends CommonProps { ChartComponent: ChartComponentWithStatic; - data: GraphData; + data: XYValues; } /** diff --git a/src/component/charts/graph-utils.tsx b/src/component/charts/graph-utils.tsx index 9a84d57..411ea99 100644 --- a/src/component/charts/graph-utils.tsx +++ b/src/component/charts/graph-utils.tsx @@ -1,52 +1,115 @@ -import { GraphData } from "./graph-types"; +import { + ScaledAndStyledYData, + XValue, + XYValues, + YAxisData, + YAxisProps, +} from "./graph-types"; -export const convertToGraphData = ( - graphData: GraphData, - options: { - selectedLeftYAxisLabel?: string; - selectedRightYAxisLabel?: string; - maxRightYAxisValue: number; - maxLeftYAxisValue: number; - includeColors: boolean; - barColors: Array; - }, +interface PrepareGraphDataReturn { + yData: ScaledAndStyledYData; + yAxisLeftLabels: YAxisData; + yAxisRightLabels: YAxisData; + xValues: XValue[]; +} + +export const prepareGraphData = ( + xyData: XYValues, + yAxisProps: YAxisProps, + includeColors: boolean, + barColors: string[], +): PrepareGraphDataReturn => { + const { leftAxisIndex, rightAxisIndex } = findAxisIndices( + xyData.yValues, + yAxisProps.selectedLeftYAxisLabel, + yAxisProps.selectedRightYAxisLabel, + ); + + const yData = createYData( + xyData.yValues, + rightAxisIndex, + yAxisProps.maxRightYAxisValue, + yAxisProps.maxLeftYAxisValue, + includeColors, + barColors, + ); + + // Ensure yAxisLeftLabels and yAxisRightLabels are correctly defined + const yAxisLeftLabels = + leftAxisIndex >= 0 + ? xyData.yValues[leftAxisIndex] + : { key: "", values: [] }; + const yAxisRightLabels = + rightAxisIndex >= 0 + ? xyData.yValues[rightAxisIndex] + : { key: "", values: [] }; + + return { + yData, + yAxisLeftLabels, + yAxisRightLabels, + xValues: xyData.xValues, + }; +}; + +const scaleValues = ( + yAxis, + index, + rightAxisIndex, + maxRightYAxisValue, + maxLeftYAxisValue, ) => { - 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 maxValue = + index === rightAxisIndex ? maxRightYAxisValue : maxLeftYAxisValue; + return yAxis.values.map((value) => (value / maxValue) * 100); +}; - // scale data points according to max value - const yData = graphData.yValues.map((yAxis, index) => { - const maxValue = - index === rightAxisIndex - ? options.maxRightYAxisValue - : options.maxLeftYAxisValue; +const getStrokeColor = (index, includeColors, barColors) => + includeColors ? barColors[index % barColors.length] : "transparent"; - // 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 mappedData = scaledValues.map((scaledValue) => ({ - value: scaledValue, - })); +/** + * Finds the indices of the datasets in `yValues` that match the specified left and right Y-axis labels. + * + * Iterates through the `yValues` , searching for objects whose `key` matches + * `selectedLeftYAxisLabel` and `selectedRightYAxisLabel`. It's used to identify which datasets + * should be plotted against the left and right Y-axes in a graph. If a match is not found for a label, + * the corresponding axis index is set to `-1`. + */ +const findAxisIndices = ( + yValues: Array<{ key: string; values: Array }>, + selectedLeftYAxisLabel?: string, + selectedRightYAxisLabel?: string, +): { leftAxisIndex: number; rightAxisIndex: number } => ({ + leftAxisIndex: yValues.findIndex((y) => y.key === selectedLeftYAxisLabel), + rightAxisIndex: yValues.findIndex((y) => y.key === selectedRightYAxisLabel), +}); +/** + * Transforms Y-axis data by scaling values and applying stroke colors for visualization. + * + * This function takes an array of Y-axis data objects, each with a key and an array of numeric values. + * It scales these values relative to specified maximum values for the left and right Y-axes and + * applies coloring based on the includeColors flag and the provided barColors array. + */ +const createYData = ( + yValues: YAxisData[], + rightAxisIndex: number, + maxRightYAxisValue: number, + maxLeftYAxisValue: number, + includeColors: boolean, + barColors: string[], +): ScaledAndStyledYData => + yValues.map((yAxis, index) => { + const scaledValues = scaleValues( + yAxis, + index, + rightAxisIndex, + maxRightYAxisValue, + maxLeftYAxisValue, + ); + const strokeColor = getStrokeColor(index, includeColors, barColors); return { - data: mappedData, - svg: { fill: "transparent", stroke: strokeColor }, // Apply the stroke color here + data: scaledValues.map((value) => ({ value })), + svg: { fill: "transparent", stroke: strokeColor }, }; }); - const yAxisLeftLabels = - leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null; - const yAxisRightLabels = - rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined; - - return { yData, yAxisLeftLabels, yAxisRightLabels, xValues }; -}; diff --git a/src/component/charts/use-graph-data.ts b/src/component/charts/use-graph-data.ts index 1be47db..cdb5e25 100644 --- a/src/component/charts/use-graph-data.ts +++ b/src/component/charts/use-graph-data.ts @@ -1,70 +1,89 @@ import { useMemo } from "react"; import { chartDefaults } from "./graph-config"; -import { GraphData, GraphProps, XValue, YAxisData } from "./graph-types"; -import { convertToGraphData } from "./graph-utils"; +import { + GraphProps, + ScaledAndStyledYData, + XValue, + XYValues, + YAxisData, + YAxisProps, +} from "./graph-types"; +import { prepareGraphData } from "./graph-utils"; -interface useGraphDataInterface { +export interface UseGraphDataReturn { xValues: Array; - yData: { - data: { - value: number; - }[]; - svg: { - fill: string; - }; - }[]; + yData: ScaledAndStyledYData; yAxisLeftLabels: YAxisData; yAxisRightLabels: YAxisData; - defaultProps: Partial; } -// 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, + xyData: XYValues, + yAxisProps: YAxisProps, props: Partial, -): 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, - ], - ); - +): UseGraphDataReturn => { + const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo(() => { + return prepareGraphData( + xyData, + yAxisProps, + chartDefaults.includeColors, + chartDefaults.barColors, + ); + }, [xyData]); return { xValues, yData, yAxisLeftLabels, yAxisRightLabels, - defaultProps, }; }; + +function computeAxisMaxValues(xyData: XYValues): { + maxLeft: number; + maxRight: number | undefined; +} { + const maxLeft = Math.max(...(xyData.yValues[0]?.values ?? [0])); + const maxRight = + xyData.yValues.length > 1 + ? Math.max(...(xyData.yValues[1]?.values ?? [])) + : undefined; + return { maxLeft, maxRight }; +} + +function selectAxisLabels(xyData: XYValues): { + leftLabel: string; + rightLabel: string | undefined; +} { + const leftLabel = xyData.yValues[0]?.key; + const rightLabel = + xyData.yValues.length > 1 ? xyData.yValues[1]?.key : undefined; + return { leftLabel, rightLabel }; +} + +function applyDefaultYAxisProps( + defaultProps: Partial, +): Partial { + return { + formatRightYAxisLabel: defaultProps.formatRightYAxisLabel, + formatLeftYAxisLabel: defaultProps.formatLeftYAxisLabel, + }; +} + +export function computeYAxisConfig( + xyData: XYValues, + yAxisPropsDefault: Partial, +): YAxisProps { + const { maxLeft, maxRight } = computeAxisMaxValues(xyData); + const { leftLabel, rightLabel } = selectAxisLabels(xyData); + const defaultYAxisProps = applyDefaultYAxisProps(yAxisPropsDefault); + + return { + maxLeftYAxisValue: maxLeft, + maxRightYAxisValue: maxRight, + selectedLeftYAxisLabel: leftLabel, + selectedRightYAxisLabel: rightLabel, + ...defaultYAxisProps, + ...yAxisPropsDefault, + }; +} diff --git a/src/screens/session.tsx b/src/screens/session.tsx index ef1d982..2166798 100644 --- a/src/screens/session.tsx +++ b/src/screens/session.tsx @@ -1,15 +1,21 @@ import React from "react"; import { StyleSheet, View } from "react-native"; -import { line_chart_two_y_data } from "../../test/mock/charts/mock-data"; +import { + graph_data_two_measures, + line_chart_two_y_data, +} from "../../test/mock/charts/mock-data"; +import BarGraph from "../component/charts/bar-graph/bar-graph"; import ChartContainer from "../component/charts/container/chart-container"; import LineGraph from "../component/charts/line-graph/line-graph"; -// 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 ( + ); } diff --git a/test/component/use-graph-data.test.tsx b/test/component/use-graph-data.test.tsx index 30b4628..b2defbd 100644 --- a/test/component/use-graph-data.test.tsx +++ b/test/component/use-graph-data.test.tsx @@ -1,11 +1,14 @@ import { renderHook } from "@testing-library/react-native"; -import { GraphData, GraphProps } from "../../src/component/charts/graph-types"; -import { useGraphData } from "../../src/component/charts/use-graph-data"; +import { GraphProps, XYValues } from "../../src/component/charts/graph-types"; +import { + computeYAxisConfig, + useGraphData, +} from "../../src/component/charts/use-graph-data"; describe("useGraphData", () => { it("should return correctly processed data from convertToGraphData", () => { // mock values - const mockGraphData: GraphData = { + const mockGraphData: XYValues = { xValues: ["full hit", "3/4 ball ball", "1/2 ball"], yValues: [ { key: "left", values: [10, 20, 30] }, @@ -23,8 +26,14 @@ describe("useGraphData", () => { formatLeftYAxisLabel: (value) => `${value}%`, }, }; + const yAxisProps = computeYAxisConfig( + mockGraphData, + mockProps.yAxisProps || {}, + ); - const { result } = renderHook(() => useGraphData(mockGraphData, mockProps)); + const { result } = renderHook(() => + useGraphData(mockGraphData, yAxisProps, mockProps), + ); // values expected const expectedYData = [ { @@ -55,12 +64,5 @@ describe("useGraphData", () => { }); 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(); }); });