chore: import upstream snapshot with attribution
CI / lint (push) Failing after 0s
CI / build-library (push) Failing after 1s
CI / build-web (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:31 +08:00
commit 9bf9d719f7
141 changed files with 21303 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
import { StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { GraphPage } from './screens/GraphPage';
export default function App() {
return (
<GestureHandlerRootView style={styles.container}>
<GraphPage />
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
+6
View File
@@ -0,0 +1,6 @@
import { Dimensions } from 'react-native';
export const SCREEN_WIDTH = Dimensions.get('window').width;
export const GRAPH_DISPLAY_MODES = ['6h', '1d', '1w', '1m', '3m'] as const;
export type GraphDisplayMode = (typeof GRAPH_DISPLAY_MODES)[number];
@@ -0,0 +1,57 @@
/**
* An example for a custom SelectionDot component.
*
* Usage:
*
* ```jsx
* <LineGraph
* points={priceHistory}
* animated={true}
* enablePanGesture={true}
* SelectionDot={CustomSelectionDot}
* />
* ```
*
* This example has removed the outer ring and light
* shadow from the default one to make it more flat.
*/
import React, { useCallback } from 'react';
import {
runOnJS,
useAnimatedReaction,
withSpring,
useSharedValue,
} from 'react-native-reanimated';
import { Circle } from '@shopify/react-native-skia';
import type { SelectionDotProps } from 'react-native-graph';
export function SelectionDot({
isActive,
color,
circleX,
circleY,
}: SelectionDotProps): React.ReactElement {
const circleRadius = useSharedValue(0);
const setIsActive = useCallback(
(active: boolean) => {
circleRadius.value = withSpring(active ? 5 : 0, {
mass: 1,
stiffness: 1000,
damping: 50,
velocity: 0,
});
},
[circleRadius]
);
useAnimatedReaction(
() => isActive.value,
(active) => {
runOnJS(setIsActive)(active);
},
[isActive, setIsActive]
);
return <Circle cx={circleX} cy={circleY} r={circleRadius} color={color} />;
}
@@ -0,0 +1,111 @@
import { useColors } from '../hooks/useColors';
import React, { useCallback, useState } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import type { LayoutChangeEvent, ViewProps } from 'react-native';
import { PressableScale } from 'react-native-pressable-scale';
import Reanimated, {
Easing,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
import { GRAPH_DISPLAY_MODES, SCREEN_WIDTH } from '../Constants';
import type { GraphDisplayMode } from '../Constants';
interface Props extends ViewProps {
graphDisplayMode: GraphDisplayMode;
setGraphDisplayMode: (graphDisplayMode: GraphDisplayMode) => void;
}
export const SPACING = 5;
export const ESTIMATED_BUTTON_WIDTH =
(SCREEN_WIDTH - 50) / GRAPH_DISPLAY_MODES.length;
export function GraphDisplayModeSelector({
graphDisplayMode,
setGraphDisplayMode,
style,
...props
}: Props): React.ReactElement {
const colors = useColors();
const [width, setWidth] = useState(ESTIMATED_BUTTON_WIDTH);
const onLayout = useCallback(
({ nativeEvent: { layout } }: LayoutChangeEvent) => {
setWidth(Math.round(layout.width));
},
[]
);
const buttonWidth = width / GRAPH_DISPLAY_MODES.length - 2 * SPACING;
const selectedModeIndex = GRAPH_DISPLAY_MODES.indexOf(graphDisplayMode);
const selectionBackgroundStyle = useAnimatedStyle(() => {
return {
width: buttonWidth,
opacity: withTiming(selectedModeIndex === -1 ? 0 : 1, {
easing: Easing.linear,
duration: 150,
}),
transform: [
{
translateX: withSpring(
buttonWidth * selectedModeIndex + 2 * SPACING * selectedModeIndex,
{
mass: 1,
stiffness: 900,
damping: 300,
}
),
},
],
};
}, [buttonWidth, selectedModeIndex]);
return (
<View {...props} onLayout={onLayout} style={[styles.container, style]}>
<Reanimated.View
style={[
styles.selectionBackground,
{ backgroundColor: colors.background },
selectionBackgroundStyle,
]}
/>
{GRAPH_DISPLAY_MODES.map((displayMode) => (
<View key={displayMode} style={styles.buttonContainer}>
<PressableScale
style={styles.button}
onPress={() => setGraphDisplayMode(displayMode)}
>
<Text style={{ color: colors.foreground }}>
{displayMode.toUpperCase()}
</Text>
</PressableScale>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-between',
},
selectionBackground: {
position: 'absolute',
height: '100%',
marginLeft: SPACING,
borderRadius: 7,
},
buttonContainer: {
flex: 1,
},
button: {
flex: 1,
alignItems: 'center',
marginHorizontal: SPACING,
paddingVertical: 2.5,
},
});
+49
View File
@@ -0,0 +1,49 @@
import SegmentedControl from '@react-native-segmented-control/segmented-control';
import { StyleSheet, Text, View } from 'react-native';
import { useColors } from '../hooks/useColors';
interface Props {
title: string;
isEnabled: boolean;
setIsEnabled: (isEnabled: boolean) => void;
}
export function Toggle({ title, isEnabled, setIsEnabled }: Props) {
const colors = useColors();
return (
<View style={styles.row}>
<Text style={[styles.toggleText, { color: colors.foreground }]}>
{title}
</Text>
<View style={styles.spacer} />
<SegmentedControl
style={styles.segmentedControl}
values={['yes', 'no']}
selectedIndex={isEnabled ? 0 : 1}
onValueChange={(v) => setIsEnabled(v === 'yes')}
/>
</View>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 5,
},
spacer: {
flexGrow: 1,
},
toggleText: {
fontSize: 18,
fontWeight: '600',
},
segmentedControl: {
marginLeft: 10,
width: 140,
},
});
+28
View File
@@ -0,0 +1,28 @@
import type { GraphPoint } from '../../../src/LineGraphProps';
import gaussian from 'gaussian';
function weightedRandom(mean: number, variance: number): number {
var distribution = gaussian(mean, variance);
// Take a random sample using inverse transform sampling method.
return distribution.ppf(Math.random());
}
export function generateRandomGraphData(length: number): GraphPoint[] {
return Array<number>(length)
.fill(0)
.map((_, index) => ({
date: new Date(
new Date(2000, 0, 1).getTime() + 1000 * 60 * 60 * 24 * index
),
value: weightedRandom(10, Math.pow(index + 1, 2)),
}));
}
export function generateSinusGraphData(length: number): GraphPoint[] {
return Array<number>(length)
.fill(0)
.map((_, index) => ({
date: new Date(index),
value: Math.sin(index),
}));
}
+21
View File
@@ -0,0 +1,21 @@
import { useColorScheme } from 'react-native';
interface Palette {
background: string;
foreground: string;
}
const dark: Palette = {
background: '#333',
foreground: '#eee',
};
const light: Palette = {
background: '#fff',
foreground: '#333',
};
export function useColors(): Palette {
const isDarkMode = useColorScheme() === 'dark';
return isDarkMode ? dark : light;
}
+195
View File
@@ -0,0 +1,195 @@
import { useCallback, useMemo, useState } from 'react'
import { View, StyleSheet, Text, Button, ScrollView } from 'react-native'
import { LineGraph } from 'react-native-graph'
// import StaticSafeAreaInsets from 'react-native-static-safe-area-insets'
import type { GraphRange } from '../../../src/LineGraphProps'
import { SelectionDot } from '../components/CustomSelectionDot'
import { Toggle } from '../components/Toggle'
import {
generateRandomGraphData,
generateSinusGraphData,
} from '../data/GraphData'
import { HapticFeedbackTypes } from 'react-native-haptic-feedback'
import { useColors } from '../hooks/useColors'
import { hapticFeedback } from '../utils/HapticFeedback'
const POINT_COUNT = 70
const POINTS = generateRandomGraphData(POINT_COUNT)
const COLOR = '#6a7ee7'
const GRADIENT_FILL_COLORS = ['#7476df5D', '#7476df4D', '#7476df00']
const SMALL_POINTS = generateSinusGraphData(9)
export function GraphPage() {
const colors = useColors()
const [isAnimated, setIsAnimated] = useState(true)
const [enablePanGesture, setEnablePanGesture] = useState(true)
const [enableFadeInEffect, setEnableFadeInEffect] = useState(false)
const [enableCustomSelectionDot, setEnableCustomSelectionDot] =
useState(false)
const [enableGradient, setEnableGradient] = useState(false)
const [enableRange, setEnableRange] = useState(false)
const [enableIndicator, setEnableIndicator] = useState(false)
const [indicatorPulsating, setIndicatorPulsating] = useState(false)
const [points, setPoints] = useState(POINTS)
const refreshData = useCallback(() => {
setPoints(generateRandomGraphData(POINT_COUNT))
hapticFeedback(HapticFeedbackTypes.impactLight)
}, [])
const highestDate = useMemo(
() =>
points.length !== 0 && points[points.length - 1] != null
? points[points.length - 1]!.date
: undefined,
[points]
)
const range: GraphRange | undefined = useMemo(() => {
// if range is disabled, default to infinite range (undefined)
if (!enableRange) return undefined
if (points.length !== 0 && highestDate != null) {
return {
x: {
min: points[0]!.date,
max: new Date(highestDate.getTime() + 50 * 1000 * 60 * 60 * 24),
},
y: {
min: -200,
max: 200,
},
}
} else {
return {
y: {
min: -200,
max: 200,
},
}
}
}, [enableRange, highestDate, points])
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.row}>
<Text style={[styles.title, { color: colors.foreground }]}>
react-native-graph
</Text>
<LineGraph
style={styles.miniGraph}
animated={false}
color={colors.foreground}
points={SMALL_POINTS}
/>
</View>
<View style={styles.spacer} />
<LineGraph
style={styles.graph}
animated={isAnimated}
color={COLOR}
points={points}
gradientFillColors={enableGradient ? GRADIENT_FILL_COLORS : undefined}
enablePanGesture={enablePanGesture}
enableFadeInMask={enableFadeInEffect}
onGestureStart={() => hapticFeedback(HapticFeedbackTypes.impactLight)}
SelectionDot={enableCustomSelectionDot ? SelectionDot : undefined}
range={range}
enableIndicator={enableIndicator}
horizontalPadding={enableIndicator ? 15 : 0}
indicatorPulsating={indicatorPulsating}
/>
<Button title="Refresh" onPress={refreshData} />
<ScrollView
style={styles.controlsScrollView}
contentContainerStyle={styles.controlsScrollViewContent}
>
<Toggle
title="Animated:"
isEnabled={isAnimated}
setIsEnabled={setIsAnimated}
/>
<Toggle
title="Enable Gesture:"
isEnabled={enablePanGesture}
setIsEnabled={setEnablePanGesture}
/>
<Toggle
title="Enable Fade-in effect:"
isEnabled={enableFadeInEffect}
setIsEnabled={setEnableFadeInEffect}
/>
<Toggle
title="Custom Selection Dot:"
isEnabled={enableCustomSelectionDot}
setIsEnabled={setEnableCustomSelectionDot}
/>
<Toggle
title="Enable Gradient:"
isEnabled={enableGradient}
setIsEnabled={setEnableGradient}
/>
<Toggle
title="Enable Range:"
isEnabled={enableRange}
setIsEnabled={setEnableRange}
/>
<Toggle
title="Enable Indicator:"
isEnabled={enableIndicator}
setIsEnabled={setEnableIndicator}
/>
<Toggle
title="Indicator pulsating:"
isEnabled={indicatorPulsating}
setIsEnabled={setIndicatorPulsating}
/>
</ScrollView>
<View style={styles.spacer} />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 42 + 15,
paddingBottom: 42 + 15,
},
spacer: {
flexGrow: 1,
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
fontSize: 30,
fontWeight: '700',
paddingHorizontal: 15,
},
graph: {
alignSelf: 'center',
width: '100%',
aspectRatio: 1.4,
marginVertical: 20,
},
miniGraph: {
width: 40,
height: 35,
marginLeft: 5,
},
controlsScrollView: {
flexGrow: 1,
paddingHorizontal: 15,
},
controlsScrollViewContent: {
justifyContent: 'center',
},
})
+13
View File
@@ -0,0 +1,13 @@
import HapticFeedback, {
HapticFeedbackTypes,
} from 'react-native-haptic-feedback';
export function hapticFeedback(
type: HapticFeedbackTypes = HapticFeedbackTypes.impactLight,
force = false
): void {
HapticFeedback.trigger(type, {
enableVibrateFallback: force,
ignoreAndroidSystemSettings: force,
});
}