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:
commit
7e7a571f5a
113
component/charts/bar-graph/bar-graph.tsx
Normal file
113
component/charts/bar-graph/bar-graph.tsx
Normal 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;
|
@ -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;
|
||||||
|
@ -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 }
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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
41
mock/charts/mock-data.ts
Normal 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]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -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
|
||||||
|
@ -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',
|
||||||
@ -20,4 +23,4 @@ export const shadows = {
|
|||||||
shadowRadius: 4.65,
|
shadowRadius: 4.65,
|
||||||
elevation: 3
|
elevation: 3
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
20
test/component/bar-graph.test.tsx
Normal file
20
test/component/bar-graph.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -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}
|
||||||
|
Loading…
Reference in New Issue
Block a user