Actually respect prettier configuration

This commit is contained in:
2024-02-03 20:23:31 -07:00
parent a6883a624a
commit 2276605e6d
37 changed files with 1629 additions and 1497 deletions

View File

@@ -1,116 +1,137 @@
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 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 { 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';
import ChartLabel from '../chart-label/chart-label';
import { CustomBars } from "../custom-bars";
import { CustomGrid } from "../custom-grid";
import { graphStyles } from "../chart-styles";
import ChartView from "../chart-view";
import ChartLabel from "../chart-label/chart-label";
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;
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
} // TODO:#38
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 #38
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useGraphData(data, { includeColors: false, ...props});
export const BarGraph: React.FC<Props> = ({
data,
useCommonScale = false,
testID,
...props
}) => {
if (!data) {
return null;
} // TODO:#38
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 #38
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useGraphData(data, { includeColors: false, ...props });
// 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 title = 'Shots Taken / Make Percentage by Cut Angle'
// 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 title = "Shots Taken / Make Percentage by Cut Angle";
return (
<ChartView>
<ChartLabel title={title} yLabels={yLabels} />
<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}
/>
return (
<ChartView>
<ChartLabel title={title} yLabels={yLabels} />
<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}
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>
<View style={graphStyles.flex}>
<BarChart
style={graphStyles.flex}
data={yData}
gridMin={min}
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>
);
<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;
export default BarGraph;

View File

@@ -1,48 +1,48 @@
import React from 'react';
import { Path } from 'react-native-svg';
import React from "react";
import { Path } from "react-native-svg";
import { calculateBarOrigin, drawBarPath } from './custom-bar-utils';
import { ScaleBandType, ScaleLinearType, } from './graph-types';
import { calculateBarOrigin, drawBarPath } from "./custom-bar-utils";
import { ScaleBandType, ScaleLinearType } from "./graph-types";
interface BarProps {
scaleX: ScaleBandType;
scaleY: ScaleLinearType;
data: { value: number };
barNumber: number;
index: number;
fill: string;
barWidth: number;
gap: number;
roundedRadius: number;
scaleX: ScaleBandType;
scaleY: ScaleLinearType;
data: { value: number };
barNumber: number;
index: number;
fill: string;
barWidth: number;
gap: number;
roundedRadius: number;
}
export const Bar: React.FC<BarProps> = ({
scaleX,
scaleY,
data,
barNumber,
index,
fill,
barWidth,
gap,
roundedRadius
scaleX,
scaleY,
data,
barNumber,
index,
fill,
barWidth,
gap,
roundedRadius,
}) => {
const { xOrigin, yOrigin, height } = calculateBarOrigin({
scaleX,
scaleY,
index,
data,
barNumber,
barWidth,
gap
});
const { xOrigin, yOrigin, height } = calculateBarOrigin({
scaleX,
scaleY,
index,
data,
barNumber,
barWidth,
gap,
});
return (
<Path
key={`bar-path-${barNumber}-${index}`}
d={drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius)}
fill={fill}
testID={`bar-${barNumber}-${index}`}
/>
);
return (
<Path
key={`bar-path-${barNumber}-${index}`}
d={drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius)}
fill={fill}
testID={`bar-${barNumber}-${index}`}
/>
);
};

View File

