Merge pull request 'ChartContainer -- axis render parent component for graphs' (#42) from loewy/chart-container-wrapper into master

Reviewed-on: railbird/railbird-mobile#42
Reviewed-by: Kat Huang <kkathuang@gmail.com>
This commit is contained in:
Kat Huang 2024-02-13 16:16:50 -07:00
commit dc5a90cf1a
8 changed files with 194 additions and 115 deletions

View File

@ -26,7 +26,7 @@ interface Props {
testID?: string; testID?: string;
} }
// TODO: separate PR will update useGraphData to take into account useCommonScale // 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 // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
export const BarGraph: React.FC<Props> = ({ export const BarGraph: React.FC<Props> = ({
data, data,

View File

@ -0,0 +1,99 @@
import React from "react";
import { View } from "react-native";
import { XAxis, YAxis } from "react-native-svg-charts";
import { graphStyles } from "../chart-styles";
import ChartView from "../chart-view";
import { ChartContainerProps } from "../graph-types";
import { useGraphData } from "../use-graph-data";
// 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
const ChartContainer: React.FC<ChartContainerProps> = ({
ChartComponent,
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);
// TODO: 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
const chartComponentProps = {
yData,
xValues,
lineStrokeWidth,
min,
contentInset,
numberOfTicks,
};
return (
<ChartView>
<View
style={[graphStyles.rowContainer, { height: height }]}
testID={`chart-container-${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}>
<ChartComponent {...chartComponentProps} />
<XAxis
data={xValues.map((_, index: number) => index)} // TODO: update when useGraphHook returns explicit display values
formatLabel={(_, index) => xValues[index]}
style={[graphStyles.xAxisMarginTop]}
svg={graphStyles.xAxisFontStyle}
numberOfTicks={numberOfTicks} // need a dynamic way of setting number of ticks
contentInset={graphStyles.horizontalInset}
// scale={scale.scaleBand} // Need to know the type of ChartComponent
/>
</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}
/>
</View>
</ChartView>
);
};
export default ChartContainer;

View File

@ -63,7 +63,6 @@ export interface GraphProps {
* @property {string} [testID] - Optional. Identifier for testing purposes. * @property {string} [testID] - Optional. Identifier for testing purposes.
*/ */
export interface CommonProps { export interface CommonProps {
data: GraphData;
height?: number; height?: number;
spacingInner?: number; spacingInner?: number;
spacingOuter?: number; spacingOuter?: number;
@ -75,4 +74,16 @@ export interface CommonProps {
useCommonScale?: boolean; useCommonScale?: boolean;
yAxisProps?: YAxisProps; yAxisProps?: YAxisProps;
testID?: string; testID?: string;
yData?: { data: { value: number }[]; svg: { fill: string } }[];
xValues?: XValue[];
}
/**
* Common properties for graph render components.
*
* @interface ChartContainerProps
* @property {React.ComponentType<CommonProps>} ChartComponent - The graph component to render
*/
export interface ChartContainerProps extends CommonProps {
ChartComponent: React.ComponentType<CommonProps>;
data: GraphData;
} }

View File

@ -1,102 +1,31 @@
import * as shape from "d3-shape"; import * as shape from "d3-shape";
import React from "react"; import React from "react";
import { View } from "react-native"; import { LineChart } from "react-native-svg-charts";
import { LineChart, XAxis, YAxis } from "react-native-svg-charts";
import { graphStyles } from "../chart-styles"; import { graphStyles } from "../chart-styles";
import ChartView from "../chart-view";
import { CustomGrid } from "../custom-grid"; import { CustomGrid } from "../custom-grid";
import { CommonProps } from "../graph-types"; import { CommonProps } from "../graph-types";
import { useGraphData } from "../use-graph-data";
// TODO: separate PR will update useGraphData to take into account useCommonScale // 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 // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const LineGraph: React.FC<CommonProps> = ({ const LineGraph: React.FC<CommonProps> = ({ ...chartComponentProps }) => {
data, const { yData, xValues, lineStrokeWidth, min, contentInset, numberOfTicks } =
useCommonScale = false, chartComponentProps;
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 ( return (
<ChartView> <>
<View <LineChart
style={[graphStyles.rowContainer, { height: height }]} style={graphStyles.flex}
testID={`line-graph-${testID}`} 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}
> >
<YAxis <CustomGrid />
data={yAxisLeftLabels.values} </LineChart>
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>
); );
}; };

View File

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

View File

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

View File

@ -1,20 +0,0 @@
import { render } from "@testing-library/react-native";
import React from "react";
import LineGraph from "../../src/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();
});
});

View File

@ -62,3 +62,38 @@ export const line_chart_two_y_data = {
}, },
], ],
}; };
export const mockYData = [
{
data: [
{ value: 71.42857142857143 },
{ value: 100 },
{ value: 64.28571428571429 },
{ value: 57.14285714285714 },
{ value: 28.57142857142857 },
{ value: 14.285714285714285 },
{ value: 50 },
{ value: 14.285714285714285 },
{ value: 21.428571428571427 },
{ value: 21.428571428571427 },
],
svg: { fill: "transparent", stroke: "#598EBB" },
},
{
data: [
{ value: 27.77777777777778 },
{ value: 37.22222222222222 },
{ value: 68.33333333333333 },
{ value: 77.77777777777779 },
{ value: 86.66666666666667 },
{ value: 81.66666666666667 },
{ value: 70 },
{ value: 100 },
{ value: 68.33333333333333 },
{ value: 48.333333333333336 },
],
svg: { fill: "transparent", stroke: "#F2D4BC" },
},
];
export const mockXValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];