Skip to content

Commit db44237

Browse files
author
Gary Tokman
authored
feat: Add fill Gradient to graph (#41)
1 parent be27193 commit db44237

File tree

4 files changed

+145
-22
lines changed

4 files changed

+145
-22
lines changed

example/src/screens/GraphPage.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
import { useColors } from '../hooks/useColors'
1313
import { hapticFeedback } from '../utils/HapticFeedback'
1414

15-
const POINTS = 70
15+
const POINT_COUNT = 70
16+
const POINTS = generateRandomGraphData(POINT_COUNT)
17+
const COLOR = '#6a7ee7'
18+
const GRADIENT_FILL_COLORS = ['#7476df5D', '#7476df4D', '#7476df00']
19+
const SMALL_POINTS = generateSinusGraphData(9)
1620

1721
export function GraphPage() {
1822
const colors = useColors()
@@ -22,15 +26,15 @@ export function GraphPage() {
2226
const [enableFadeInEffect, setEnableFadeInEffect] = useState(false)
2327
const [enableCustomSelectionDot, setEnableCustomSelectionDot] =
2428
useState(false)
29+
const [enableGradient, setEnableGradient] = useState(false)
2530
const [enableRange, setEnableRange] = useState(false)
2631
const [enableIndicator, setEnableIndicator] = useState(false)
2732
const [indicatorPulsating, setIndicatorPulsating] = useState(false)
2833

29-
const [points, setPoints] = useState(() => generateRandomGraphData(POINTS))
30-
const smallPoints = useMemo(() => generateSinusGraphData(9), [])
34+
const [points, setPoints] = useState(POINTS)
3135

3236
const refreshData = useCallback(() => {
33-
setPoints(generateRandomGraphData(POINTS))
37+
setPoints(generateRandomGraphData(POINT_COUNT))
3438
hapticFeedback('impactLight')
3539
}, [])
3640

@@ -76,7 +80,7 @@ export function GraphPage() {
7680
style={styles.miniGraph}
7781
animated={false}
7882
color={colors.foreground}
79-
points={smallPoints}
83+
points={SMALL_POINTS}
8084
/>
8185
</View>
8286

@@ -85,14 +89,16 @@ export function GraphPage() {
8589
<LineGraph
8690
style={styles.graph}
8791
animated={isAnimated}
88-
color="#6a7ee7"
92+
color={COLOR}
8993
points={points}
94+
gradientFillColors={enableGradient ? GRADIENT_FILL_COLORS : undefined}
9095
enablePanGesture={enablePanGesture}
9196
enableFadeInMask={enableFadeInEffect}
9297
onGestureStart={() => hapticFeedback('impactLight')}
9398
SelectionDot={enableCustomSelectionDot ? SelectionDot : undefined}
9499
range={range}
95100
enableIndicator={enableIndicator}
101+
horizontalPadding={enableIndicator ? 15 : 0}
96102
indicatorPulsating={indicatorPulsating}
97103
/>
98104

@@ -119,6 +125,11 @@ export function GraphPage() {
119125
isEnabled={enableCustomSelectionDot}
120126
setIsEnabled={setEnableCustomSelectionDot}
121127
/>
128+
<Toggle
129+
title="Enable Gradient:"
130+
isEnabled={enableGradient}
131+
setIsEnabled={setEnableGradient}
132+
/>
122133
<Toggle
123134
title="Enable Range:"
124135
isEnabled={enableRange}
@@ -144,7 +155,6 @@ export function GraphPage() {
144155
const styles = StyleSheet.create({
145156
container: {
146157
flex: 1,
147-
paddingHorizontal: 15,
148158
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + 15,
149159
paddingBottom: StaticSafeAreaInsets.safeAreaInsetsBottom + 15,
150160
},
@@ -158,6 +168,7 @@ const styles = StyleSheet.create({
158168
title: {
159169
fontSize: 30,
160170
fontWeight: '700',
171+
paddingHorizontal: 15,
161172
},
162173
graph: {
163174
alignSelf: 'center',
@@ -173,5 +184,6 @@ const styles = StyleSheet.create({
173184
controls: {
174185
flexGrow: 1,
175186
justifyContent: 'center',
187+
paddingHorizontal: 15,
176188
},
177189
})

src/AnimatedLineGraph.tsx

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,10 @@ import {
1818
Shadow,
1919
} from '@shopify/react-native-skia'
2020
import type { AnimatedLineGraphProps } from './LineGraphProps'
21-
import {
22-
CIRCLE_RADIUS,
23-
CIRCLE_RADIUS_MULTIPLIER,
24-
SelectionDot as DefaultSelectionDot,
25-
} from './SelectionDot'
21+
import { SelectionDot as DefaultSelectionDot } from './SelectionDot'
2622
import {
2723
createGraphPath,
24+
createGraphPathWithGradient,
2825
getGraphPathRange,
2926
GraphPathRange,
3027
pixelFactorX,
@@ -59,6 +56,7 @@ const ReanimatedView = Reanimated.View as any
5956
export function AnimatedLineGraph({
6057
points,
6158
color,
59+
gradientFillColors,
6260
lineThickness = 3,
6361
range,
6462
enableFadeInMask,
@@ -69,8 +67,10 @@ export function AnimatedLineGraph({
6967
SelectionDot = DefaultSelectionDot,
7068
enableIndicator = false,
7169
indicatorPulsating = false,
72-
horizontalPadding = CIRCLE_RADIUS * CIRCLE_RADIUS_MULTIPLIER,
73-
verticalPadding = lineThickness + CIRCLE_RADIUS * CIRCLE_RADIUS_MULTIPLIER,
70+
horizontalPadding = enableIndicator
71+
? INDICATOR_RADIUS * INDICATOR_BORDER_MULTIPLIER
72+
: 0,
73+
verticalPadding = lineThickness,
7474
TopAxisLabel,
7575
BottomAxisLabel,
7676
...props
@@ -107,7 +107,6 @@ export function AnimatedLineGraph({
107107
],
108108
[pathEnd]
109109
)
110-
111110
const onLayout = useCallback(
112111
({ nativeEvent: { layout } }: LayoutChangeEvent) => {
113112
setWidth(Math.round(layout.width))
@@ -129,6 +128,7 @@ export function AnimatedLineGraph({
129128
}, [height, width])
130129

131130
const paths = useValue<{ from?: SkPath; to?: SkPath }>({})
131+
const gradientPaths = useValue<{ from?: SkPath; to?: SkPath }>({})
132132
const commands = useRef<PathCommand[]>([])
133133
const [commandsChanged, setCommandsChanged] = useState(0)
134134

@@ -166,6 +166,8 @@ export function AnimatedLineGraph({
166166

167167
const indicatorPulseColor = useMemo(() => hexToRgba(color, 0.4), [color])
168168

169+
const shouldFillGradient = gradientFillColors != null
170+
169171
useEffect(() => {
170172
if (height < 1 || width < 1) {
171173
// view is not yet measured!
@@ -176,17 +178,50 @@ export function AnimatedLineGraph({
176178
return
177179
}
178180

179-
const path = createGraphPath({
181+
let path
182+
let gradientPath
183+
184+
const createGraphPathProps = {
180185
points: points,
181186
range: pathRange,
182187
horizontalPadding: horizontalPadding,
183188
verticalPadding: verticalPadding,
184189
canvasHeight: height,
185190
canvasWidth: width,
186-
})
191+
}
192+
193+
if (shouldFillGradient) {
194+
const { path: pathNew, gradientPath: gradientPathNew } =
195+
createGraphPathWithGradient(createGraphPathProps)
196+
197+
path = pathNew
198+
gradientPath = gradientPathNew
199+
} else {
200+
path = createGraphPath(createGraphPathProps)
201+
}
187202

188203
commands.current = path.toCmds()
189204

205+
if (gradientPath != null) {
206+
const previous = gradientPaths.current
207+
let from: SkPath = previous.to ?? straightLine
208+
if (previous.from != null && interpolateProgress.current < 1)
209+
from =
210+
from.interpolate(previous.from, interpolateProgress.current) ?? from
211+
212+
if (gradientPath.isInterpolatable(from)) {
213+
gradientPaths.current = {
214+
from: from,
215+
to: gradientPath,
216+
}
217+
} else {
218+
gradientPaths.current = {
219+
from: gradientPath,
220+
to: gradientPath,
221+
}
222+
}
223+
}
224+
190225
const previous = paths.current
191226
let from: SkPath = previous.to ?? straightLine
192227
if (previous.from != null && interpolateProgress.current < 1)
@@ -224,6 +259,8 @@ export function AnimatedLineGraph({
224259
interpolateProgress,
225260
pathRange,
226261
paths,
262+
shouldFillGradient,
263+
gradientPaths,
227264
points,
228265
range,
229266
straightLine,
@@ -262,6 +299,17 @@ export function AnimatedLineGraph({
262299
[interpolateProgress]
263300
)
264301

302+
const gradientPath = useComputedValue(
303+
() => {
304+
const from = gradientPaths.current.from ?? straightLine
305+
const to = gradientPaths.current.to ?? straightLine
306+
307+
return to.interpolate(from, interpolateProgress.current)
308+
},
309+
// RN Skia deals with deps differently. They are actually the required SkiaValues that the derived value listens to, not react values.
310+
[interpolateProgress]
311+
)
312+
265313
const stopPulsating = useCallback(() => {
266314
cancelAnimation(indicatorPulseAnimation)
267315
indicatorPulseAnimation.value = 0
@@ -424,6 +472,19 @@ export function AnimatedLineGraph({
424472
positions={positions}
425473
/>
426474
</Path>
475+
476+
{shouldFillGradient && (
477+
<Path
478+
// @ts-ignore
479+
path={gradientPath}
480+
>
481+
<LinearGradient
482+
start={vec(0, 0)}
483+
end={vec(0, height)}
484+
colors={gradientFillColors}
485+
/>
486+
</Path>
487+
)}
427488
</Group>
428489

429490
{SelectionDot != null && (

src/CreateGraphPath.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface GraphPathRange {
2222
y: GraphYRange
2323
}
2424

25-
interface GraphPathConfig {
25+
type GraphPathConfig = {
2626
/**
2727
* Graph Points to use for the Path. Will be normalized and centered.
2828
*/
@@ -53,6 +53,13 @@ interface GraphPathConfig {
5353
range: GraphPathRange
5454
}
5555

56+
type GraphPathConfigWithGradient = GraphPathConfig & {
57+
shouldFillGradient: true
58+
}
59+
type GraphPathConfigWithoutGradient = GraphPathConfig & {
60+
shouldFillGradient: false
61+
}
62+
5663
export const controlPoint = (
5764
reverse: boolean,
5865
smoothing: number,
@@ -129,15 +136,25 @@ export const pixelFactorY = (
129136
// A Graph Point will be drawn every second "pixel"
130137
const PIXEL_RATIO = 2
131138

132-
export function createGraphPath({
139+
type GraphPathWithGradient = { path: SkPath; gradientPath: SkPath }
140+
141+
function createGraphPathBase(
142+
props: GraphPathConfigWithGradient
143+
): GraphPathWithGradient
144+
function createGraphPathBase(props: GraphPathConfigWithoutGradient): SkPath
145+
146+
function createGraphPathBase({
133147
points,
134148
smoothing = 0.2,
135149
range,
136150
horizontalPadding,
137151
verticalPadding,
138152
canvasHeight: height,
139153
canvasWidth: width,
140-
}: GraphPathConfig): SkPath {
154+
shouldFillGradient,
155+
}: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient):
156+
| SkPath
157+
| GraphPathWithGradient {
141158
const path = Skia.Path.Make()
142159

143160
const actualWidth = width - 2 * horizontalPadding
@@ -218,5 +235,34 @@ export function createGraphPath({
218235
}
219236
})
220237

221-
return path
238+
if (!shouldFillGradient) return path
239+
240+
const gradientPath = path.copy()
241+
242+
const lastPointX = pixelFactorX(
243+
points[points.length - 1]!.date,
244+
range.x.min,
245+
range.x.max
246+
)
247+
248+
gradientPath.lineTo(
249+
actualWidth * lastPointX + horizontalPadding,
250+
height + verticalPadding
251+
)
252+
gradientPath.lineTo(0 + horizontalPadding, height + verticalPadding)
253+
254+
return { path: path, gradientPath: gradientPath }
255+
}
256+
257+
export function createGraphPath(props: GraphPathConfig): SkPath {
258+
return createGraphPathBase({ ...props, shouldFillGradient: false })
259+
}
260+
261+
export function createGraphPathWithGradient(
262+
props: GraphPathConfig
263+
): GraphPathWithGradient {
264+
return createGraphPathBase({
265+
...props,
266+
shouldFillGradient: true,
267+
})
222268
}

src/LineGraphProps.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type React from 'react'
22
import type { ViewProps } from 'react-native'
33
import type { GraphPathRange } from './CreateGraphPath'
44
import type { SharedValue } from 'react-native-reanimated'
5-
import type { SkiaMutableValue } from '@shopify/react-native-skia'
5+
import type { Color, SkiaMutableValue } from '@shopify/react-native-skia'
66

77
export interface GraphPoint {
88
value: number
@@ -33,6 +33,10 @@ interface BaseLineGraphProps extends ViewProps {
3333
* Color of the graph line (path)
3434
*/
3535
color: string
36+
/**
37+
* (Optional) Colors for the fill gradient below the graph line
38+
*/
39+
gradientFillColors?: Color[]
3640
/**
3741
* The width of the graph line (path)
3842
*

0 commit comments

Comments
 (0)