Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@


## [0.3.0](https://github.com/codeherence/react-native-graph/compare/v0.2.0...v0.3.0) (2024-08-08)


### Features

* introduce multiline chart ([ac12f01](https://github.com/codeherence/react-native-graph/commit/ac12f0129799b55955b9ec0b25ad605eb35397c8))

## [0.2.0](https://github.com/codeherence/react-native-graph/compare/v0.1.1-rc.2...v0.2.0) (2024-05-12)


Expand Down
1 change: 1 addition & 0 deletions example/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default () => (
<Stack>
<Stack.Screen name="index" />
<Stack.Screen name="chart" />
<Stack.Screen name="multi_line_chart" />
</Stack>
</GestureHandlerRootView>
);
Expand Down
3 changes: 3 additions & 0 deletions example/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export default () => {
<Pressable onPress={() => navigate("/static_chart")}>
<Text style={styles.link}>Go to static chart</Text>
</Pressable>
<Pressable onPress={() => navigate("/multi_line_chart")}>
<Text style={styles.link}>Go to multi line chart</Text>
</Pressable>
</ScrollView>
);
};
Expand Down
99 changes: 99 additions & 0 deletions example/app/multi_line_chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Line, MultiLineChart } from "@codeherence/react-native-graph";
import { Circle, Group } from "@shopify/react-native-skia";
import * as Haptics from "expo-haptics";
import { ScrollView, StyleSheet } from "react-native";
import { runOnJS, useDerivedValue, useSharedValue, withTiming } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { priceMap } from "../src/store/prices";

const gestureStartImpact = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
};

export default () => {
const cursorShown = useSharedValue(false);
const x = useSharedValue(0);
const y = useSharedValue(0);
const { bottom, left, right } = useSafeAreaInsets();

const opacity = useDerivedValue(() => {
// return cursorShown.value ? 1 : 0;
// Use a timing function to animate the opacity
return withTiming(cursorShown.value ? 1 : 0, { duration: 200 });
});

return (
<ScrollView
style={styles.container}
contentContainerStyle={[
styles.contentContainer,
{
paddingBottom: bottom,
paddingLeft: left,
paddingRight: right,
},
]}
showsVerticalScrollIndicator={false}
>
<MultiLineChart
isStatic={false}
points={priceMap}
style={styles.chart}
// ExtraCanvasElements={
// <>
// <Group color="blue" opacity={opacity}>
// <Circle cx={x} cy={y} r={10} />
// </Group>
// </>
// }
onPanGestureBegin={(payload) => {
"worklet";
cursorShown.value = true;
x.value = payload.event.x;
y.value = payload.event.y;
runOnJS(gestureStartImpact)();
}}
onPanGestureEnd={() => {
"worklet";
cursorShown.value = false;
}}
onPanGestureChange={(payload) => {
"worklet";
x.value = payload.event.x;
y.value = payload.event.y;
}}
>
{(args) => (
<>
<Line points={args.points.aapl} strokeWidth={1} color="green" />
<Line points={args.points.msft} strokeWidth={1} color="purple" />
<Line points={args.points.nvda} strokeWidth={1} color="black" />
<Line points={args.points.unity} strokeWidth={1} color="orange" />
</>
)}
</MultiLineChart>
</ScrollView>
);
};

const styles = StyleSheet.create({
container: { flex: 1 },
contentContainer: { flexGrow: 1 },
chart: { flex: 1, maxHeight: 200 },
price: { fontSize: 32 },
buttonContainer: {
flexDirection: "row",
justifyContent: "center",
paddingVertical: 12,
},
toggleBtn: {
padding: 12,
backgroundColor: "blue",
borderRadius: 12,
},
buttonText: {
color: "white",
fontSize: 16,
},
});
Binary file removed example/assets/circle-pic.png
Binary file not shown.
Binary file removed example/assets/planets.jpeg
Binary file not shown.
6 changes: 0 additions & 6 deletions example/assets/twitter-verified.svg

This file was deleted.