@@ -1,42 +1,44 @@
import React from 'react';
import { Text, View } from 'react-native';
import React from "react";
import { Text, View } from "react-native";
import { chartLabel } from '../chart-styles';
import { chartLabel } from "../chart-styles";
type Axis = 'RIGHT' | 'LEFT'
type Axis = "RIGHT" | "LEFT";
interface YLabel {
axis: Axis;
displayName: string;
color: string;
axis: Axis;
displayName: string;
color: string;
}
type ChartLabelProps = {
title: string;
yLabels: Array<YLabel>;
title: string;
yLabels: Array<YLabel>;
};
const renderLabels = (yLabels: Array<YLabel>) => {
return yLabels.map((label) => (
<View key={`${label.axis}-${label.displayName}`} style={chartLabel.labelInnerRow}>
<View
style={[chartLabel.labelColorBox, { backgroundColor: label.color }]}
/>
<View style={chartLabel.labelTextMargin}>
<Text style={chartLabel.labelText}>{label.displayName}</Text>
</View>
</View>
));
return yLabels.map((label) => (
<View
key={`${label.axis}-${label.displayName}`}
style={chartLabel.labelInnerRow}
>
<View
style={[chartLabel.labelColorBox, { backgroundColor: label.color }]}
/>
<View style={chartLabel.labelTextMargin}>
<Text style={chartLabel.labelText}>{label.displayName}</Text>
</View>
</View>
));
};
export default function ChartLabel({ title, yLabels }: ChartLabelProps) {
return (
<View>
<View style={chartLabel.titleRow}>
<Text style={chartLabel.titleText}>{title}</Text>
</View>
<View style={chartLabel.labelOuterRow}>{renderLabels(yLabels)}</View>
</View>
);
}
return (
<View>
<View style={chartLabel.titleRow}>
<Text style={chartLabel.titleText}>{title}</Text>
</View>
<View style={chartLabel.labelOuterRow}>{renderLabels(yLabels)}</View>
</View>
);
}

View File

@@ -1,46 +1,46 @@
import { StyleSheet } from 'react-native';
import { StyleSheet } from "react-native";
import { colors, shadows } from '../../styles';
import { colors, shadows } from "../../styles";
export const graphStyles = StyleSheet.create({
container: {
backgroundColor: colors.panelWhite,
borderColor: 'black',
borderRadius: 5,
marginVertical: 10,
marginHorizontal: 15,
paddingTop: 15,
paddingHorizontal: 15,
...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 },
horizontalInset: { right: 10, left: 10 },
container: {
backgroundColor: colors.panelWhite,
borderColor: "black",
borderRadius: 5,
marginVertical: 10,
marginHorizontal: 15,
paddingTop: 15,
paddingHorizontal: 15,
...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 },
horizontalInset: { right: 10, left: 10 },
});
export const chartLabel = StyleSheet.create({
titleRow: {
flexDirection: 'row',
marginHorizontal: 10
},
titleText: { fontWeight: '500' },
labelOuterRow: {
flexDirection: 'row',
flexWrap: 'wrap',
marginHorizontal: 10
},
labelInnerRow: { flexDirection: 'row', alignItems: 'center', marginTop: 5 },
labelColorBox: {
height: 15,
width: 15,
borderRadius: 4,
marginRight: 2
},
labelTextMargin: { marginRight: 15 },
labelText: { fontSize: 12 }
titleRow: {
flexDirection: "row",
marginHorizontal: 10,
},
titleText: { fontWeight: "500" },
labelOuterRow: {
flexDirection: "row",
flexWrap: "wrap",
marginHorizontal: 10,
},
labelInnerRow: { flexDirection: "row", alignItems: "center", marginTop: 5 },
labelColorBox: {
height: 15,
width: 15,
borderRadius: 4,
marginRight: 2,
},
labelTextMargin: { marginRight: 15 },
labelText: { fontSize: 12 },
});

View File

@@ -1,16 +1,20 @@
import React from 'react';
import { StyleProp, View, ViewStyle } from 'react-native';
import React from "react";
import { StyleProp, View, ViewStyle } from "react-native";
import { graphStyles } from './chart-styles';
import { graphStyles } from "./chart-styles";
interface ChartViewProps {
style?: StyleProp<ViewStyle>;
children: React.ReactNode;
testID?: string;
style?: StyleProp<ViewStyle>;
children: React.ReactNode;
testID?: string;
}
const ChartView: React.FC<ChartViewProps> = ({ children, style, testID }) => {
return <View style={[graphStyles.container, style]} testID={testID} >{children}</View>;
return (
<View style={[graphStyles.container, style]} testID={testID}>
{children}
</View>
);
};
export default ChartView;
export default ChartView;

View File

