Merge pull request 'Bar Graph Render Component' (#28) from loewy/bar-graph-render into master

Reviewed-on: billnerds/rn-playground#28
Reviewed-by: Kat Huang <kkathuang@gmail.com>
This commit is contained in:
loewy 2024-01-16 15:16:59 -07:00
commit 7e7a571f5a
14 changed files with 986 additions and 56 deletions

View File

@ -0,0 +1,113 @@
import React from 'react';
import * as scale from 'd3-scale';
import { View } from 'react-native';
import { BarChart, XAxis, YAxis } from 'react-native-svg-charts';
import { useGraphData } from '../use-graph-data';
import { GraphData, YAxisProps } from '../graph-types';
import { CustomBars } from '../custom-bars';
import { CustomGrid } from '../custom-grid';
import { graphStyles } from '../chart-styles';
import ChartView from '../chart-view';
interface Props {
data: GraphData;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
testID?: string;
}
// TODO: separate PR will update useGraphData to take into account useCommonScale
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
export const BarGraph: React.FC<Props> = ({ data, useCommonScale = false, testID, ...props }) => {
if (!data) {
return null
}
const {
xValues,
yData,
yAxisRightLabels,
yAxisLeftLabels,
defaultProps: {
height,
spacingInner,
spacingOuter,
contentInset,
min,
numberOfTicks,
barColors,
yAxisProps: {
maxLeftYAxisValue,
maxRightYAxisValue,
formatRightYAxisLabel,
formatLeftYAxisLabel
}
}
// Proper error/loading handling from useQueryHandler can work with this rule
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useGraphData(data, props);
return (
<ChartView>
<View style={[graphStyles.rowContainer, { height: height }]} testID={`bar-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}>
<BarChart
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}
spacingInner={spacingInner}
spacingOuter={spacingOuter}
>
<CustomGrid />
<CustomBars barData={yData} xValues={xValues} barColors={barColors} />
</BarChart>
<XAxis
data={xValues.map((_, index) => index)}
formatLabel={(_, index) => xValues[index]}
style={graphStyles.xAxisMarginTop}
svg={graphStyles.xAxisFontStyle}
scale={scale.scaleBand}
/>
</View>
<YAxis
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
contentInset={contentInset}
svg={graphStyles.yAxisFontStyle}
style={graphStyles.yAxisRightPadding}
min={min}
max={maxRightYAxisValue}
numberOfTicks={numberOfTicks}
formatLabel={formatRightYAxisLabel}
/>
</View>
</ChartView>
);
};
export default BarGraph;

View File

@ -2,11 +2,11 @@ import React from 'react';
import { Path } from 'react-native-svg'; import { Path } from 'react-native-svg';
import { calculateBarOrigin, drawBarPath } from './custom-bar-utils'; import { calculateBarOrigin, drawBarPath } from './custom-bar-utils';
import { ScaleFunction } from './graph-types'; import { ScaleBandType, ScaleLinearType, } from './graph-types';
interface BarProps { interface BarProps {
scaleX: ScaleFunction; scaleX: ScaleBandType;
scaleY: ScaleFunction; scaleY: ScaleLinearType;
data: { value: number }; data: { value: number };
barNumber: number; barNumber: number;
index: number; index: number;

View File

@ -13,4 +13,11 @@ export const graphStyles = StyleSheet.create({
paddingHorizontal: 15, paddingHorizontal: 15,
...shadows.standard ...shadows.standard
}, },
rowContainer: { flexDirection: 'row', padding: 10 },
flex: { flex: 1 },
yAxisLeftPadding: { paddingRight: 3 },
yAxisRightPadding: { paddingLeft: 3 },
yAxisFontStyle: { fontSize: 10, fill: 'grey' },
xAxisFontStyle: { fontSize: 12, fill: 'black' },
xAxisMarginTop: { marginTop: -15 }
}); });

View File

@ -1,10 +1,10 @@
import { path as d3 } from 'd3-path'; import { path as d3 } from 'd3-path';
import { ScaleFunction } from './graph-types'; import { ScaleLinearType, ScaleBandType } from './graph-types';
type BarCalculationProps = { type BarCalculationProps = {
scaleX: ScaleFunction; scaleX: ScaleBandType;
scaleY: ScaleFunction; scaleY: ScaleLinearType;
data: { value: number }; data: { value: number };
barNumber: number; barNumber: number;
index: number; index: number;

View File

@ -2,7 +2,7 @@
import { Svg } from 'react-native-svg'; import { Svg } from 'react-native-svg';
import { Bar } from './bar'; import { Bar } from './bar';
import { ScaleFunction } from './graph-types'; import { ScaleBandType, ScaleLinearType } from './graph-types';
import { calculateBarWidth } from './custom-bar-utils'; import { calculateBarWidth } from './custom-bar-utils';
export interface CombinedData { export interface CombinedData {
@ -11,38 +11,38 @@ export interface CombinedData {
} }
interface CustomBarsProps { interface CustomBarsProps {
x: ScaleFunction; x: ScaleBandType;
y: ScaleFunction; y: ScaleLinearType;
bandwidth: number; bandwidth: number;
barColors: string[]; barColors: string[];
rawData: unknown[]; // TODO: update this value when this data type is defined xValues: unknown[]; // TODO: update this value when this data type is defined
combinedData: CombinedData[]; barData: CombinedData[];
gap: number; gap: number;
roundedRadius: number; roundedRadius: number;
} }
export const CustomBars: React.FC<CustomBarsProps> = ({ export const CustomBars: React.FC<Partial<CustomBarsProps>> = ({
x: scaleX, x: scaleX,
y: scaleY, y: scaleY,
bandwidth, bandwidth,
combinedData, barData,
rawData, xValues,
barColors, barColors,
gap = 2, gap = 2,
roundedRadius = 4 roundedRadius = 4
}) => { }) => {
const barWidth = calculateBarWidth(bandwidth, combinedData.length, gap); const barWidth = calculateBarWidth(bandwidth, barData.length, gap);
return rawData.map((_, index) => ( return xValues.map((_, index) => (
<Svg key={`group-${index}`} testID={`svg-${index}`}> <Svg key={`group-${index}`} testID={`svg-${index}`}>
{combinedData.map((item, i) => ( {barData.map((item, i) => (
<Bar <Bar
key={`bar-${i}-${index}`} key={`bar-${i}-${index}`}
scaleX={scaleX} scaleX={scaleX}
scaleY={scaleY} scaleY={scaleY}
data={item.data[index]} data={item.data[index]}
barNumber={i} // index of bar barNumber={i}
index={index} // index of group index={index}
fill={barColors[i]} fill={barColors[i]}
barWidth={barWidth} barWidth={barWidth}
gap={gap} gap={gap}

View File

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react';
import { G, Line } from 'react-native-svg'; import { G, Line } from 'react-native-svg';
import { colors } from '../../styles'; import { colors } from '../../styles';
import { ScaleFunction } from './graph-types'; import { ScaleLinearType } from './graph-types';
interface CustomGridProps { interface CustomGridProps {
y: ScaleFunction; y: ScaleLinearType;
ticks: Array<number>; ticks: Array<number>;
} }
export const CustomGrid: React.FC<CustomGridProps> = ({ y, ticks }) => { export const CustomGrid: React.FC<Partial<CustomGridProps>> = ({ y, ticks }) => {
const [firstTick, ...remainingTicks] = ticks; const [firstTick, ...remainingTicks] = ticks;
const dashArray = [1, 3]; const dashArray = [1, 3];
const strokeSolidWidth = 0.2; const strokeSolidWidth = 0.2;

View File

@ -1,6 +1,7 @@
import { ScaleLinear } from 'd3-scale' import { ScaleBand, ScaleLinear } from 'd3-scale'
export type ScaleFunction = ScaleLinear<number, number>; export type ScaleLinearType = ScaleLinear<number, number>;
export type ScaleBandType = ScaleBand<number | string>;
export interface YAxisData { export interface YAxisData {
key: string; // string value for ChartLabel and useGraphData key: string; // string value for ChartLabel and useGraphData
@ -19,7 +20,7 @@ export interface GraphData {
yValues: Array<YAxisData>; yValues: Array<YAxisData>;
} }
interface YAxisProps { export interface YAxisProps {
maxLeftYAxisValue?: number; maxLeftYAxisValue?: number;
maxRightYAxisValue?: number; maxRightYAxisValue?: number;
selectedLeftYAxisLabel?: string; selectedLeftYAxisLabel?: string;
@ -40,5 +41,6 @@ export interface GraphProps {
min?: number; min?: number;
numberOfTicks?: number; numberOfTicks?: number;
barColors?: Array<string>; barColors?: Array<string>;
useCommonScale?: boolean;
yAxisProps?: YAxisProps; yAxisProps?: YAxisProps;
} }

41
mock/charts/mock-data.ts Normal file
View File

@ -0,0 +1,41 @@
export const graph_data_one_measures = {
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
yValues: [
{
key: 'measure_1',
values: [100, 140, 90, 80, 40]
}
]
};
export const graph_data_two_measures = {
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
yValues: [
{
key: 'measure_1',
values: [100, 140, 90, 80, 40]
},
{
key: 'measure_2',
values: [78, 82, 73, 56, 61],
formatLabel: (value: number) => `${value}%`
}
]
};
export const graph_data_three_measures = {
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
yValues: [
{
key: 'measure_1',
values: [100, 140, 90, 80, 40]
},
{
key: 'measure_2',
values: [78, 82, 73, 56, 61],
formatLabel: (value: number) => `${value}%`
},
{
key: 'measure_3',
values: [77, 32, 45, 65, 50]
}
]
};

View File

@ -14,7 +14,7 @@
"jest": { "jest": {
"preset": "jest-expo", "preset": "jest-expo",
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|d3-path)/)" "node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-native-svg-charts|d3-path)/)"
] ]
}, },
"dependencies": { "dependencies": {
@ -24,7 +24,7 @@
"@typescript-eslint/parser": "^6.17.0", "@typescript-eslint/parser": "^6.17.0",
"babel-plugin-inline-dotenv": "^1.7.0", "babel-plugin-inline-dotenv": "^1.7.0",
"d3-path": "^3.1.0", "d3-path": "^3.1.0",
"d3-scale": "1.0.6", "d3-scale": "^1.0.6",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
@ -37,7 +37,8 @@
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.72.6", "react-native": "0.72.6",
"react-native-dotenv": "^3.4.9", "react-native-dotenv": "^3.4.9",
"react-native-svg": "^14.1.0", "react-native-svg": "13.9.0",
"react-native-svg-charts": "^5.4.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"devDependencies": { "devDependencies": {
@ -46,6 +47,7 @@
"@testing-library/react-native": "^12.4.3", "@testing-library/react-native": "^12.4.3",
"@types/d3-path": "^3.0.2", "@types/d3-path": "^3.0.2",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"@types/react-native-svg-charts": "^5.0.16",
"eslint-config-prettier": "^9.1.0" "eslint-config-prettier": "^9.1.0"
}, },
"private": true "private": true

View File

@ -1,3 +1,6 @@
// GLOBAL STYLES
// COLORS:
// can be made more granular to specify utility (ex: fontColors vs backgroundColors)
export const colors = { export const colors = {
bgBlack: '#121212', bgBlack: '#121212',
lightGrey: '#BFC2C8', lightGrey: '#BFC2C8',

View File

@ -0,0 +1,20 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import BarGraph from '../../component/charts/bar-graph/bar-graph';
import { graph_data_two_measures } from '../../mock/charts/mock-data';
describe('BarGraph Component Tests', () => {
it('renders correctly with data', () => {
const { getByTestId } = render(<BarGraph data={graph_data_two_measures} testID='1'/>);
expect(getByTestId(`bar-graph-1`)).toBeTruthy();
});
it('does not render without data', () => {
// Have to ts-ignore to test null data conditions
// @ts-ignore
const { queryByTestId } = render(<BarGraph testID='2'/>);
expect(queryByTestId(`bar-graph-2`)).toBeNull();
});
});

View File

@ -26,7 +26,6 @@ describe('ChartView Component Tests', () => {
); );
const childText = getByText('Child Component'); const childText = getByText('Child Component');
console.log(childText)
expect(childText).toBeTruthy(); expect(childText).toBeTruthy();
}); });
}); });

View File

@ -29,8 +29,8 @@ describe('CustomBars Component Tests', () => {
x={mockXScaleFunction} x={mockXScaleFunction}
y={mockYScaleFunction} y={mockYScaleFunction}
bandwidth={mockBandwidth} bandwidth={mockBandwidth}
combinedData={mockCombinedData} barData={mockCombinedData}
rawData={mockRawData} xValues={mockRawData}
barColors={mockBarColors} barColors={mockBarColors}
gap={mockGap} gap={mockGap}
roundedRadius={mockRoundedRadius} roundedRadius={mockRoundedRadius}

791
yarn.lock

File diff suppressed because it is too large Load Diff