chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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),
|
||||
}));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user