@@ -1,59 +1,63 @@
import { path as d3 } from 'd3-path';
import { path as d3 } from "d3-path";
import { ScaleLinearType, ScaleBandType } from './graph-types';
import { ScaleLinearType, ScaleBandType } from "./graph-types";
type BarCalculationProps = {
scaleX: ScaleBandType;
scaleY: ScaleLinearType;
data: { value: number };
barNumber: number;
index: number;
barWidth: number;
gap: number;
scaleX: ScaleBandType;
scaleY: ScaleLinearType;
data: { value: number };
barNumber: number;
index: number;
barWidth: number;
gap: number;
};
export function calculateBarOrigin({
scaleX,
scaleY,
barWidth,
data,
index,
barNumber,
gap
scaleX,
scaleY,
barWidth,
data,
index,
barNumber,
gap,
}: BarCalculationProps): { xOrigin: number; yOrigin: number; height: number } {
const firstBar = barNumber === 0;
const xOrigin = scaleX(index) + (firstBar ? 0 : barWidth * barNumber + gap * barNumber);
const yOrigin = scaleY(data.value);
const height = scaleY(0) - yOrigin;
const firstBar = barNumber === 0;
const xOrigin =
scaleX(index) + (firstBar ? 0 : barWidth * barNumber + gap * barNumber);
const yOrigin = scaleY(data.value);
const height = scaleY(0) - yOrigin;
return { xOrigin, yOrigin, height };
return { xOrigin, yOrigin, height };
}
export function drawBarPath(
xOrigin: number,
yOrigin: number,
barWidth: number,
height: number,
roundedRadius: number
xOrigin: number,
yOrigin: number,
barWidth: number,
height: number,
roundedRadius: number,
): string {
const path = d3();
path.moveTo(xOrigin, yOrigin + height);
path.lineTo(xOrigin, yOrigin + roundedRadius);
path.arcTo(xOrigin, yOrigin, xOrigin + roundedRadius, yOrigin, roundedRadius);
path.lineTo(xOrigin + barWidth - roundedRadius, yOrigin);
path.arcTo(
xOrigin + barWidth,
yOrigin,
xOrigin + barWidth,
yOrigin + roundedRadius,
roundedRadius
);
path.lineTo(xOrigin + barWidth, yOrigin + height);
path.lineTo(xOrigin, yOrigin + height);
path.closePath();
const path = d3();
path.moveTo(xOrigin, yOrigin + height);
path.lineTo(xOrigin, yOrigin + roundedRadius);
path.arcTo(xOrigin, yOrigin, xOrigin + roundedRadius, yOrigin, roundedRadius);
path.lineTo(xOrigin + barWidth - roundedRadius, yOrigin);
path.arcTo(
xOrigin + barWidth,
yOrigin,
xOrigin + barWidth,
yOrigin + roundedRadius,
roundedRadius,
);
path.lineTo(xOrigin + barWidth, yOrigin + height);
path.lineTo(xOrigin, yOrigin + height);
path.closePath();
return path.toString();
return path.toString();
}
export const calculateBarWidth = (bandwidth: number, combinedDataLength: number, gap: number) =>
(bandwidth - gap * (combinedDataLength - 1)) / combinedDataLength;
export const calculateBarWidth = (
bandwidth: number,
combinedDataLength: number,
gap: number,
) => (bandwidth - gap * (combinedDataLength - 1)) / combinedDataLength;

View File

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

View File

