-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(graph): Add selection based zoom
- Loading branch information
1 parent
2a1b615
commit ddd06cd
Showing
5 changed files
with
167 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import * as React from 'react'; | ||
import * as d3 from 'd3'; | ||
import { observer } from 'mobx-react'; | ||
import { action } from 'mobx'; | ||
import ElementContext from '../utils/ElementContext'; | ||
import useCallbackRef from '../utils/useCallbackRef'; | ||
import { Graph, isGraph } from '../types'; | ||
import { PanZoomRef } from './usePanZoom'; | ||
import Point from '../geom/Point'; | ||
|
||
export type DragZoomRef = (node: SVGGElement | null) => void; | ||
|
||
// Used to send events prevented by d3.zoom to the document allowing modals, dropdowns, etc, to close | ||
const propagateDragZoomMouseEvent = (e: Event): void => { | ||
document.dispatchEvent(new MouseEvent(e.type, e)); | ||
}; | ||
|
||
export const useDragZoom = (): WithDragZoomProps => { | ||
const element = React.useContext(ElementContext); | ||
const [draggingState, setDraggingState] = React.useState<Omit<WithDragZoomProps, 'dragZoomRef'>>({}); | ||
|
||
if (!isGraph(element)) { | ||
throw new Error('useDragZoom must be used within the scope of a Graph'); | ||
} | ||
const elementRef = React.useRef<Graph>(element); | ||
elementRef.current = element; | ||
|
||
const zoomToDragArea = (dragStart: Point, dragEnd: Point) => { | ||
const currentScale = elementRef.current.getScale(); | ||
const graphPosition = elementRef.current.getPosition(); | ||
|
||
const x = (Math.min(dragStart.x, dragEnd.x) - graphPosition.x) / currentScale; | ||
const y = (Math.min(dragStart.y, dragEnd.y) - graphPosition.y) / currentScale; | ||
const width = Math.abs(dragEnd.x - dragStart.x) / currentScale; | ||
const height = Math.abs(dragEnd.y - dragStart.y) / currentScale; | ||
|
||
if (width < 10 || height < 10) { | ||
return; | ||
} | ||
|
||
const { width: fullWidth, height: fullHeight } = elementRef.current.getDimensions(); | ||
|
||
// compute the scale | ||
const xScale = fullWidth / width; | ||
const yScale = fullHeight / height; | ||
const scale = Math.min(xScale, yScale); | ||
|
||
// translate to center | ||
const midX = x + width / 2; | ||
const midY = y + height / 2; | ||
const tx = fullWidth / 2 - midX * scale; | ||
const ty = fullHeight / 2 - midY * scale; | ||
|
||
elementRef.current.setScale(scale); | ||
elementRef.current.setPosition(new Point(tx, ty)); | ||
}; | ||
|
||
const dragZoomRef = useCallbackRef<PanZoomRef>((node: SVGGElement | null) => { | ||
if (node) { | ||
// TODO fix any type | ||
const $svg = d3.select(node.ownerSVGElement) as any; | ||
if (node && node.ownerSVGElement) { | ||
node.ownerSVGElement.addEventListener('mousedown', propagateDragZoomMouseEvent); | ||
node.ownerSVGElement.addEventListener('click', propagateDragZoomMouseEvent); | ||
} | ||
const drag = d3 | ||
.drag() | ||
.on( | ||
'start', | ||
action((event: d3.D3DragEvent<Element, any, any>) => { | ||
const { offsetX, offsetY } = | ||
event.sourceEvent instanceof MouseEvent ? event.sourceEvent : { offsetX: 0, offsetY: 0 }; | ||
const { width: maxX, height: maxY } = elementRef.current.getDimensions(); | ||
|
||
const startPoint = new Point(Math.min(Math.max(offsetX, 0), maxX), Math.min(Math.max(offsetY, 0), maxY)); | ||
setDraggingState({ | ||
isZoomDragging: true, | ||
zoomDragStart: startPoint, | ||
zoomDragEnd: startPoint | ||
}); | ||
}) | ||
) | ||
.on( | ||
'drag', | ||
action((event: d3.D3DragEvent<Element, any, any>) => { | ||
const { offsetX, offsetY } = | ||
event.sourceEvent instanceof MouseEvent ? event.sourceEvent : { offsetX: 0, offsetY: 0 }; | ||
const { width: maxX, height: maxY } = elementRef.current.getDimensions(); | ||
setDraggingState((prev) => ({ | ||
...prev, | ||
zoomDragEnd: new Point(Math.min(Math.max(offsetX, 0), maxX), Math.min(Math.max(offsetY, 0), maxY)) | ||
})); | ||
}) | ||
) | ||
.on( | ||
'end', | ||
action(() => { | ||
setDraggingState((prev) => { | ||
zoomToDragArea(prev.zoomDragStart, prev.zoomDragEnd); | ||
return { isZoomDragging: false }; | ||
}); | ||
}) | ||
) | ||
.filter((event: React.MouseEvent) => event.ctrlKey && !event.button); | ||
drag($svg); | ||
} | ||
|
||
return () => { | ||
if (node) { | ||
// remove all drag listeners | ||
d3.select(node.ownerSVGElement).on('.drag', null); | ||
if (node.ownerSVGElement) { | ||
node.ownerSVGElement.removeEventListener('mousedown', propagateDragZoomMouseEvent); | ||
node.ownerSVGElement.removeEventListener('click', propagateDragZoomMouseEvent); | ||
} | ||
} | ||
}; | ||
}); | ||
return { dragZoomRef, ...draggingState }; | ||
}; | ||
export interface WithDragZoomProps { | ||
dragZoomRef?: DragZoomRef; | ||
isZoomDragging?: boolean; | ||
zoomDragStart?: Point; | ||
zoomDragEnd?: Point; | ||
} | ||
|
||
export const withDragZoom = | ||
() => | ||
<P extends WithDragZoomProps>(WrappedComponent: React.ComponentType<P>) => { | ||
const Component: React.FunctionComponent<Omit<P, keyof WithDragZoomProps>> = (props) => { | ||
const dragZoomProps = useDragZoom(); | ||
return <WrappedComponent {...(props as any)} {...dragZoomProps} />; | ||
}; | ||
Component.displayName = `withPanZoom(${WrappedComponent.displayName || WrappedComponent.name})`; | ||
return observer(Component); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters