Skip to content

Commit

Permalink
feat: Add fill Gradient to graph (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
gtokman committed Sep 8, 2022
1 parent be27193 commit db44237
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 22 deletions.
26 changes: 19 additions & 7 deletions example/src/screens/GraphPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
import { useColors } from '../hooks/useColors'
import { hapticFeedback } from '../utils/HapticFeedback'

const POINTS = 70
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()
Expand All @@ -22,15 +26,15 @@ export function GraphPage() {
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(() => generateRandomGraphData(POINTS))
const smallPoints = useMemo(() => generateSinusGraphData(9), [])
const [points, setPoints] = useState(POINTS)

const refreshData = useCallback(() => {
setPoints(generateRandomGraphData(POINTS))
setPoints(generateRandomGraphData(POINT_COUNT))
hapticFeedback('impactLight')
}, [])

Expand Down Expand Up @@ -76,7 +80,7 @@ export function GraphPage() {
style={styles.miniGraph}
animated={false}
color={colors.foreground}
points={smallPoints}
points={SMALL_POINTS}
/>
</View>

Expand All @@ -85,14 +89,16 @@ export function GraphPage() {
<LineGraph
style={styles.graph}
animated={isAnimated}
color="#6a7ee7"
color={COLOR}
points={points}
gradientFillColors={enableGradient ? GRADIENT_FILL_COLORS : undefined}
enablePanGesture={enablePanGesture}
enableFadeInMask={enableFadeInEffect}
onGestureStart={() => hapticFeedback('impactLight')}
SelectionDot={enableCustomSelectionDot ? SelectionDot : undefined}
range={range}
enableIndicator={enableIndicator}
horizontalPadding={enableIndicator ? 15 : 0}
indicatorPulsating={indicatorPulsating}
/>

Expand All @@ -119,6 +125,11 @@ export function GraphPage() {
isEnabled={enableCustomSelectionDot}
setIsEnabled={setEnableCustomSelectionDot}
/>
<Toggle
title="Enable Gradient:"
isEnabled={enableGradient}
setIsEnabled={setEnableGradient}
/>
<Toggle
title="Enable Range:"
isEnabled={enableRange}
Expand All @@ -144,7 +155,6 @@ export function GraphPage() {
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 15,
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + 15,
paddingBottom: StaticSafeAreaInsets.safeAreaInsetsBottom + 15,
},
Expand All @@ -158,6 +168,7 @@ const styles = StyleSheet.create({
title: {
fontSize: 30,
fontWeight: '700',
paddingHorizontal: 15,
},
graph: {
alignSelf: 'center',
Expand All @@ -173,5 +184,6 @@ const styles = StyleSheet.create({
controls: {
flexGrow: 1,
justifyContent: 'center',
paddingHorizontal: 15,
},
})
81 changes: 71 additions & 10 deletions src/AnimatedLineGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,10 @@ import {
Shadow,
} from '@shopify/react-native-skia'
import type { AnimatedLineGraphProps } from './LineGraphProps'
import {
CIRCLE_RADIUS,
CIRCLE_RADIUS_MULTIPLIER,
SelectionDot as DefaultSelectionDot,
} from './SelectionDot'
import { SelectionDot as DefaultSelectionDot } from './SelectionDot'
import {
createGraphPath,
createGraphPathWithGradient,
getGraphPathRange,
GraphPathRange,
pixelFactorX,
Expand Down Expand Up @@ -59,6 +56,7 @@ const ReanimatedView = Reanimated.View as any
export function AnimatedLineGraph({
points,
color,
gradientFillColors,
lineThickness = 3,
range,
enableFadeInMask,
Expand All @@ -69,8 +67,10 @@ export function AnimatedLineGraph({
SelectionDot = DefaultSelectionDot,
enableIndicator = false,
indicatorPulsating = false,
horizontalPadding = CIRCLE_RADIUS * CIRCLE_RADIUS_MULTIPLIER,
verticalPadding = lineThickness + CIRCLE_RADIUS * CIRCLE_RADIUS_MULTIPLIER,
horizontalPadding = enableIndicator
? INDICATOR_RADIUS * INDICATOR_BORDER_MULTIPLIER
: 0,
verticalPadding = lineThickness,
TopAxisLabel,
BottomAxisLabel,
...props
Expand Down Expand Up @@ -107,7 +107,6 @@ export function AnimatedLineGraph({
],
[pathEnd]
)

const onLayout = useCallback(
({ nativeEvent: { layout } }: LayoutChangeEvent) => {
setWidth(Math.round(layout.width))
Expand All @@ -129,6 +128,7 @@ export function AnimatedLineGraph({
}, [height, width])

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

Expand Down Expand Up @@ -166,6 +166,8 @@ export function AnimatedLineGraph({

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

const shouldFillGradient = gradientFillColors != null

useEffect(() => {
if (height < 1 || width < 1) {
// view is not yet measured!
Expand All @@ -176,17 +178,50 @@ export function AnimatedLineGraph({
return
}

const path = createGraphPath({
let path
let gradientPath

const createGraphPathProps = {
points: points,
range: pathRange,
horizontalPadding: horizontalPadding,
verticalPadding: verticalPadding,
canvasHeight: height,
canvasWidth: width,
})
}

if (shouldFillGradient) {
const { path: pathNew, gradientPath: gradientPathNew } =
createGraphPathWithGradient(createGraphPathProps)

path = pathNew
gradientPath = gradientPathNew
} else {
path = createGraphPath(createGraphPathProps)
}

commands.current = path.toCmds()

if (gradientPath != null) {
const previous = gradientPaths.current
let from: SkPath = previous.to ?? straightLine
if (previous.from != null && interpolateProgress.current < 1)
from =
from.interpolate(previous.from, interpolateProgress.current) ?? from

if (gradientPath.isInterpolatable(from)) {
gradientPaths.current = {
from: from,
to: gradientPath,
}
} else {
gradientPaths.current = {
from: gradientPath,
to: gradientPath,
}
}
}

const previous = paths.current
let from: SkPath = previous.to ?? straightLine
if (previous.from != null && interpolateProgress.current < 1)
Expand Down Expand Up @@ -224,6 +259,8 @@ export function AnimatedLineGraph({
interpolateProgress,
pathRange,
paths,
shouldFillGradient,
gradientPaths,
points,
range,
straightLine,
Expand Down Expand Up @@ -262,6 +299,17 @@ export function AnimatedLineGraph({
[interpolateProgress]
)

const gradientPath = useComputedValue(
() => {
const from = gradientPaths.current.from ?? straightLine
const to = gradientPaths.current.to ?? straightLine

return to.interpolate(from, interpolateProgress.current)
},
// RN Skia deals with deps differently. They are actually the required SkiaValues that the derived value listens to, not react values.
[interpolateProgress]
)

const stopPulsating = useCallback(() => {
cancelAnimation(indicatorPulseAnimation)
indicatorPulseAnimation.value = 0
Expand Down Expand Up @@ -424,6 +472,19 @@ export function AnimatedLineGraph({
positions={positions}
/>
</Path>

{shouldFillGradient && (
<Path
// @ts-ignore
path={gradientPath}
>
<LinearGradient
start={vec(0, 0)}
end={vec(0, height)}
colors={gradientFillColors}
/>
</Path>
)}
</Group>

{SelectionDot != null && (
Expand Down
54 changes: 50 additions & 4 deletions src/CreateGraphPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface GraphPathRange {
y: GraphYRange
}

interface GraphPathConfig {
type GraphPathConfig = {
/**
* Graph Points to use for the Path. Will be normalized and centered.
*/
Expand Down Expand Up @@ -53,6 +53,13 @@ interface GraphPathConfig {
range: GraphPathRange
}

type GraphPathConfigWithGradient = GraphPathConfig & {
shouldFillGradient: true
}
type GraphPathConfigWithoutGradient = GraphPathConfig & {
shouldFillGradient: false
}

export const controlPoint = (
reverse: boolean,
smoothing: number,
Expand Down Expand Up @@ -129,15 +136,25 @@ export const pixelFactorY = (
// A Graph Point will be drawn every second "pixel"
const PIXEL_RATIO = 2

export function createGraphPath({
type GraphPathWithGradient = { path: SkPath; gradientPath: SkPath }

function createGraphPathBase(
props: GraphPathConfigWithGradient
): GraphPathWithGradient
function createGraphPathBase(props: GraphPathConfigWithoutGradient): SkPath

function createGraphPathBase({
points,
smoothing = 0.2,
range,
horizontalPadding,
verticalPadding,
canvasHeight: height,
canvasWidth: width,
}: GraphPathConfig): SkPath {
shouldFillGradient,
}: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient):
| SkPath
| GraphPathWithGradient {
const path = Skia.Path.Make()

const actualWidth = width - 2 * horizontalPadding
Expand Down Expand Up @@ -218,5 +235,34 @@ export function createGraphPath({
}
})

return path
if (!shouldFillGradient) return path

const gradientPath = path.copy()

const lastPointX = pixelFactorX(
points[points.length - 1]!.date,
range.x.min,
range.x.max
)

gradientPath.lineTo(
actualWidth * lastPointX + horizontalPadding,
height + verticalPadding
)
gradientPath.lineTo(0 + horizontalPadding, height + verticalPadding)

return { path: path, gradientPath: gradientPath }
}

export function createGraphPath(props: GraphPathConfig): SkPath {
return createGraphPathBase({ ...props, shouldFillGradient: false })
}

export function createGraphPathWithGradient(
props: GraphPathConfig
): GraphPathWithGradient {
return createGraphPathBase({
...props,
shouldFillGradient: true,
})
}
6 changes: 5 additions & 1 deletion src/LineGraphProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type React from 'react'
import type { ViewProps } from 'react-native'
import type { GraphPathRange } from './CreateGraphPath'
import type { SharedValue } from 'react-native-reanimated'
import type { SkiaMutableValue } from '@shopify/react-native-skia'
import type { Color, SkiaMutableValue } from '@shopify/react-native-skia'

export interface GraphPoint {
value: number
Expand Down Expand Up @@ -33,6 +33,10 @@ interface BaseLineGraphProps extends ViewProps {
* Color of the graph line (path)
*/
color: string
/**
* (Optional) Colors for the fill gradient below the graph line
*/
gradientFillColors?: Color[]
/**
* The width of the graph line (path)
*
Expand Down

0 comments on commit db44237

Please sign in to comment.