6 changes: 6 additions & 0 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ PODS:
- ExpoModulesCore
- ExpoFileSystem (16.0.7):
- ExpoModulesCore
- ExpoHaptics (12.8.1):
- ExpoModulesCore
- ExpoHead (3.4.8):
- ExpoModulesCore
- ExpoImage (1.10.6):
Expand Down Expand Up @@ -1231,6 +1233,7 @@ DEPENDENCIES:
- ExpoBlur (from `../node_modules/expo-blur/ios`)
- ExpoCrypto (from `../node_modules/expo-crypto/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoHaptics (from `../node_modules/expo-haptics/ios`)
- ExpoHead (from `../node_modules/expo-router/ios`)
- ExpoImage (from `../node_modules/expo-image/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
Expand Down Expand Up @@ -1336,6 +1339,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-crypto/ios"
ExpoFileSystem:
:path: "../node_modules/expo-file-system/ios"
ExpoHaptics:
:path: "../node_modules/expo-haptics/ios"
ExpoHead:
:path: "../node_modules/expo-router/ios"
ExpoImage:
Expand Down Expand Up @@ -1469,6 +1474,7 @@ SPEC CHECKSUMS:
ExpoBlur: e832d874bd94afc0645daddbd3162ec1ce172080
ExpoCrypto: b6428f48599c007676dc81a9b5f72c07e62fdccc
ExpoFileSystem: c7488590959bf85ebc114909eb8186cbd62e3a25
ExpoHaptics: 28a771b630353cd6e8dcf1b1e3e693e38ad7c3c3
ExpoHead: 8224345e80abcf4c97b31c99805dd5a3c8d3404d
ExpoImage: 8cf2d51de3d03b7e984e9b0ba8f19c0c22057001
ExpoKeepAwake: 0f5cad99603a3268e50af9a6eb8b76d0d9ac956c
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"expo-constants": "~15.4.5",
"expo-crypto": "~12.8.1",
"expo-dev-client": "~3.3.9",
"expo-haptics": "~12.8.1",
"expo-image": "~1.10.6",
"expo-linking": "~6.2.2",
"expo-router": "~3.4.8",
Expand Down
5 changes: 5 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5189,6 +5189,11 @@ expo-font@~11.10.3:
dependencies:
fontfaceobserver "^2.1.0"

expo-haptics@~12.8.1:
version "12.8.1"
resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-12.8.1.tgz#42b996763be33d661bd33bbc3b3958c3f2734b9d"
integrity sha512-ntLsHkfle8K8w9MW8pZEw92ZN3sguaGUSSIxv30fPKNeQFu7Cq/h47Qv3tONv2MO3wU48N9FbKnant6XlfptpA==

expo-image@~1.10.6:
version "1.10.6"
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.10.6.tgz#b0e54d31d97742505296c076a5f18d094ba9a8cc"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codeherence/react-native-graph",
"version": "0.2.0",
"version": "0.3.0",
"description": "A graphing library for React Native built with Shopify's react-native-skia.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down
9 changes: 2 additions & 7 deletions src/components/LineChart/InteractiveLineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { Cursor } from "./Cursor";
import { LineChartProps } from "./LineChart";
import { computePath, computeGraphData } from "./computations";
import { useGestures } from "./useGestures";
import { batchedUpdates } from "../../libs/batchedUpdates";

export const InteractiveLineChart: React.FC<LineChartProps<false>> = ({
points = [],
Expand All @@ -33,8 +32,7 @@ export const InteractiveLineChart: React.FC<LineChartProps<false>> = ({
onHoverGestureEnd = null,
...viewProps
}) => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const [{ width, height }, setSize] = useState({ width: 0, height: 0 });

// Initially -cursorRadius so that the cursor is hidden
const x = useSharedValue(-cursorRadius);
Expand Down Expand Up @@ -67,10 +65,7 @@ export const InteractiveLineChart: React.FC<LineChartProps<false>> = ({

const onLayout = useCallback((e: LayoutChangeEvent) => {
// Batch the updates to avoid unnecessary re-renders
batchedUpdates(() => {
setWidth(e.nativeEvent.layout.width);
setHeight(e.nativeEvent.layout.height);
});
setSize(e.nativeEvent.layout);
}, []);

return (
Expand Down
101 changes: 101 additions & 0 deletions src/components/MultiLineChart/InteractiveMultiLineChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Canvas } from "@shopify/react-native-skia";
import React, { useCallback, useState } from "react";
import { LayoutChangeEvent, StyleSheet, View } from "react-native";
import { GestureDetector } from "react-native-gesture-handler";

import type { MultiLineChartProps } from "./MultiLineChart";
import { useMultiLineChartContext } from "./context";
import { UseGestureProps, useGestures } from "./useGestures";
import { batchedUpdates } from "../../libs/batchedUpdates";

export interface InteractiveLineChartProps<Data extends Record<string, [number, number][]>> {
gestureLongPressDelay?: number;
/**
* Extra elements to render on the canvas. This prop is separated from the children prop to allow
* for clear separation between line chart elements and extra elements.
*/
ExtraCanvasElements?: JSX.Element;
onPanGestureBegin?: UseGestureProps<Data>["onPanGestureBegin"];
onPanGestureChange?: UseGestureProps<Data>["onPanGestureChange"];
onPanGestureEnd?: UseGestureProps<Data>["onPanGestureEnd"];
}

export const InteractiveMultiLineChart = <Data extends Record<string, [number, number][]>>({
points,
children,
gestureLongPressDelay = 200,
ExtraCanvasElements,
onCanvasResize,
onPanGestureBegin,
onPanGestureChange,
onPanGestureEnd,
...viewProps
}: MultiLineChartProps<Data, false>) => {
const [layoutComputed, setLayoutComputed] = useState(false);
const { height, width, setCanvasSize } = useMultiLineChartContext();

const gestures = useGestures({
points,
height,
precision: 2,
gestureLongPressDelay,
onPanGestureBegin,
onPanGestureChange,
onPanGestureEnd,
});

const onLayout = useCallback((e: LayoutChangeEvent) => {
// Batch the updates to avoid unnecessary re-renders
batchedUpdates(() => {
setLayoutComputed(true);
onCanvasResize?.(e.nativeEvent.layout.width, e.nativeEvent.layout.height);
setCanvasSize(e.nativeEvent.layout);
});
}, []);

return (
<View style={[styles.root, viewProps.style]} {...viewProps}>
<GestureDetector gesture={gestures}>
<View style={styles.container} onLayout={onLayout}>
<Canvas style={{ height, width }}>
{!layoutComputed
? null
: // Since the children need to be invoked, we invoke the children then inject the width and height manually.
(() => {
const invokedChildren = children({ height, width, points });
if (!React.isValidElement(invokedChildren)) return null;

if (invokedChildren.type === React.Fragment) {
// If the child is a fragment, iteratively clone all children
return React.Children.map(invokedChildren.props.children, (c) => {
if (!React.isValidElement(c)) return null;
return React.cloneElement(c, {
// @ts-ignore
...c.props,
width,
height,
});
});
}

return React.cloneElement(invokedChildren, {
...invokedChildren.props,
width,
height,
});
})()}

{ExtraCanvasElements}
</Canvas>
</View>
</GestureDetector>
</View>
);
};
InteractiveMultiLineChart.displayName = "StaticMultiLineChart";

const styles = StyleSheet.create({
root: { position: "relative", overflow: "hidden" },
container: { flex: 1 },
canvas: { flex: 1 },
});
38 changes: 38 additions & 0 deletions src/components/MultiLineChart/Line.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Path, PathProps } from "@shopify/react-native-skia";
import { useMemo } from "react";

import { computeGraphData, computePath, ComputePathProps } from "../../utils/math";

interface LineProps extends Pick<PathProps, "children" | "color" | "strokeWidth" | "stroke"> {
/** An array of tuples representing [time, value] pairs. */
points?: [number, number][];
/**
* The width of the canvas. This value is not modifiable and will be injected by the parent component.
*/
width?: number;
/**
* The height of the canvas. This value is not modifiable and will be injected by the parent component.
*/
height?: number;
strokeWidth?: number;
curveType?: ComputePathProps["curveType"];
}

export const Line: React.FC<LineProps> = ({
points = [],
width = 0,
height = 0,
strokeWidth = 2,
curveType = "linear",
...pathProps
}) => {
const data = useMemo(() => {
return computeGraphData(points);
}, [points]);

const path = useMemo(() => {
return computePath({ ...data, width, height, cursorRadius: 0, curveType });
}, [data, width, height, curveType]);

return <Path style="stroke" strokeWidth={strokeWidth} color="gray" {...pathProps} path={path} />;
};
Loading