@@ -1,20 +1,20 @@
import React from 'react';
import { G, Line } from 'react-native-svg';
import { colors } from '../../styles';
import { ScaleBandType, ScaleLinearType } from './graph-types';
import React from "react";
import { G, Line } from "react-native-svg";
import { colors } from "../../styles";
import { ScaleBandType, ScaleLinearType } from "./graph-types";
interface CustomGridProps {
x: ScaleBandType;
y: ScaleLinearType;
ticks: Array<number>;
includeVertical?: boolean;
xTicks?: Array<number | string>;
x: ScaleBandType;
y: ScaleLinearType;
ticks: Array<number>;
includeVertical?: boolean;
xTicks?: Array<number | string>;
}
/**
* CustomGrid Component
*
*
* This component is used within a react-native-svg-chart component to render grid lines.
*
*
* @param {ScaleBandType} x - X-axis scale function, received from the parent chart component.
* @param {ScaleLinearType} y - Y-axis scale function, received from the parent chart component.
* @param {Array<number>} ticks - Y-axis tick values for horizontal lines, received from the parent.
@@ -34,55 +34,74 @@ interface CustomGridProps {
* Note: Use `includeVertical` cautiously; vertical lines are not fully developed.
*/
export const CustomGrid: React.FC<Partial<CustomGridProps>> = ({
x,
y,
ticks,
xTicks,
includeVertical = false
x,
y,
ticks,
xTicks,
includeVertical = false,
}) => {
const [firstTick, ...remainingTicks] = ticks;
const dashArray = [1, 3];
const strokeSolidWidth = 0.2;
const strokeSolidColor = colors.bgBlack;
const strokeDashWidth = 1;
const strokeDashColor = colors.lightGrey;
const [firstTick, ...remainingTicks] = ticks;
const dashArray = [1, 3];
const strokeSolidWidth = 0.2;
const strokeSolidColor = colors.bgBlack;
const strokeDashWidth = 1;
const strokeDashColor = colors.lightGrey;
const renderHorizontalLine = (tick: number, stroke: string, strokeWidth: number, dashArray?: number[]) => (
<Line
key={`line-${tick}`}
x1="0%"
x2="100%"
y1={y(tick)}
y2={y(tick)}
stroke={stroke}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
/>
);
const renderHorizontalLine = (
tick: number,
stroke: string,
strokeWidth: number,
dashArray?: number[],
) => (
<Line
key={`line-${tick}`}
x1="0%"
x2="100%"
y1={y(tick)}
y2={y(tick)}
stroke={stroke}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
/>
);
const topY = y(Math.max(...ticks));
const bottomY = y(Math.min(...ticks));
const renderVerticalLine = (tick: number, stroke: string, strokeWidth: number, dashArray?: number[]) => {
return (
<Line
key={`vertical-line-${tick}`}
x1={x(tick)}
x2={x(tick)}
y1={topY}
y2={bottomY}
stroke={stroke}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
/>
);
};
const topY = y(Math.max(...ticks));
const bottomY = y(Math.min(...ticks));
const renderVerticalLine = (
tick: number,
stroke: string,
strokeWidth: number,
dashArray?: number[],
) => {
return (
<Line
key={`vertical-line-${tick}`}
x1={x(tick)}
x2={x(tick)}
y1={topY}
y2={bottomY}
stroke={stroke}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
/>
);
};
return (
<G>
{renderHorizontalLine(firstTick, strokeSolidColor, strokeSolidWidth)}
{remainingTicks.map((tick) => renderHorizontalLine(tick, strokeDashColor, strokeDashWidth, dashArray))}
{includeVertical && xTicks.map((_, index) => renderVerticalLine(index, strokeDashColor, strokeDashWidth, dashArray))}
</G>
);
return (
<G>
{renderHorizontalLine(firstTick, strokeSolidColor, strokeSolidWidth)}
{remainingTicks.map((tick) =>
renderHorizontalLine(tick, strokeDashColor, strokeDashWidth, dashArray),
)}
{includeVertical &&
xTicks.map((_, index) =>
renderVerticalLine(
index,
strokeDashColor,
strokeDashWidth,
dashArray,
),
)}
</G>
);
};

View File

@@ -1,13 +1,13 @@
// TODO: Style values should be moved to styles
// non-style config can live here as chartDefaults
export const chartDefaults = {
height: 300,
spacingInner: 0.3,
spacingOuter: 0.2,
contentInset: { top: 30, bottom: 30 },
numberOfTicks: 6,
min: 0,
barColors: ['#598EBB', '#F2D4BC', '#DB7878'],
includeColors: true,
lineStrokeWidth: 2
};
height: 300,
spacingInner: 0.3,
spacingOuter: 0.2,
contentInset: { top: 30, bottom: 30 },
numberOfTicks: 6,
min: 0,
barColors: ["#598EBB", "#F2D4BC", "#DB7878"],
includeColors: true,
lineStrokeWidth: 2,
};

