From b80b05fbd8262e817802378f56432e487ecbd119 Mon Sep 17 00:00:00 2001 From: Loewy Date: Thu, 18 Jan 2024 16:21:01 -0800 Subject: [PATCH] add line graph, associated changes to utils/config/styles & test --- component/charts/bar-graph/bar-graph.tsx | 7 +- component/charts/chart-styles.ts | 3 +- component/charts/graph-config.ts | 7 +- component/charts/graph-types.ts | 32 ++++++++ component/charts/graph-utils.tsx | 11 ++- component/charts/line-graph/line-graph.tsx | 90 ++++++++++++++++++++++ component/charts/use-graph-data.ts | 4 +- mock/charts/mock-data.ts | 23 ++++++ test/component/line-graph.test.tsx | 21 +++++ 9 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 component/charts/line-graph/line-graph.tsx create mode 100644 test/component/line-graph.test.tsx diff --git a/component/charts/bar-graph/bar-graph.tsx b/component/charts/bar-graph/bar-graph.tsx index 6dff88d..ff3bb51 100644 --- a/component/charts/bar-graph/bar-graph.tsx +++ b/component/charts/bar-graph/bar-graph.tsx @@ -31,7 +31,7 @@ interface Props { export const BarGraph: React.FC = ({ data, useCommonScale = false, testID, ...props }) => { if (!data) { return null - } + } // TODO:#38 const { xValues, yData, @@ -52,9 +52,9 @@ export const BarGraph: React.FC = ({ data, useCommonScale = false, testID formatLeftYAxisLabel } } - // Proper error/loading handling from useQueryHandler can work with this rule + // Proper error/loading handling from useQueryHandler can work with this rule #38 // eslint-disable-next-line react-hooks/rules-of-hooks - } = useGraphData(data, props); + } = 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] }] @@ -80,7 +80,6 @@ export const BarGraph: React.FC = ({ data, useCommonScale = false, testID style={graphStyles.flex} data={yData} gridMin={min} - svg={{ stroke: 'transparent' }} // might want to do the transparent from here - if so may be best built as coming from defaultConfig 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} diff --git a/component/charts/chart-styles.ts b/component/charts/chart-styles.ts index 726d0c1..a924840 100644 --- a/component/charts/chart-styles.ts +++ b/component/charts/chart-styles.ts @@ -19,7 +19,8 @@ export const graphStyles = StyleSheet.create({ yAxisRightPadding: { paddingLeft: 3 }, yAxisFontStyle: { fontSize: 10, fill: 'grey' }, xAxisFontStyle: { fontSize: 12, fill: 'black' }, - xAxisMarginTop: { marginTop: -15 } + xAxisMarginTop: { marginTop: -15 }, + horizontalInset: { right: 10, left: 10 }, }); export const chartLabel = StyleSheet.create({ diff --git a/component/charts/graph-config.ts b/component/charts/graph-config.ts index 072f1bc..d906f2f 100644 --- a/component/charts/graph-config.ts +++ b/component/charts/graph-config.ts @@ -1,4 +1,5 @@ -// if values in chartDefaults should not be accessible as props & are a style, move to graphStyles +// TODO: Style values should be moved to styles +// non-style config can live here as chartDefaults export const chartDefaults = { height: 300, spacingInner: 0.3, @@ -6,5 +7,7 @@ export const chartDefaults = { contentInset: { top: 30, bottom: 30 }, numberOfTicks: 6, min: 0, - barColors: ['#598EBB', '#F2D4BC', '#DB7878'] + barColors: ['#598EBB', '#F2D4BC', '#DB7878'], + includeColors: true, + lineStrokeWidth: 2 }; \ No newline at end of file diff --git a/component/charts/graph-types.ts b/component/charts/graph-types.ts index 58e3209..b7b7a5c 100644 --- a/component/charts/graph-types.ts +++ b/component/charts/graph-types.ts @@ -34,6 +34,7 @@ export interface YAxisProps { } export interface GraphProps { data: GraphData; + includeColors?: boolean; height?: number; spacingInner?: number; spacingOuter?: number; @@ -41,6 +42,37 @@ export interface GraphProps { min?: number; numberOfTicks?: number; barColors?: Array; + lineStrokeWidth?: number; useCommonScale?: boolean; yAxisProps?: YAxisProps; +} +/** + * Common properties for graph render components. + * + * @interface CommonProps + * @property {GraphData} data - The primary data source for the graph. + * @property {number} [height] - Optional. The height of the graph. + * @property {number} [spacingInner] - Optional. The inner spacing between elements in the graph. + * @property {number} [spacingOuter] - Optional. The outer spacing of the elements in the graph. + * @property {{ top: number; bottom: number }} [contentInset] - Optional. Insets for the content of the graph. + * @property {number} [min] - Optional. The minimum value represented on the graph. + * @property {number} [numberOfTicks] - Optional. The number of tick marks along the axis. + * @property {Array} [barColors] - Optional. Colors used for bars in the graph. + * @property {boolean} [useCommonScale] - Optional. Flag to use a common scale across multiple components. + * @property {YAxisProps} [yAxisProps] - Optional. Properties for the Y-axis of the graph. + * @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; + lineStrokeWidth?: number; + useCommonScale?: boolean; + yAxisProps?: YAxisProps; + testID?: string; } \ No newline at end of file diff --git a/component/charts/graph-utils.tsx b/component/charts/graph-utils.tsx index b114f03..631e614 100644 --- a/component/charts/graph-utils.tsx +++ b/component/charts/graph-utils.tsx @@ -8,6 +8,8 @@ export const convertToGraphData = ( selectedRightYAxisLabel?: string; maxRightYAxisValue: number; maxLeftYAxisValue: number; + includeColors: boolean; + barColors: Array } ) => { const xValues = graphData.xValues; @@ -20,17 +22,18 @@ export const convertToGraphData = ( // scale data points according to max value const yData = graphData.yValues.map((yAxis, index) => { - const maxValue = - index === rightAxisIndex ? options.maxRightYAxisValue : options.maxLeftYAxisValue; + 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 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 })); return { data: mappedData, - svg: { fill: 'transparent' } // fill handled in CustomBars + svg: { fill: 'transparent', stroke: strokeColor } // Apply the stroke color here }; }); const yAxisLeftLabels = leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null; diff --git a/component/charts/line-graph/line-graph.tsx b/component/charts/line-graph/line-graph.tsx new file mode 100644 index 0000000..43bacd6 --- /dev/null +++ b/component/charts/line-graph/line-graph.tsx @@ -0,0 +1,90 @@ +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'; + +// 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 = ({ 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 ( + + + + + (item as unknown as { value: number }).value} + xAccessor={({ index }) => xValues[index] as number} + gridMin={min} + contentInset={contentInset} + numberOfTicks={numberOfTicks} + > + + + index)} // TODO: update when useGraphHook returns explicit display values + style={[graphStyles.xAxisMarginTop]} + svg={graphStyles.xAxisFontStyle} + numberOfTicks={numberOfTicks} + contentInset={graphStyles.horizontalInset} + /> + + + + + ); +}; + +export default LineGraph; diff --git a/component/charts/use-graph-data.ts b/component/charts/use-graph-data.ts index 892e93d..30269b5 100644 --- a/component/charts/use-graph-data.ts +++ b/component/charts/use-graph-data.ts @@ -43,8 +43,8 @@ export const useGraphData = (graphData: GraphData, props: Partial): }; const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo( - () => convertToGraphData(graphData, defaultProps.yAxisProps), - [graphData, defaultProps.yAxisProps] + () => convertToGraphData(graphData, { ...defaultProps.yAxisProps, includeColors: defaultProps.includeColors, barColors: defaultProps.barColors}), + [graphData, defaultProps.yAxisProps, defaultProps.includeColors, defaultProps.barColors] ); return { diff --git a/mock/charts/mock-data.ts b/mock/charts/mock-data.ts index 4a4d864..6c70623 100644 --- a/mock/charts/mock-data.ts +++ b/mock/charts/mock-data.ts @@ -38,4 +38,27 @@ export const graph_data_three_measures = { 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] + } + ] +}; +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] + } + ] }; \ No newline at end of file diff --git a/test/component/line-graph.test.tsx b/test/component/line-graph.test.tsx new file mode 100644 index 0000000..6cb238d --- /dev/null +++ b/test/component/line-graph.test.tsx @@ -0,0 +1,21 @@ +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(); + 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(); + expect(queryByTestId(`line-graph-2`)).toBeNull(); + }); + +});