Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Touch crosshair for line graphs #2524

Merged
merged 17 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ the various packages, please execute the following:
make init
```

> please note that it will take a while as this project uses a lot of dependencies…
> please note that it will take a while as this project uses a lot of dependencies…'

### Windows

If you want to build this project on Windows, it is recommended to use either WSL 2, or Git bash + `choco install make`.

## Development

Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"keywords": [],
"devDependencies": {
"@babel/core": "^7.21.5",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.5",
"@ekino/config": "^0.3.0",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.2",
Expand All @@ -27,6 +30,7 @@
"@types/lodash": "^4.14.170",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"@wojtekmaj/enzyme-adapter-react-17": "0.6.6",
Expand All @@ -35,6 +39,7 @@
"babel-loader": "^8.2.3",
"chalk": "^5.2.0",
"chalk-template": "^1.0.0",
"cypress": "^12.11.0",
"enzyme": "^3.11.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
Expand All @@ -57,16 +62,14 @@
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-test-renderer": "^18.2.0",
"@types/react-test-renderer": "^18.0.0",
"resize-observer-polyfill": "^1.5.1",
"rollup": "^3.21.0",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-size": "^0.3.1",
"rollup-plugin-strip-banner": "^3.0.0",
"rollup-plugin-visualizer": "^5.5.2",
"serve": "^13.0.2",
"typescript": "^4.9.5",
"cypress": "^12.11.0"
"typescript": "^4.9.5"
},
"resolutions": {
"@types/react": "^18.2.0",
Expand Down
5 changes: 4 additions & 1 deletion packages/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,10 @@ export function usePropertyAccessor<Datum, Value>(
accessor: PropertyAccessor<Datum, Value>
): (datum: Datum) => Value

export function getRelativeCursor(element: Element, event: React.MouseEvent): [number, number]
export function getRelativeCursor(
element: Element,
event: Pick<React.MouseEvent, 'clientX' | 'clientY'>
): [number, number]
export function isCursorInRect(
x: number,
y: number,
Expand Down
5 changes: 5 additions & 0 deletions packages/line/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface Point {
export type AccessorFunc = (datum: Point['data']) => string

export type PointMouseHandler = (point: Point, event: React.MouseEvent) => void
export type PointTouchHandler = (point: Point, event: React.TouchEvent) => void

export interface PointTooltipProps {
point: Point
Expand Down Expand Up @@ -185,6 +186,9 @@ export interface LineProps {
onMouseMove?: PointMouseHandler
onMouseLeave?: PointMouseHandler
onClick?: PointMouseHandler
onTouchStart?: PointTouchHandler
plouc marked this conversation as resolved.
Show resolved Hide resolved
onTouchMove?: PointTouchHandler
onTouchEnd?: PointTouchHandler

debugMesh?: boolean

Expand All @@ -197,6 +201,7 @@ export interface LineProps {

enableCrosshair?: boolean
crosshairType?: CrosshairType
touchCrosshair?: boolean

legends?: LegendProps[]
}
Expand Down
11 changes: 11 additions & 0 deletions packages/line/src/Line.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ const Line = props => {
onMouseMove,
onMouseLeave,
onClick,
onTouchStart,
onTouchMove,
onTouchEnd,

tooltip = PointTooltip,

Expand All @@ -110,6 +113,7 @@ const Line = props => {

enableCrosshair = true,
crosshairType = 'bottom-left',
touchCrosshair = false,

role = 'img',
} = props
Expand Down Expand Up @@ -241,6 +245,9 @@ const Line = props => {
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onClick={onClick}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
/>
)
}
Expand Down Expand Up @@ -303,7 +310,11 @@ const Line = props => {
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onClick={onClick}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
tooltip={tooltip}
touchCrosshair={touchCrosshair}
debug={debugMesh}
/>
)
Expand Down
43 changes: 43 additions & 0 deletions packages/line/src/Mesh.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ const Mesh = ({
onMouseMove,
onMouseLeave,
onClick,
onTouchStart,
onTouchMove,
onTouchEnd,
tooltip,
debug,
touchCrosshair,
}) => {
const { showTooltipAt, hideTooltip } = useTooltip()

Expand Down Expand Up @@ -68,6 +72,41 @@ const Mesh = ({
[onClick]
)

const handleTouchStart = useCallback(
plouc marked this conversation as resolved.
Show resolved Hide resolved
(point, event) => {
showTooltipAt(
createElement(tooltip, { point }),
[point.x + margin.left, point.y + margin.top],
'top'
)
setCurrent(point)
onTouchStart && onTouchStart(point, event)
},
[onTouchStart]
)

const handleTouchMove = useCallback(
(point, event) => {
showTooltipAt(
createElement(tooltip, { point }),
[point.x + margin.left, point.y + margin.top],
'top'
)
setCurrent(point)
onTouchMove && onTouchMove(point, event)
},
[onTouchMove]
)

const handleTouchEnd = useCallback(
(point, event) => {
hideTooltip()
setCurrent(null)
onTouchEnd && onTouchEnd(point, event)
},
[onTouchEnd, hideTooltip, setCurrent]
)

return (
<BaseMesh
nodes={points}
Expand All @@ -77,6 +116,10 @@ const Mesh = ({
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
touchCrosshair={touchCrosshair}
debug={debug}
/>
)
Expand Down
6 changes: 5 additions & 1 deletion packages/tooltip/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export interface TooltipActionsContextData {
position: [number, number],
anchor?: TooltipAnchor
) => void
showTooltipFromEvent: (content: JSX.Element, event: MouseEvent, anchor?: TooltipAnchor) => void
showTooltipFromEvent: (
plouc marked this conversation as resolved.
Show resolved Hide resolved
content: JSX.Element,
event: Pick<MouseEvent, 'clientX' | 'clientY'>,
anchor?: TooltipAnchor
) => void
hideTooltip: () => void
}

Expand Down
6 changes: 5 additions & 1 deletion packages/tooltip/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export const useTooltipHandlers = (container: MutableRefObject<HTMLDivElement>)
)

const showTooltipFromEvent: TooltipActionsContextData['showTooltipFromEvent'] = useCallback(
(content: JSX.Element, event: MouseEvent, anchor: TooltipAnchor = 'top') => {
(
content: JSX.Element,
event: Pick<MouseEvent, 'clientX' | 'clientY'>,
anchor: TooltipAnchor = 'top'
) => {
const bounds = container.current.getBoundingClientRect()
const offsetWidth = container.current.offsetWidth
// In a normal situation mouse enter / mouse leave events
Expand Down
84 changes: 76 additions & 8 deletions packages/voronoi/src/Mesh.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useRef, useState, useCallback, useMemo, MouseEvent } from 'react'
import { useRef, useState, useCallback, useMemo, MouseEvent, TouchEvent } from 'react'
import { getRelativeCursor } from '@nivo/core'
import { useVoronoiMesh } from './hooks'
import { XYAccessor } from './computeMesh'

type MouseHandler<Datum> = (datum: Datum, event: MouseEvent) => void
type TouchHandler<Datum> = (datum: Datum, event: TouchEvent) => void

interface MeshProps<Datum> {
nodes: Datum[]
Expand All @@ -15,6 +16,10 @@ interface MeshProps<Datum> {
onMouseMove?: MouseHandler<Datum>
onMouseLeave?: MouseHandler<Datum>
onClick?: MouseHandler<Datum>
onTouchStart?: TouchHandler<Datum>
onTouchMove?: TouchHandler<Datum>
onTouchEnd?: TouchHandler<Datum>
touchCrosshair?: boolean
debug?: boolean
}

Expand All @@ -28,6 +33,10 @@ export const Mesh = <Datum,>({
onMouseMove,
onMouseLeave,
onClick,
onTouchStart,
onTouchMove,
onTouchEnd,
touchCrosshair = false,
debug,
}: MeshProps<Datum>) => {
const elementRef = useRef<SVGGElement>(null)
Expand All @@ -50,7 +59,7 @@ export const Mesh = <Datum,>({
return undefined
}, [debug, voronoi])

const getIndexAndNodeFromEvent = useCallback(
const getIndexAndNodeFromMouseEvent = useCallback(
(event: MouseEvent<SVGRectElement>) => {
if (!elementRef.current) {
return [null, null]
Expand All @@ -64,26 +73,40 @@ export const Mesh = <Datum,>({
[elementRef, delaunay]
)

const getIndexAndNodeFromTouchEvent = useCallback(
(event: TouchEvent<SVGRectElement>) => {
if (!elementRef.current) {
return [null, null]
}

const [x, y] = getRelativeCursor(elementRef.current, event.touches[0])
const index = delaunay.find(x, y)

return [index, index !== undefined ? nodes[index] : null] as [number, Datum | null]
},
[elementRef, delaunay]
)

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

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

const handleMouseLeave = useCallback(
Expand All @@ -102,13 +125,55 @@ export const Mesh = <Datum,>({

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

const handleTouchStart = useCallback(
(event: TouchEvent<SVGRectElement>) => {
plouc marked this conversation as resolved.
Show resolved Hide resolved
const [index, node] = getIndexAndNodeFromTouchEvent(event)
if (touchCrosshair) {
setCurrentIndex(index)
}
if (node) {
onTouchStart?.(node, event)
}
},
[getIndexAndNodeFromTouchEvent, setCurrentIndex, onTouchStart]
)

const handleTouchMove = useCallback(
(event: TouchEvent<SVGRectElement>) => {
plouc marked this conversation as resolved.
Show resolved Hide resolved
const [index, node] = getIndexAndNodeFromTouchEvent(event)
if (touchCrosshair) {
setCurrentIndex(index)
}
if (node) {
onTouchMove?.(node, event)
}
},
[getIndexAndNodeFromTouchEvent, setCurrentIndex, onTouchMove]
)

const handleTouchEnd = useCallback(
(event: TouchEvent<SVGRectElement>) => {
if (touchCrosshair) {
setCurrentIndex(null)
}
if (onTouchEnd) {
let previousNode: Datum | undefined = undefined
if (currentIndex !== null) {
previousNode = nodes[currentIndex]
}
previousNode && onTouchEnd(previousNode, event)
}
},
[setCurrentIndex, currentIndex, onTouchEnd, nodes]
)
plouc marked this conversation as resolved.
Show resolved Hide resolved

return (
Expand All @@ -132,6 +197,9 @@ export const Mesh = <Datum,>({
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onClick={handleClick}
/>
</g>
Expand Down