From 12c3a6ef6fa01cfaf4bff07264af3009064584b4 Mon Sep 17 00:00:00 2001 From: Loewy Date: Fri, 12 Jan 2024 13:04:45 -0800 Subject: [PATCH 1/3] add func logic for bar graph --- App.tsx | 1 - component/charts/graph-config.ts | 10 +++++ component/charts/graph-types.ts | 41 +++++++++++++++++- component/charts/graph-utils.tsx | 40 ++++++++++++++++++ component/charts/use-graph-data.ts | 57 +++++++++++++++++++++++++ test/component/use-graph-data.test.tsx | 58 ++++++++++++++++++++++++++ 6 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 component/charts/graph-config.ts create mode 100644 component/charts/graph-utils.tsx create mode 100644 component/charts/use-graph-data.ts create mode 100644 test/component/use-graph-data.test.tsx diff --git a/App.tsx b/App.tsx index 6a47ac6..8fa0f1b 100644 --- a/App.tsx +++ b/App.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Text } from "react-native"; import ClientProvider from "./graphql/client"; -import ShotsContainer from "./component/shot"; const App: React.FC = () => { return ( diff --git a/component/charts/graph-config.ts b/component/charts/graph-config.ts new file mode 100644 index 0000000..072f1bc --- /dev/null +++ b/component/charts/graph-config.ts @@ -0,0 +1,10 @@ +// if values in chartDefaults should not be accessible as props & are a style, move to graphStyles +export const chartDefaults = { + height: 300, + spacingInner: 0.3, + spacingOuter: 0.2, + contentInset: { top: 30, bottom: 30 }, + numberOfTicks: 6, + min: 0, + barColors: ['#598EBB', '#F2D4BC', '#DB7878'] +}; \ No newline at end of file diff --git a/component/charts/graph-types.ts b/component/charts/graph-types.ts index 345312a..e7b6178 100644 --- a/component/charts/graph-types.ts +++ b/component/charts/graph-types.ts @@ -1,3 +1,42 @@ import { ScaleLinear } from 'd3-scale' -export type ScaleFunction = ScaleLinear; \ No newline at end of file +export type ScaleFunction = ScaleLinear; + +export interface YAxisData { + key: string; // string value for ChartLabel component + values: 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 interface GraphData { + xValues: string[]; + yValues: YAxisData[]; +} + +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; +} +export interface Props { + data: GraphData; + height?: number; + spacingInner?: number; + spacingOuter?: number; + contentInset?: { top: number; bottom: number }; + min?: number; + numberOfTicks?: number; + barColors?: Array; + yAxisProps?: YAxisProps; +} \ No newline at end of file diff --git a/component/charts/graph-utils.tsx b/component/charts/graph-utils.tsx new file mode 100644 index 0000000..fafc21d --- /dev/null +++ b/component/charts/graph-utils.tsx @@ -0,0 +1,40 @@ +import { GraphData } from "./graph-types"; + + +export const convertToBarData = ( + graphData: GraphData, + options: { + selectedLeftYAxisLabel?: string; + selectedRightYAxisLabel?: string; + maxRightYAxisValue: number; + maxLeftYAxisValue: number; + } +) => { + 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 barData = 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); + + const mappedData = scaledValues.map((scaledValue) => ({ value: scaledValue })); + + return { + data: mappedData, + svg: { fill: 'transparent' } // fill handled in CustomBars + }; + }); + const yAxisLeftLabels = leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null; + const yAxisRightLabels = rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined; + + return { barData, yAxisLeftLabels, yAxisRightLabels, xValues }; +}; diff --git a/component/charts/use-graph-data.ts b/component/charts/use-graph-data.ts new file mode 100644 index 0000000..f9586ce --- /dev/null +++ b/component/charts/use-graph-data.ts @@ -0,0 +1,57 @@ +import { useMemo } from 'react'; + +import { convertToBarData } from './graph-utils'; +import { chartDefaults } from './graph-config'; +import { GraphData, Props, YAxisData } from './graph-types'; + + + +interface useGraphDataInterface { + xValues: Array; + barData: { + data: { + value: number; + }[]; + svg: { + fill: string; + }; + }[]; + yAxisLeftLabels: YAxisData; + yAxisRightLabels: YAxisData; + defaultProps: Partial; +} + +// this version assumes string values for X, this isn't necessarily the case +// convertToBarData 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): 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 { barData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo( + () => convertToBarData(graphData, defaultProps.yAxisProps), + [graphData, defaultProps.yAxisProps] + ); + + return { + xValues, + barData, + yAxisLeftLabels, + yAxisRightLabels, + defaultProps + }; +}; diff --git a/test/component/use-graph-data.test.tsx b/test/component/use-graph-data.test.tsx new file mode 100644 index 0000000..6455db0 --- /dev/null +++ b/test/component/use-graph-data.test.tsx @@ -0,0 +1,58 @@ +import { renderHook } from '@testing-library/react-native'; +import { useGraphData } from '../../component/charts/use-graph-data'; +import { GraphData, Props } from '../../component/charts/graph-types'; + +describe('useGraphData', () => { + it('should return correctly processed data from convertToBarData', () => { + // 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 = { + yAxisProps: { + maxLeftYAxisValue: 30, + maxRightYAxisValue: 60, + selectedLeftYAxisLabel: 'left', + selectedRightYAxisLabel: 'right', + formatRightYAxisLabel: (value) => `${value}%`, + formatLeftYAxisLabel: (value) => `${value}%` + } + }; + + + const { result } = renderHook(() => useGraphData(mockGraphData, mockProps)); + // values expected + const expectedBarData = [ + { + 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.barData.forEach((barDataItem, index) => { + barDataItem.data.forEach((dataItem, dataIndex) => { + expect(dataItem.value).toBeCloseTo(expectedBarData[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(); + }); +}); + From 7faa25e10341a649aea8a673c127bbc76fad3524 Mon Sep 17 00:00:00 2001 From: Loewy Date: Fri, 12 Jan 2024 16:21:40 -0800 Subject: [PATCH 2/3] update types/naming to reflect intended usage for useGraphData hook --- component/charts/graph-types.ts | 12 +++++++----- component/charts/graph-utils.tsx | 6 +++--- component/charts/use-graph-data.ts | 20 ++++++++++---------- test/component/use-graph-data.test.tsx | 14 +++++++------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/component/charts/graph-types.ts b/component/charts/graph-types.ts index e7b6178..35aaa49 100644 --- a/component/charts/graph-types.ts +++ b/component/charts/graph-types.ts @@ -3,8 +3,8 @@ import { ScaleLinear } from 'd3-scale' export type ScaleFunction = ScaleLinear; export interface YAxisData { - key: string; // string value for ChartLabel component - values: number[]; + key: string; // string value for ChartLabel and useGraphData + values: Array; // 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 @@ -12,9 +12,11 @@ export interface YAxisData { formatLabel?: (value: number) => string; } +export type XValue = string | number + export interface GraphData { - xValues: string[]; - yValues: YAxisData[]; + xValues: Array; + yValues: Array; } interface YAxisProps { @@ -29,7 +31,7 @@ interface YAxisProps { // eslint-disable-next-line no-unused-vars formatLeftYAxisLabel?: (value: string) => string; } -export interface Props { +export interface GraphProps { data: GraphData; height?: number; spacingInner?: number; diff --git a/component/charts/graph-utils.tsx b/component/charts/graph-utils.tsx index fafc21d..b114f03 100644 --- a/component/charts/graph-utils.tsx +++ b/component/charts/graph-utils.tsx @@ -1,7 +1,7 @@ import { GraphData } from "./graph-types"; -export const convertToBarData = ( +export const convertToGraphData = ( graphData: GraphData, options: { selectedLeftYAxisLabel?: string; @@ -19,7 +19,7 @@ export const convertToBarData = ( ); // scale data points according to max value - const barData = graphData.yValues.map((yAxis, index) => { + const yData = graphData.yValues.map((yAxis, index) => { const maxValue = index === rightAxisIndex ? options.maxRightYAxisValue : options.maxLeftYAxisValue; @@ -36,5 +36,5 @@ export const convertToBarData = ( const yAxisLeftLabels = leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null; const yAxisRightLabels = rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined; - return { barData, yAxisLeftLabels, yAxisRightLabels, xValues }; + return { yData, yAxisLeftLabels, yAxisRightLabels, xValues }; }; diff --git a/component/charts/use-graph-data.ts b/component/charts/use-graph-data.ts index f9586ce..892e93d 100644 --- a/component/charts/use-graph-data.ts +++ b/component/charts/use-graph-data.ts @@ -1,14 +1,14 @@ import { useMemo } from 'react'; -import { convertToBarData } from './graph-utils'; +import { convertToGraphData } from './graph-utils'; import { chartDefaults } from './graph-config'; -import { GraphData, Props, YAxisData } from './graph-types'; +import { GraphData, GraphProps, XValue, YAxisData } from './graph-types'; interface useGraphDataInterface { - xValues: Array; - barData: { + xValues: Array; + yData: { data: { value: number; }[]; @@ -18,13 +18,13 @@ interface useGraphDataInterface { }[]; yAxisLeftLabels: YAxisData; yAxisRightLabels: YAxisData; - defaultProps: Partial; + defaultProps: Partial; } // this version assumes string values for X, this isn't necessarily the case -// convertToBarData is specifically tailored to bar/group bar graphs +// 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): useGraphDataInterface => { +export const useGraphData = (graphData: GraphData, props: Partial): useGraphDataInterface => { const { yAxisProps = {}, ...otherProps } = props; const defaultProps = { ...chartDefaults, @@ -42,14 +42,14 @@ export const useGraphData = (graphData: GraphData, props: Partial): useGr } }; - const { barData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo( - () => convertToBarData(graphData, defaultProps.yAxisProps), + const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo( + () => convertToGraphData(graphData, defaultProps.yAxisProps), [graphData, defaultProps.yAxisProps] ); return { xValues, - barData, + yData, yAxisLeftLabels, yAxisRightLabels, defaultProps diff --git a/test/component/use-graph-data.test.tsx b/test/component/use-graph-data.test.tsx index 6455db0..81f03a6 100644 --- a/test/component/use-graph-data.test.tsx +++ b/test/component/use-graph-data.test.tsx @@ -1,9 +1,9 @@ import { renderHook } from '@testing-library/react-native'; import { useGraphData } from '../../component/charts/use-graph-data'; -import { GraphData, Props } from '../../component/charts/graph-types'; +import { GraphData, GraphProps } from '../../component/charts/graph-types'; describe('useGraphData', () => { - it('should return correctly processed data from convertToBarData', () => { + it('should return correctly processed data from convertToGraphData', () => { // mock values const mockGraphData: GraphData = { xValues: ['full hit', '3/4 ball ball', '1/2 ball'], @@ -13,7 +13,7 @@ describe('useGraphData', () => { ] }; - const mockProps: Partial = { + const mockProps: Partial = { yAxisProps: { maxLeftYAxisValue: 30, maxRightYAxisValue: 60, @@ -27,7 +27,7 @@ describe('useGraphData', () => { const { result } = renderHook(() => useGraphData(mockGraphData, mockProps)); // values expected - const expectedBarData = [ + const expectedYData = [ { data: [{ value: 33.33 }, { value: 66.67 }, { value: 100 }], svg: { fill: 'transparent' }, @@ -43,9 +43,9 @@ describe('useGraphData', () => { expect(result.current).toBeDefined(); expect(result.current.xValues).toEqual(['full hit', '3/4 ball ball', '1/2 ball']); - result.current.barData.forEach((barDataItem, index) => { - barDataItem.data.forEach((dataItem, dataIndex) => { - expect(dataItem.value).toBeCloseTo(expectedBarData[index].data[dataIndex].value, 2); + 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); From c391a07d7097b8c2963a09592b31ecce38b71a4d Mon Sep 17 00:00:00 2001 From: Loewy Date: Mon, 15 Jan 2024 11:51:43 -0800 Subject: [PATCH 3/3] fix lint issue with master rebase --- App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/App.tsx b/App.tsx index 8fa0f1b..6a47ac6 100644 --- a/App.tsx +++ b/App.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Text } from "react-native"; import ClientProvider from "./graphql/client"; +import ShotsContainer from "./component/shot"; const App: React.FC = () => { return (