-
Notifications
You must be signed in to change notification settings - Fork 3.5k
feat: Add SearchLineChart component
#81049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9e179c2
a8c40b2
6340c82
2c42fde
acec8e6
03dc033
e53afcd
d5966d6
d3f8df9
c643578
513f1ac
b7b1a17
01ab33f
d584243
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| import {useFont} from '@shopify/react-native-skia'; | ||
| import React, {useCallback, useMemo, useState} from 'react'; | ||
| import type {LayoutChangeEvent} from 'react-native'; | ||
| import {View} from 'react-native'; | ||
| import Animated from 'react-native-reanimated'; | ||
| import {CartesianChart, Line, Scatter} from 'victory-native'; | ||
| import ActivityIndicator from '@components/ActivityIndicator'; | ||
| import ChartHeader from '@components/Charts/components/ChartHeader'; | ||
| import ChartTooltip from '@components/Charts/components/ChartTooltip'; | ||
| import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants'; | ||
| import fontSource from '@components/Charts/font'; | ||
| import type {HitTestArgs} from '@components/Charts/hooks'; | ||
| import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks'; | ||
| import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types'; | ||
| import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
| import useTheme from '@hooks/useTheme'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import variables from '@styles/variables'; | ||
|
|
||
| /** Inner dot radius for line chart data points */ | ||
| const DOT_INNER_RADIUS = 6; | ||
|
|
||
| type LineChartProps = CartesianChartProps & { | ||
| /** Callback when a data point is pressed */ | ||
| onPointPress?: (dataPoint: ChartDataPoint, index: number) => void; | ||
| }; | ||
|
|
||
| function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUnitPosition = 'left', onPointPress}: LineChartProps) { | ||
| const theme = useTheme(); | ||
| const styles = useThemeStyles(); | ||
| const {shouldUseNarrowLayout} = useResponsiveLayout(); | ||
| const font = useFont(fontSource, variables.iconSizeExtraSmall); | ||
| const [chartWidth, setChartWidth] = useState(0); | ||
| const [containerHeight, setContainerHeight] = useState(0); | ||
|
|
||
| const yAxisDomain = useDynamicYDomain(data); | ||
|
|
||
| const chartData = useMemo(() => { | ||
| return data.map((point, index) => ({ | ||
| x: index, | ||
| y: point.total, | ||
| })); | ||
| }, [data]); | ||
|
|
||
| const handlePointPress = useCallback( | ||
| (index: number) => { | ||
| if (index < 0 || index >= data.length) { | ||
| return; | ||
| } | ||
| const dataPoint = data.at(index); | ||
| if (dataPoint && onPointPress) { | ||
| onPointPress(dataPoint, index); | ||
| } | ||
| }, | ||
| [data, onPointPress], | ||
| ); | ||
|
|
||
| const handleLayout = useCallback((event: LayoutChangeEvent) => { | ||
| const {width, height} = event.nativeEvent.layout; | ||
| setChartWidth(width); | ||
| setContainerHeight(height); | ||
| }, []); | ||
|
|
||
| const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({ | ||
| data, | ||
| font, | ||
| chartWidth, | ||
| barAreaWidth: chartWidth, | ||
| containerHeight, | ||
| }); | ||
|
|
||
| const {formatXAxisLabel, formatYAxisLabel} = useChartLabelFormats({ | ||
| data, | ||
| yAxisUnit, | ||
| yAxisUnitPosition, | ||
| labelSkipInterval, | ||
| labelRotation, | ||
| truncatedLabels, | ||
| }); | ||
|
|
||
| const checkIsOverDot = useCallback((args: HitTestArgs) => { | ||
| 'worklet'; | ||
|
|
||
| const dx = args.cursorX - args.targetX; | ||
| const dy = args.cursorY - args.targetY; | ||
| return Math.sqrt(dx * dx + dy * dy) <= DOT_INNER_RADIUS; | ||
| }, []); | ||
|
|
||
| const {actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle} = useChartInteractions({ | ||
| handlePress: handlePointPress, | ||
| checkIsOver: checkIsOverDot, | ||
| }); | ||
|
|
||
| const tooltipData = useTooltipData(activeDataIndex, data, yAxisUnit, yAxisUnitPosition); | ||
|
|
||
| const dynamicChartStyle = useMemo( | ||
| () => ({ | ||
| height: CHART_CONTENT_MIN_HEIGHT + (maxLabelLength ?? 0), | ||
| }), | ||
| [maxLabelLength], | ||
| ); | ||
|
|
||
| if (isLoading || !font) { | ||
| return ( | ||
| <View style={[styles.lineChartContainer, styles.highlightBG, shouldUseNarrowLayout ? styles.p5 : styles.p8, styles.justifyContentCenter, styles.alignItemsCenter]}> | ||
| <ActivityIndicator size="large" /> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| if (data.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <View style={[styles.lineChartContainer, styles.highlightBG, shouldUseNarrowLayout ? styles.p5 : styles.p8]}> | ||
| <ChartHeader | ||
| title={title} | ||
| titleIcon={titleIcon} | ||
| /> | ||
| <View | ||
| style={[styles.lineChartChartContainer, labelRotation === -90 ? dynamicChartStyle : undefined]} | ||
| onLayout={handleLayout} | ||
| > | ||
| {chartWidth > 0 && ( | ||
| <CartesianChart | ||
| xKey="x" | ||
| padding={CHART_PADDING} | ||
| yKeys={['y']} | ||
| domainPadding={{left: 20, right: 20, top: 20, bottom: 20}} | ||
| actionsRef={actionsRef} | ||
| customGestures={customGestures} | ||
| xAxis={{ | ||
| font, | ||
| tickCount: data.length, | ||
| labelColor: theme.textSupporting, | ||
| lineWidth: X_AXIS_LINE_WIDTH, | ||
| formatXLabel: formatXAxisLabel, | ||
| labelRotate: labelRotation, | ||
| labelOverflow: 'visible', | ||
| }} | ||
| yAxis={[ | ||
| { | ||
| font, | ||
| labelColor: theme.textSupporting, | ||
| formatYLabel: formatYAxisLabel, | ||
| tickCount: Y_AXIS_TICK_COUNT, | ||
| lineWidth: Y_AXIS_LINE_WIDTH, | ||
| lineColor: theme.border, | ||
| labelOffset: Y_AXIS_LABEL_OFFSET, | ||
| domain: yAxisDomain, | ||
| }, | ||
| ]} | ||
| frame={{lineWidth: {left: 1, bottom: 1, top: 0, right: 0}}} | ||
| data={chartData} | ||
| > | ||
| {({points}) => ( | ||
| <> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ CONSISTENCY-2 (docs)The hardcoded Suggested fix: Add to /** Stroke width for line chart lines */
const LINE_STROKE_WIDTH = 2;Then import and use it: strokeWidth={LINE_STROKE_WIDTH}Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| <Line | ||
| points={points.y} | ||
| color={DEFAULT_CHART_COLOR} | ||
| strokeWidth={2} | ||
| curveType="linear" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ CONSISTENCY-2 (docs)Same magic number issue as line 190 - the hardcoded color index Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| /> | ||
| <Scatter | ||
| points={points.y} | ||
| radius={DOT_INNER_RADIUS} | ||
| color={DEFAULT_CHART_COLOR} | ||
| /> | ||
| </> | ||
| )} | ||
| </CartesianChart> | ||
| )} | ||
| {isTooltipActive && !!tooltipData && ( | ||
| <Animated.View style={tooltipStyle}> | ||
| <ChartTooltip | ||
| label={tooltipData.label} | ||
| amount={tooltipData.amount} | ||
| percentage={tooltipData.percentage} | ||
| /> | ||
| </Animated.View> | ||
| )} | ||
| </View> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| export default LineChartContent; | ||
| export type {LineChartProps}; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import React from 'react'; | ||
| import type {LineChartProps} from './LineChartContent'; | ||
| import LineChartContent from './LineChartContent'; | ||
|
|
||
| function LineChart(props: LineChartProps) { | ||
| // eslint-disable-next-line react/jsx-props-no-spreading | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ CONSISTENCY-5 (docs)The ESLint rule disable lacks an accompanying comment explaining why the rule is being disabled. Suggested fix: function LineChart(props: LineChartProps) {
// eslint-disable-next-line react/jsx-props-no-spreading -- Spreading props is necessary here to pass all LineChartProps to the platform-specific content component
return <LineChartContent {...props} />;
}This ensures team members understand why spreading props is acceptable in this platform wrapper component. Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| return <LineChartContent {...props} />; | ||
| } | ||
|
|
||
| LineChart.displayName = 'LineChart'; | ||
|
|
||
| export default LineChart; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ CONSISTENCY-2 (docs)
The hardcoded color index
5is a magic number without documentation or a named constant.Suggested fix:
Define a named constant at the top of the file:
Then use it:
And similarly on line 197 for the Scatter component.
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.