Skip to content

Commit

Permalink
feat: implement indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
chrispader committed Jun 13, 2022
1 parent eab9646 commit 7ea733e
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 29 deletions.
144 changes: 115 additions & 29 deletions src/AnimatedLineGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import {
Group,
Shadow,
PathCommand,
useValueEffect,
} from '@shopify/react-native-skia'
import type { AnimatedLineGraphProps } from './LineGraphProps'
import {
createGraphPath,
getGraphPathRange,
GraphPathRange,
pixelFactorX,
} from './CreateGraphPath'
import Reanimated, {
runOnJS,
Expand All @@ -32,6 +34,8 @@ import { getYForX } from './GetYForX'

const CIRCLE_RADIUS = 5
const CIRCLE_RADIUS_MULTIPLIER = 6
const INDICATOR_RADIUS = 7
const INDICATOR_BORDER_MULTIPLIER = 1.3

// weird rea type bug
const ReanimatedView = Reanimated.View as any
Expand All @@ -46,6 +50,7 @@ export function AnimatedLineGraph({
onPointSelected,
onGestureStart,
onGestureEnd,
alwaysShowIndicator = false,
horizontalPadding = CIRCLE_RADIUS * CIRCLE_RADIUS_MULTIPLIER,
verticalPadding = lineThickness + CIRCLE_RADIUS * CIRCLE_RADIUS_MULTIPLIER,
TopAxisLabel,
Expand All @@ -56,7 +61,33 @@ export function AnimatedLineGraph({
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const interpolateProgress = useValue(0)
const graphPadding = lineThickness
const [indicatorVisible, setIndicatorVisible] = useState(false)

const { gesture, isActive, x } = useHoldOrPanGesture({ holdDuration: 300 })
const circleX = useValue(0)
const circleY = useValue(0)
const pathEnd = useValue(0)
const circleRadius = useValue(0)
const circleStrokeRadius = useDerivedValue(
() => circleRadius.current * 6,
[circleRadius]
)
const indicatorRadius = useValue(alwaysShowIndicator ? INDICATOR_RADIUS : 0)
const indicatorBorderRadius = useDerivedValue(
() => indicatorRadius.current * INDICATOR_BORDER_MULTIPLIER,
[indicatorRadius]
)

const positions = useDerivedValue(
() => [
0,
Math.min(0.15, pathEnd.current),
pathEnd.current,
pathEnd.current,
1,
],
[pathEnd]
)

const onLayout = useCallback(
({ nativeEvent: { layout } }: LayoutChangeEvent) => {
Expand Down Expand Up @@ -86,6 +117,28 @@ export function AnimatedLineGraph({
[points, range]
)

const drawingWidth = useMemo(() => {
const lastPoint = points[points.length - 1]!

return (
(width - 2 * horizontalPadding) *
pixelFactorX(lastPoint.date, pathRange.x.min, pathRange.x.max)
)
}, [horizontalPadding, pathRange.x.max, pathRange.x.min, points, width])

const indicatorX = useMemo(
() =>
indicatorVisible
? Math.floor(drawingWidth) + horizontalPadding
: undefined,
[drawingWidth, horizontalPadding, indicatorVisible]
)
const indicatorY = useMemo(
() =>
indicatorX != null ? getYForX(commands.current, indicatorX) : undefined,
[indicatorX]
)

useEffect(() => {
if (height < 1 || width < 1) {
// view is not yet measured!
Expand Down Expand Up @@ -134,7 +187,6 @@ export function AnimatedLineGraph({
}
)
}, [
graphPadding,
height,
horizontalPadding,
interpolateProgress,
Expand Down Expand Up @@ -179,33 +231,46 @@ export function AnimatedLineGraph({
[interpolateProgress]
)

const { gesture, isActive, x } = useHoldOrPanGesture({ holdDuration: 300 })
const circleX = useValue(0)
const circleY = useValue(0)
const pathEnd = useValue(0)
const circleRadius = useValue(0)
const circleStrokeRadius = useDerivedValue(
() => circleRadius.current * 6,
[circleRadius]
)

const setFingerX = useCallback(
(fingerX: number) => {
const y = getYForX(commands.current, fingerX)
const fingerXInRange = Math.min(
Math.max(fingerX, horizontalPadding + 1),
drawingWidth + horizontalPadding - 1
)
const y = getYForX(commands.current, fingerXInRange)

if (y != null) {
circleY.current = y
circleX.current = fingerX
circleX.current = fingerXInRange
}
pathEnd.current = fingerX / width

const index = Math.round((fingerX / width) * points.length)
if (
fingerX > horizontalPadding &&
fingerX < drawingWidth + horizontalPadding
)
pathEnd.current = fingerX / width

const actualFingerX = fingerX - 2 * horizontalPadding + horizontalPadding

const index = Math.round(
(actualFingerX / (drawingWidth + horizontalPadding)) * points.length
)
const pointIndex = Math.min(Math.max(index, 0), points.length - 1)
const dataPoint = points[Math.round(pointIndex)]
const dataPoint = points[pointIndex]
if (dataPoint != null) onPointSelected?.(dataPoint)
},
[circleX, circleY, onPointSelected, pathEnd, points, width]
[
circleX,
circleY,
drawingWidth,
horizontalPadding,
onPointSelected,
pathEnd,
points,
width,
]
)

const setIsActive = useCallback(
(active: boolean) => {
runSpring(circleRadius, active ? 5 : 0, {
Expand All @@ -214,12 +279,20 @@ export function AnimatedLineGraph({
damping: 50,
velocity: 0,
})

runSpring(indicatorRadius, !active ? INDICATOR_RADIUS : 0, {
mass: 1,
stiffness: 1000,
damping: 50,
velocity: 0,
})

if (!active) pathEnd.current = 1

if (active) onGestureStart?.()
else onGestureEnd?.()
},
[circleRadius, onGestureEnd, onGestureStart, pathEnd]
[circleRadius, indicatorRadius, onGestureEnd, onGestureStart, pathEnd]
)
useAnimatedReaction(
() => x.value,
Expand All @@ -237,16 +310,10 @@ export function AnimatedLineGraph({
},
[isActive, setIsActive]
)
const positions = useDerivedValue(
() => [
0,
Math.min(0.15, pathEnd.current),
pathEnd.current,
pathEnd.current,
1,
],
[pathEnd]
)

useValueEffect(paths, ({ from }) => {
runOnJS(setIndicatorVisible)(from != null)
})

return (
<View {...props}>
Expand Down Expand Up @@ -298,6 +365,25 @@ export function AnimatedLineGraph({
</Circle>
</Group>
)}

{alwaysShowIndicator && indicatorVisible && (
<Group>
<Circle
cx={indicatorX}
cy={indicatorY}
r={indicatorBorderRadius}
color={'#ffffff'}
>
<Shadow dx={2} dy={2} color="rgba(0,0,0,0.2)" blur={4} />
</Circle>
<Circle
cx={indicatorX}
cy={indicatorY}
r={indicatorRadius}
color={color}
/>
</Group>
)}
</Canvas>
</View>

Expand Down
4 changes: 4 additions & 0 deletions src/LineGraphProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export type AnimatedLineGraphProps = BaseLineGraphProps & {
* Vertical padding applied to graph, so the selection dot doesn't get cut off vertically
*/
verticalPadding?: number
/**
* Enables an indicator which is displayed at the end of the graph
*/
alwaysShowIndicator?: boolean

/**
* Called for each point while the user is scrubbing/panning through the graph
Expand Down

0 comments on commit 7ea733e

Please sign in to comment.