Merge pull request 'Logic handler for bar graph' (#21) from loewy/bar-graph-data into master
Reviewed-on: billnerds/rn-playground#21 Reviewed-by: Kat Huang <kkathuang@gmail.com>
This commit is contained in:
commit
4e3a93f126
10
component/charts/graph-config.ts
Normal file
10
component/charts/graph-config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// if values in chartDefaults should not be accessible as props & are a style, move to graphStyles
|
||||||
|
export const chartDefaults = {
|
||||||
|
height: 300,
|
||||||
|
spacingInner: 0.3,
|
||||||
|
spacingOuter: 0.2,
|
||||||
|
contentInset: { top: 30, bottom: 30 },
|
||||||
|
numberOfTicks: 6,
|
||||||
|
min: 0,
|
||||||
|
barColors: ['#598EBB', '#F2D4BC', '#DB7878']
|
||||||
|
};
|
@ -1,3 +1,44 @@
|
|||||||
import { ScaleLinear } from 'd3-scale'
|
import { ScaleLinear } from 'd3-scale'
|
||||||
|
|
||||||
export type ScaleFunction = ScaleLinear<number, number>;
|
export type ScaleFunction = ScaleLinear<number, number>;
|
||||||
|
|
||||||
|
export interface YAxisData {
|
||||||
|
key: string; // string value for ChartLabel and useGraphData
|
||||||
|
values: Array<number>;
|
||||||
|
// including this code only for review --
|
||||||
|
// do we prefer the idea of passing label formatting from the data or in the component?
|
||||||
|
// generic function type, specific usage of value varies
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
formatLabel?: (value: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type XValue = string | number
|
||||||
|
|
||||||
|
export interface GraphData {
|
||||||
|
xValues: Array<XValue>;
|
||||||
|
yValues: Array<YAxisData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface YAxisProps {
|
||||||
|
maxLeftYAxisValue?: number;
|
||||||
|
maxRightYAxisValue?: number;
|
||||||
|
selectedLeftYAxisLabel?: string;
|
||||||
|
selectedRightYAxisLabel?: string;
|
||||||
|
// generic function type, specific usage of value varies
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
formatRightYAxisLabel?: (value: string) => string;
|
||||||
|
// generic function type, specific usage of value varies
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
formatLeftYAxisLabel?: (value: string) => string;
|
||||||
|
}
|
||||||
|
export interface GraphProps {
|
||||||
|
data: GraphData;
|
||||||
|
height?: number;
|
||||||
|
spacingInner?: number;
|
||||||
|
spacingOuter?: number;
|
||||||
|
contentInset?: { top: number; bottom: number };
|
||||||
|
min?: number;
|
||||||
|
numberOfTicks?: number;
|
||||||
|
barColors?: Array<string>;
|
||||||
|
yAxisProps?: YAxisProps;
|
||||||
|
}
|
40
component/charts/graph-utils.tsx
Normal file
40
component/charts/graph-utils.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { GraphData } from "./graph-types";
|
||||||
|
|
||||||
|
|
||||||
|
export const convertToGraphData = (
|
||||||
|
graphData: GraphData,
|
||||||
|
options: {
|
||||||
|
selectedLeftYAxisLabel?: string;
|
||||||
|
selectedRightYAxisLabel?: string;
|
||||||
|
maxRightYAxisValue: number;
|
||||||
|
maxLeftYAxisValue: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const xValues = graphData.xValues;
|
||||||
|
const leftAxisIndex = graphData.yValues.findIndex(
|
||||||
|
(y) => y.key === options.selectedLeftYAxisLabel
|
||||||
|
);
|
||||||
|
const rightAxisIndex = graphData.yValues.findIndex(
|
||||||
|
(y) => y.key === options.selectedRightYAxisLabel
|
||||||
|
);
|
||||||
|
|
||||||
|
// scale data points according to max value
|
||||||
|
const yData = graphData.yValues.map((yAxis, index) => {
|
||||||
|
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 mappedData = scaledValues.map((scaledValue) => ({ value: scaledValue }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: mappedData,
|
||||||
|
svg: { fill: 'transparent' } // fill handled in CustomBars
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const yAxisLeftLabels = leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null;
|
||||||
|
const yAxisRightLabels = rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined;
|
||||||
|
|
||||||
|
return { yData, yAxisLeftLabels, yAxisRightLabels, xValues };
|
||||||
|
};
|
57
component/charts/use-graph-data.ts
Normal file
57
component/charts/use-graph-data.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { convertToGraphData } from './graph-utils';
|
||||||
|
import { chartDefaults } from './graph-config';
|
||||||
|
import { GraphData, GraphProps, XValue, YAxisData } from './graph-types';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface useGraphDataInterface {
|
||||||
|
xValues: Array<XValue>;
|
||||||
|
yData: {
|
||||||
|
data: {
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
svg: {
|
||||||
|
fill: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
yAxisLeftLabels: YAxisData;
|
||||||
|
yAxisRightLabels: YAxisData;
|
||||||
|
defaultProps: Partial<GraphProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this version assumes string values for X, this isn't necessarily the case
|
||||||
|
// convertToGraphData is specifically tailored to bar/group bar graphs
|
||||||
|
// ultimately this component could be used by any x & y axis graph types (line/bar/scatter)
|
||||||
|
export const useGraphData = (graphData: GraphData, props: Partial<GraphProps>): useGraphDataInterface => {
|
||||||
|
const { yAxisProps = {}, ...otherProps } = props;
|
||||||
|
const defaultProps = {
|
||||||
|
...chartDefaults,
|
||||||
|
...otherProps,
|
||||||
|
// assign default values for yAxisProps + spread to override with values coming from props
|
||||||
|
yAxisProps: {
|
||||||
|
maxLeftYAxisValue: Math.max(...(graphData.yValues[0]?.values ?? [0])),
|
||||||
|
maxRightYAxisValue:
|
||||||
|
graphData.yValues.length > 1 ? Math.max(...graphData.yValues[1]?.values) : undefined,
|
||||||
|
formatRightYAxisLabel: yAxisProps.formatRightYAxisLabel,
|
||||||
|
formatLeftYAxisLabel: yAxisProps.formatLeftYAxisLabel,
|
||||||
|
selectedLeftYAxisLabel: graphData.yValues[0]?.key,
|
||||||
|
selectedRightYAxisLabel: graphData.yValues[1]?.key,
|
||||||
|
...yAxisProps
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo(
|
||||||
|
() => convertToGraphData(graphData, defaultProps.yAxisProps),
|
||||||
|
[graphData, defaultProps.yAxisProps]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
xValues,
|
||||||
|
yData,
|
||||||
|
yAxisLeftLabels,
|
||||||
|
yAxisRightLabels,
|
||||||
|
defaultProps
|
||||||
|
};
|
||||||
|
};
|
58
test/component/use-graph-data.test.tsx
Normal file
58
test/component/use-graph-data.test.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { renderHook } from '@testing-library/react-native';
|
||||||
|
import { useGraphData } from '../../component/charts/use-graph-data';
|
||||||
|
import { GraphData, GraphProps } from '../../component/charts/graph-types';
|
||||||
|
|
||||||
|
describe('useGraphData', () => {
|
||||||
|
it('should return correctly processed data from convertToGraphData', () => {
|
||||||
|
// mock values
|
||||||
|
const mockGraphData: GraphData = {
|
||||||
|
xValues: ['full hit', '3/4 ball ball', '1/2 ball'],
|
||||||
|
yValues: [
|
||||||
|
{ key: 'left', values: [10, 20, 30] },
|
||||||
|
{ key: 'right', values: [40, 50, 60] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProps: Partial<GraphProps> = {
|
||||||
|
yAxisProps: {
|
||||||
|
maxLeftYAxisValue: 30,
|
||||||
|
maxRightYAxisValue: 60,
|
||||||
|
selectedLeftYAxisLabel: 'left',
|
||||||
|
selectedRightYAxisLabel: 'right',
|
||||||
|
formatRightYAxisLabel: (value) => `${value}%`,
|
||||||
|
formatLeftYAxisLabel: (value) => `${value}%`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGraphData(mockGraphData, mockProps));
|
||||||
|
// values expected
|
||||||
|
const expectedYData = [
|
||||||
|
{
|
||||||
|
data: [{ value: 33.33 }, { value: 66.67 }, { value: 100 }],
|
||||||
|
svg: { fill: 'transparent' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: [{ value: 66.67 }, { value: 83.33 }, { value: 100 }],
|
||||||
|
svg: { fill: 'transparent' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const expectedLeftLabels = { key: 'left', values: [10, 20, 30] };
|
||||||
|
const expectedRightLabels = { key: 'right', values: [40, 50, 60] };
|
||||||
|
|
||||||
|
|
||||||
|
expect(result.current).toBeDefined();
|
||||||
|
expect(result.current.xValues).toEqual(['full hit', '3/4 ball ball', '1/2 ball']);
|
||||||
|
result.current.yData.forEach((yDataItem, index) => {
|
||||||
|
yDataItem.data.forEach((dataItem, dataIndex) => {
|
||||||
|
expect(dataItem.value).toBeCloseTo(expectedYData[index].data[dataIndex].value, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(result.current.yAxisLeftLabels).toEqual(expectedLeftLabels);
|
||||||
|
expect(result.current.yAxisRightLabels).toEqual(expectedRightLabels);
|
||||||
|
expect(result.current.defaultProps.yAxisProps.selectedLeftYAxisLabel).toEqual('left');
|
||||||
|
expect(result.current.defaultProps.yAxisProps.selectedRightYAxisLabel).toEqual('right');
|
||||||
|
expect(result.current.defaultProps).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user