View File

@@ -1,54 +1,54 @@
import { ScaleBand, ScaleLinear } from 'd3-scale'
import { ScaleBand, ScaleLinear } from "d3-scale";
export type ScaleLinearType = ScaleLinear<number, number>;
export type ScaleBandType = ScaleBand<number | string>;
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;
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 type XValue = string | number;
export interface GraphData {
xValues: Array<XValue>;
yValues: Array<YAxisData>;
xValues: Array<XValue>;
yValues: Array<YAxisData>;
}
export 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;
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;
includeColors?: boolean;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
lineStrokeWidth?: number;
useCommonScale?: boolean;
yAxisProps?: YAxisProps;
data: GraphData;
includeColors?: boolean;
height?: number;
spacingInner?: number;
spacingOuter?: number;
contentInset?: { top: number; bottom: number };
min?: number;
numberOfTicks?: number;
barColors?: Array<string>;
lineStrokeWidth?: number;
useCommonScale?: boolean;
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.
@@ -63,16 +63,16 @@ export interface GraphProps {
* @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;
}
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

@@ -1,43 +1,52 @@
import { GraphData } from "./graph-types";
export const convertToGraphData = (
graphData: GraphData,
options: {
selectedLeftYAxisLabel?: string;
selectedRightYAxisLabel?: string;
maxRightYAxisValue: number;
maxLeftYAxisValue: number;
includeColors: boolean;
barColors: Array<string>
}
graphData: GraphData,
options: {
selectedLeftYAxisLabel?: string;
selectedRightYAxisLabel?: string;
maxRightYAxisValue: number;
maxLeftYAxisValue: number;
includeColors: boolean;
barColors: Array<string>;
},
) => {
const xValues = graphData.xValues;
const leftAxisIndex = graphData.yValues.findIndex(
(y) => y.key === options.selectedLeftYAxisLabel
);
const rightAxisIndex = graphData.yValues.findIndex(
(y) => y.key === options.selectedRightYAxisLabel
);
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 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);
// scale values as a percentage of the max value
const scaledValues = yAxis.values.map((value) => (value / maxValue) * 100);
const strokeColor = options.includeColors && options.barColors ? options.barColors[index % options.barColors.length] : 'transparent';
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 {
data: mappedData,
svg: { fill: 'transparent', stroke: strokeColor } // Apply the stroke color here
};
});
const yAxisLeftLabels = leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null;
const yAxisRightLabels = rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined;
return {
data: mappedData,
svg: { fill: "transparent", stroke: strokeColor }, // Apply the stroke color here
};
});
const yAxisLeftLabels =
leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null;
const yAxisRightLabels =
rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined;
return { yData, yAxisLeftLabels, yAxisRightLabels, xValues };
return { yData, yAxisLeftLabels, yAxisRightLabels, xValues };
};

View File

@@ -1,90 +1,103 @@
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 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';
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);
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>
);
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

@@ -1,57 +1,70 @@
import { useMemo } from 'react';
import { convertToGraphData } from './graph-utils';
import { chartDefaults } from './graph-config';
import { GraphData, GraphProps, XValue, YAxisData } from './graph-types';
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>;
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
}
};
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, includeColors: defaultProps.includeColors, barColors: defaultProps.barColors}),
[graphData, defaultProps.yAxisProps, defaultProps.includeColors, defaultProps.barColors]
);
const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo(
() =>
convertToGraphData(graphData, {
...defaultProps.yAxisProps,
includeColors: defaultProps.includeColors,
barColors: defaultProps.barColors,
}),
[
graphData,
defaultProps.yAxisProps,
defaultProps.includeColors,
defaultProps.barColors,
],
);
return {
xValues,
yData,
yAxisLeftLabels,
yAxisRightLabels,
defaultProps
};
return {
xValues,
yData,
yAxisLeftLabels,
yAxisRightLabels,
defaultProps,
};
};