Skip to content

Commit ad33fa6

Browse files
committed
fix: add single point handling
Key Highlights: - If a single point is passed to the line chart, it will now render a straight line instead of nothing.
1 parent dc328bd commit ad33fa6

File tree

3 files changed

+113
-1
lines changed

3 files changed

+113
-1
lines changed

example/app/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export default () => {
1919
<Pressable onPress={() => navigate("/multi_line_chart")}>
2020
<Text style={styles.link}>Go to multi line chart</Text>
2121
</Pressable>
22+
<Pressable onPress={() => navigate("/single_point_multi_line_chart")}>
23+
<Text style={styles.link}>Go to single point multi line chart</Text>
24+
</Pressable>
2225
</ScrollView>
2326
);
2427
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Line, MultiLineChart } from "@codeherence/react-native-graph";
2+
import { Circle, Group } from "@shopify/react-native-skia";
3+
import * as Haptics from "expo-haptics";
4+
import { ScrollView, StyleSheet } from "react-native";
5+
import { runOnJS, useDerivedValue, useSharedValue, withTiming } from "react-native-reanimated";
6+
import { useSafeAreaInsets } from "react-native-safe-area-context";
7+
8+
const gestureStartImpact = () => {
9+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
10+
};
11+
12+
export default () => {
13+
const { bottom, left, right } = useSafeAreaInsets();
14+
15+
const cursorShown = useSharedValue(false);
16+
const opacity = useDerivedValue(() => {
17+
return withTiming(cursorShown.value ? 1 : 0, { duration: 200 });
18+
});
19+
20+
const msftX = useSharedValue(0);
21+
const msftY = useSharedValue(0);
22+
23+
return (
24+
<ScrollView
25+
style={styles.container}
26+
contentContainerStyle={[
27+
styles.contentContainer,
28+
{
29+
paddingBottom: bottom,
30+
paddingLeft: left,
31+
paddingRight: right,
32+
},
33+
]}
34+
showsVerticalScrollIndicator={false}
35+
>
36+
<MultiLineChart
37+
isStatic={false}
38+
points={{ msft: [[1000, 0]] }}
39+
style={styles.chart}
40+
ExtraCanvasElements={
41+
<>
42+
<Group opacity={opacity}>
43+
<Circle cx={msftX} cy={msftY} r={4} color="purple" />
44+
</Group>
45+
</>
46+
}
47+
onPanGestureBegin={(payload) => {
48+
"worklet";
49+
cursorShown.value = true;
50+
msftX.value = payload.points.msft.x;
51+
msftY.value = payload.points.msft.y;
52+
runOnJS(gestureStartImpact)();
53+
}}
54+
onPanGestureEnd={() => {
55+
"worklet";
56+
cursorShown.value = false;
57+
}}
58+
onPanGestureChange={(payload) => {
59+
"worklet";
60+
msftX.value = payload.points.msft.x;
61+
msftY.value = payload.points.msft.y;
62+
console.log({ payload: payload.points.msft });
63+
}}
64+
>
65+
{(args) => (
66+
<>
67+
<Line points={args.points.msft} strokeWidth={1} color="purple" />
68+
</>
69+
)}
70+
</MultiLineChart>
71+
</ScrollView>
72+
);
73+
};
74+
75+
const styles = StyleSheet.create({
76+
container: { flex: 1 },
77+
contentContainer: { flexGrow: 1 },
78+
chart: { flex: 1, maxHeight: 200 },
79+
price: { fontSize: 32 },
80+
buttonContainer: {
81+
flexDirection: "row",
82+
justifyContent: "center",
83+
paddingVertical: 12,
84+
},
85+
toggleBtn: {
86+
padding: 12,
87+
backgroundColor: "blue",
88+
borderRadius: 12,
89+
},
90+
buttonText: {
91+
color: "white",
92+
fontSize: 16,
93+
},
94+
});

src/utils/math.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,24 @@ export const round = (value: number, precision: number = 15): number => {
2929
const linearYForX = (path: SkPath, x: number): [number, number, number] => {
3030
"worklet";
3131
const cmds = path.toCmds();
32+
// If there are only 2 commands, the path is a straight line
33+
if (cmds.length <= 2) {
34+
const cmd = cmds[0];
35+
if (cmd === undefined) return [0, 0, 0];
36+
if (cmd[0] !== PathVerb.Move) return [0, 0, 0];
37+
const x1 = cmd[1];
38+
const y1 = cmd[2];
39+
if (x1 === undefined || y1 === undefined) return [0, 0, 0];
40+
return [x1, y1, 0];
41+
}
42+
43+
// TODO: Optimize this using binary search. Note that the segments on the line chart may not
44+
// be equidistant, so a naive binary search may not work that well.
45+
3246
let from: Vector = vec(0, 0);
3347
let dataIndex = 0;
3448

49+
// Find the closest x value on the path and return the x, y, and index values
3550
for (let i = 0; i < cmds.length; ++i) {
3651
const cmd = cmds[i];
3752
if (cmd == null) break;
@@ -101,7 +116,7 @@ export const computePath = ({
101116
const straightLine = Skia.Path.Make()
102117
.moveTo(0, height / 2)
103118
.lineTo(width, height / 2);
104-
if (points.length === 0) return straightLine; // No data, return a straight line
119+
if (points.length <= 1) return straightLine; // No data, return a straight line
105120

106121
const scaleX = scaleTime().domain([minTimestamp, maxTimestamp]).range([0, width]);
107122
const scaleY = scaleSqrt()

0 commit comments

Comments
 (0)