Merge pull request 'Line Graph -- render component' (#39) from loewy/line-graph-render-component into master

Reviewed-on: railbird/rn-playground#39
Reviewed-by: Kat Huang <kkathuang@gmail.com>
This commit is contained in:
loewy 2024-01-18 20:10:09 -07:00
commit ec0904ffb1
9 changed files with 185 additions and 13 deletions

View File

@ -31,7 +31,7 @@ interface Props {
export const BarGraph: React.FC<Props> = ({ data, useCommonScale = false, testID, ...props }) => { export const BarGraph: React.FC<Props> = ({ data, useCommonScale = false, testID, ...props }) => {
if (!data) { if (!data) {
return null return null
} } // TODO:#38
const { const {
xValues, xValues,
yData, yData,
@ -52,9 +52,9 @@ export const BarGraph: React.FC<Props> = ({ data, useCommonScale = false, testID
formatLeftYAxisLabel 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 // 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 // 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 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<Props> = ({ data, useCommonScale = false, testID
style={graphStyles.flex} style={graphStyles.flex}
data={yData} data={yData}
gridMin={min} 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 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} yAccessor={({ item }) => (item as unknown as { value: number }).value}
contentInset={contentInset} contentInset={contentInset}

View File

@ -19,7 +19,8 @@ export const graphStyles = StyleSheet.create({
yAxisRightPadding: { paddingLeft: 3 }, yAxisRightPadding: { paddingLeft: 3 },
yAxisFontStyle: { fontSize: 10, fill: 'grey' }, yAxisFontStyle: { fontSize: 10, fill: 'grey' },
xAxisFontStyle: { fontSize: 12, fill: 'black' }, xAxisFontStyle: { fontSize: 12, fill: 'black' },
xAxisMarginTop: { marginTop: -15 } xAxisMarginTop: { marginTop: -15 },
horizontalInset: { right: 10, left: 10 },
}); });
export const chartLabel = StyleSheet.create({ export const chartLabel = StyleSheet.create({

View File

@ -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 = { export const chartDefaults = {
height: 300, height: 300,
spacingInner: 0.3, spacingInner: 0.3,
@ -6,5 +7,7 @@ export const chartDefaults = {
contentInset: { top: 30, bottom: 30 }, contentInset: { top: 30, bottom: 30 },
numberOfTicks: 6, numberOfTicks: 6,
min: 0, min: 0,
barColors: ['#598EBB', '#F2D4BC', '#DB7878'] barColors: ['#598EBB', '#F2D4BC', '#DB7878'],
includeColors: true,
lineStrokeWidth: 2
}; };

View File

@ -34,6 +34,7 @@ export interface YAxisProps {
} }
export interface GraphProps { export interface GraphProps {
data: GraphData; data: GraphData;
includeColors?: boolean;
height?: number; height?: number;
spacingInner?: number; spacingInner?: number;
spacingOuter?: number; spacingOuter?: number;
@ -41,6 +42,37 @@ export interface GraphProps {
min?: number; min?: number;
numberOfTicks?: number; numberOfTicks?: number;
barColors?: Array<string>; barColors?: Array<string>;
lineStrokeWidth?: number;
useCommonScale?: boolean; useCommonScale?: boolean;
yAxisProps?: YAxisProps; 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<string>} [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<string>;
lineStrokeWidth?: number;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
testID?: string;
} }

View File

@ -8,6 +8,8 @@ export const convertToGraphData = (
selectedRightYAxisLabel?: string; selectedRightYAxisLabel?: string;
maxRightYAxisValue: number; maxRightYAxisValue: number;
maxLeftYAxisValue: number; maxLeftYAxisValue: number;
includeColors: boolean;
barColors: Array<string>
} }
) => { ) => {
const xValues = graphData.xValues; const xValues = graphData.xValues;
@ -20,17 +22,18 @@ export const convertToGraphData = (
// scale data points according to max value // scale data points according to max value
const yData = graphData.yValues.map((yAxis, index) => { const yData = graphData.yValues.map((yAxis, index) => {
const maxValue = const maxValue = index === rightAxisIndex ? options.maxRightYAxisValue : options.maxLeftYAxisValue;
index === rightAxisIndex ? options.maxRightYAxisValue : options.maxLeftYAxisValue;
// scale values as a percentage of the max value // 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 })); const mappedData = scaledValues.map((scaledValue) => ({ value: scaledValue }));
return { return {
data: mappedData, 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; const yAxisLeftLabels = leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null;

View File

@ -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<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>
);
};
export default LineGraph;

View File

@ -43,8 +43,8 @@ export const useGraphData = (graphData: GraphData, props: Partial<GraphProps>):
}; };
const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo( const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo(
() => convertToGraphData(graphData, defaultProps.yAxisProps), () => convertToGraphData(graphData, { ...defaultProps.yAxisProps, includeColors: defaultProps.includeColors, barColors: defaultProps.barColors}),
[graphData, defaultProps.yAxisProps] [graphData, defaultProps.yAxisProps, defaultProps.includeColors, defaultProps.barColors]
); );
return { return {

View File

@ -38,4 +38,27 @@ export const graph_data_three_measures = {
values: [77, 32, 45, 65, 50] 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]
}
]
}; };

View File

@ -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(<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();
});
});