Skip to content

Commit

Permalink
feat(voronoi): attempt to simulate enter/leave events
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed May 3, 2024
1 parent ed28d78 commit 10ae9e3
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 68 deletions.
132 changes: 68 additions & 64 deletions packages/voronoi/src/Mesh.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useState, useCallback, useMemo, MouseEvent, TouchEvent } from 'react'
import { useRef, useState, useCallback, useMemo, useEffect, MouseEvent, TouchEvent } from 'react'
import { getRelativeCursor, getDistance } from '@nivo/core'
import { useVoronoiMesh } from './hooks'
import { XYAccessor } from './computeMesh'
Expand Down Expand Up @@ -41,8 +41,18 @@ export const Mesh = <Datum,>({
detectionThreshold = Infinity,
debug,
}: MeshProps<Datum>) => {
// Used to get the relative cursor position.
const elementRef = useRef<SVGGElement>(null)
const [currentIndex, setCurrentIndex] = useState<number | null>(null)
// Store the index of the current point and the current node.
const [current, setCurrent] = useState<[number, Datum] | null>(null)
// Keep track of the previous index and node, this is needed as we don't have enter/leave events
// for each nodes because we use a single rect element to capture events.
const previous = useRef<[number, Datum] | null>(null)

useEffect(() => {
// Assign the latest current node to the ref, assigning a value to ref doesn't re-render.
previous.current = current
}, [current])

const { delaunay, voronoi } = useVoronoiMesh({
points: nodes,
Expand All @@ -54,18 +64,15 @@ export const Mesh = <Datum,>({
})

const voronoiPath = useMemo(() => {
if (debug && voronoi) {
return voronoi.render()
}

if (debug && voronoi) return voronoi.render()
return undefined
}, [debug, voronoi])

const getIndexAndNodeFromEvent = useCallback(
(event: MouseEvent<SVGRectElement> | TouchEvent<SVGRectElement>) => {
if (!elementRef.current) {
return [null, null]
}
(
event: MouseEvent<SVGRectElement> | TouchEvent<SVGRectElement>
): null | [number, Datum] => {
if (!elementRef.current) return null

const [x, y] = getRelativeCursor(elementRef.current, event)
let index: number | null = delaunay.find(x, y)
Expand All @@ -80,98 +87,95 @@ export const Mesh = <Datum,>({
}
}

return [node ? index : null, node] as [null, null] | [number, Datum]
if (index === null || node === null) return null
return [index, node]
},
[delaunay, nodes, detectionThreshold]
)

const handleMouseEnter = useCallback(
(event: MouseEvent<SVGRectElement>) => {
const [index, node] = getIndexAndNodeFromEvent(event)
setCurrentIndex(index)
if (node) {
onMouseEnter?.(node, event)
}
const match = getIndexAndNodeFromEvent(event)
setCurrent(match)
match && onMouseEnter?.(match[1], event)
},
[getIndexAndNodeFromEvent, setCurrentIndex, onMouseEnter]
[getIndexAndNodeFromEvent, setCurrent, onMouseEnter]
)

const handleMouseMove = useCallback(
(event: MouseEvent<SVGRectElement>) => {
const [index, node] = getIndexAndNodeFromEvent(event)
setCurrentIndex(index)
if (node) {
onMouseMove?.(node, event)
const match = getIndexAndNodeFromEvent(event)
setCurrent(match)

if (match) {
const [index, node] = match
if (previous.current) {
const [previousIndex, previousNode] = previous.current
if (index !== previousIndex) {
// Simulate an enter event if the previous index is different.
onMouseLeave?.(previousNode, event)
} else {
// If it's the same, trigger a regular move event.
onMouseMove?.(node, event)
}
} else {
onMouseEnter?.(node, event)
}
} else {
if (previous.current) {
// Simulate a leave event if there's a previous node.
onMouseLeave?.(previous.current[1], event)
}
}
},
[getIndexAndNodeFromEvent, setCurrentIndex, onMouseMove]
[getIndexAndNodeFromEvent, setCurrent, previous, onMouseEnter, onMouseMove, onMouseLeave]
)

const handleMouseLeave = useCallback(
(event: MouseEvent<SVGRectElement>) => {
setCurrentIndex(null)
if (onMouseLeave) {
let previousNode: Datum | undefined = undefined
if (currentIndex !== null) {
previousNode = nodes[currentIndex]
}
previousNode && onMouseLeave(previousNode, event)
setCurrent(null)
if (onMouseLeave && previous.current) {
onMouseLeave(previous.current[1], event)
}
},
[setCurrentIndex, currentIndex, onMouseLeave, nodes]
[setCurrent, previous, onMouseLeave]
)

const handleClick = useCallback(
(event: MouseEvent<SVGRectElement>) => {
const [index, node] = getIndexAndNodeFromEvent(event)
setCurrentIndex(index)
if (node) {
onClick?.(node, event)
}
const match = getIndexAndNodeFromEvent(event)
setCurrent(match)
match && onClick?.(match[1], event)
},
[getIndexAndNodeFromEvent, setCurrentIndex, onClick]
[getIndexAndNodeFromEvent, setCurrent, onClick]
)

const handleTouchStart = useCallback(
(event: TouchEvent<SVGRectElement>) => {
const [index, node] = getIndexAndNodeFromEvent(event)
if (enableTouchCrosshair) {
setCurrentIndex(index)
}
if (node) {
onTouchStart?.(node, event)
}
const match = getIndexAndNodeFromEvent(event)
enableTouchCrosshair && setCurrent(match)
match && onTouchStart?.(match[1], event)
},
[getIndexAndNodeFromEvent, enableTouchCrosshair, onTouchStart]
[getIndexAndNodeFromEvent, setCurrent, enableTouchCrosshair, onTouchStart]
)

const handleTouchMove = useCallback(
(event: TouchEvent<SVGRectElement>) => {
const [index, node] = getIndexAndNodeFromEvent(event)
if (enableTouchCrosshair) {
setCurrentIndex(index)
}
if (node) {
onTouchMove?.(node, event)
}
const match = getIndexAndNodeFromEvent(event)
enableTouchCrosshair && setCurrent(match)
match && onTouchMove?.(match[1], event)
},
[getIndexAndNodeFromEvent, enableTouchCrosshair, onTouchMove]
[getIndexAndNodeFromEvent, setCurrent, enableTouchCrosshair, onTouchMove]
)

const handleTouchEnd = useCallback(
(event: TouchEvent<SVGRectElement>) => {
if (enableTouchCrosshair) {
setCurrentIndex(null)
}
if (onTouchEnd) {
let previousNode: Datum | undefined = undefined
if (currentIndex !== null) {
previousNode = nodes[currentIndex]
}
previousNode && onTouchEnd(previousNode, event)
enableTouchCrosshair && setCurrent(null)
if (onTouchEnd && previous.current) {
onTouchEnd(previous.current[1], event)
}
},
[enableTouchCrosshair, onTouchEnd, currentIndex, nodes]
[enableTouchCrosshair, setCurrent, onTouchEnd, previous, nodes]
)

return (
Expand All @@ -180,8 +184,8 @@ export const Mesh = <Datum,>({
<>
<path d={voronoiPath} stroke="red" strokeWidth={1} opacity={0.75} />
{/* highlight the current cell */}
{currentIndex !== null && (
<path fill="pink" opacity={0.35} d={voronoi.renderCell(currentIndex)} />
{current && (
<path fill="pink" opacity={0.35} d={voronoi.renderCell(current[0])} />
)}
</>
)}
Expand Down
Binary file modified website/src/assets/captures/dendogram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions website/src/data/components/dendogram/meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Dendogram:
package: '@nivo/dendogram'
tags:
- hierarchy
- experimental
stories:
- label: Custom node component
link: dendogram--custom-node-component
Expand Down
8 changes: 4 additions & 4 deletions website/src/pages/dendogram/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,20 @@ const initialProperties: Pick<

isInteractive: defaults.isInteractive,
useMesh: true,
meshDetectionThreshold: 40,
meshDetectionThreshold: 60,
debugMesh: defaults.debugMesh,
highlightAncestorNodes: defaults.highlightAncestorNodes,
highlightDescendantNodes: defaults.highlightDescendantNodes,
}

const TreeMap = () => {
const Dendogram = () => {
const {
image: {
childImageSharp: { gatsbyImageData: image },
},
} = useStaticQuery(graphql`
query {
image: file(absolutePath: { glob: "**/src/assets/captures/treemap.png" }) {
image: file(absolutePath: { glob: "**/src/assets/captures/dendogram.png" }) {
childImageSharp {
gatsbyImageData(layout: FIXED, width: 700, quality: 100)
}
Expand Down Expand Up @@ -117,4 +117,4 @@ const TreeMap = () => {
)
}

export default TreeMap
export default Dendogram

0 comments on commit 10ae9e3

Please sign in to comment.