mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 06:12:59 +08:00
NodeGraph: Exploration mode (#33623)
* Add exploration option to node layout * Add hidden node count * Add grid layout option * Fix panning bounds calculation * Add legend with sorting * Allow sorting on any stats or arc value * Fix merge * Make sorting better * Reset focused node on layout change * Refactor limit hook a bit * Disable selected layout button * Don't show markers if only 1 node is hidden * Move legend to the bottom * Fix text backgrounds * Add show in graph layout action in grid layout * Center view on the focused node, fix perf issue when expanding big graph * Limit the node counting * Comment and linting fixes * Bit of code cleanup and comments * Add state for computing layout * Prevent computing map with partial data * Add rollup plugin for worker * Add rollup plugin for worker * Enhance data from worker * Fix perf issues with reduce and object creation * Improve comment * Fix tests * Css fixes * Remove worker plugin * Add comments * Fix test * Add test for exploration * Add test switching to grid layout * Apply suggestions from code review Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com> * Remove unused plugin * Fix function name * Remove unused rollup plugin * Review fixes * Fix context menu shown on layout change * Make buttons bigger * Moved NodeGraph to core grafana Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
193
public/app/plugins/panel/nodeGraph/usePanning.ts
Normal file
193
public/app/plugins/panel/nodeGraph/usePanning.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import { useEffect, useRef, RefObject, useState, useMemo } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { Bounds } from './utils';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
|
||||
export interface State {
|
||||
isPanning: boolean;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Options {
|
||||
scale?: number;
|
||||
bounds?: Bounds;
|
||||
focus?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on https://github.com/streamich/react-use/blob/master/src/useSlider.ts
|
||||
* Returns position x/y coordinates which can be directly used in transform: translate().
|
||||
* @param scale - Can be used when we want to scale the movement if we are moving a scaled element. We need to do it
|
||||
* here because we don't want to change the pos when scale changes.
|
||||
* @param bounds - If set the panning cannot go outside of those bounds.
|
||||
* @param focus - Position to focus on.
|
||||
*/
|
||||
export function usePanning<T extends Element>({ scale = 1, bounds, focus }: Options = {}): {
|
||||
state: State;
|
||||
ref: RefObject<T>;
|
||||
} {
|
||||
const isMounted = useMountedState();
|
||||
const isPanning = useRef(false);
|
||||
const frame = useRef(0);
|
||||
const panRef = useRef<T>(null);
|
||||
|
||||
const initial = { x: 0, y: 0 };
|
||||
// As we return a diff of the view port to be applied we need as translate coordinates we have to invert the
|
||||
// bounds of the content to get the bounds of the view port diff.
|
||||
const viewBounds = useMemo(
|
||||
() => ({
|
||||
right: bounds ? -bounds.left : Infinity,
|
||||
left: bounds ? -bounds.right : -Infinity,
|
||||
bottom: bounds ? -bounds.top : -Infinity,
|
||||
top: bounds ? -bounds.bottom : Infinity,
|
||||
}),
|
||||
[bounds]
|
||||
);
|
||||
|
||||
// We need to keep some state so we can compute the position diff and add that to the previous position.
|
||||
const startMousePosition = useRef(initial);
|
||||
const prevPosition = useRef(initial);
|
||||
// We cannot use the state as that would rerun the effect on each state change which we don't want so we have to keep
|
||||
// separate variable for the state that won't cause useEffect eval
|
||||
const currentPosition = useRef(initial);
|
||||
|
||||
const [state, setState] = useState<State>({
|
||||
isPanning: false,
|
||||
position: initial,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const startPanning = (event: Event) => {
|
||||
if (!isPanning.current && isMounted()) {
|
||||
isPanning.current = true;
|
||||
// Snapshot the current position of both mouse pointer and the element
|
||||
startMousePosition.current = getEventXY(event);
|
||||
prevPosition.current = { ...currentPosition.current };
|
||||
setState((state) => ({ ...state, isPanning: true }));
|
||||
bindEvents();
|
||||
}
|
||||
};
|
||||
|
||||
const stopPanning = () => {
|
||||
if (isPanning.current && isMounted()) {
|
||||
isPanning.current = false;
|
||||
setState((state) => ({ ...state, isPanning: false }));
|
||||
unbindEvents();
|
||||
}
|
||||
};
|
||||
|
||||
const onPanStart = (event: Event) => {
|
||||
startPanning(event);
|
||||
onPan(event);
|
||||
};
|
||||
|
||||
const bindEvents = () => {
|
||||
document.addEventListener('mousemove', onPan);
|
||||
document.addEventListener('mouseup', stopPanning);
|
||||
document.addEventListener('touchmove', onPan);
|
||||
document.addEventListener('touchend', stopPanning);
|
||||
};
|
||||
|
||||
const unbindEvents = () => {
|
||||
document.removeEventListener('mousemove', onPan);
|
||||
document.removeEventListener('mouseup', stopPanning);
|
||||
document.removeEventListener('touchmove', onPan);
|
||||
document.removeEventListener('touchend', stopPanning);
|
||||
};
|
||||
|
||||
const onPan = (event: Event) => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
const pos = getEventXY(event);
|
||||
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
if (isMounted() && panRef.current) {
|
||||
// Get the diff by which we moved the mouse.
|
||||
let xDiff = pos.x - startMousePosition.current.x;
|
||||
let yDiff = pos.y - startMousePosition.current.y;
|
||||
|
||||
// Add the diff to the position from the moment we started panning.
|
||||
currentPosition.current = {
|
||||
x: inBounds(prevPosition.current.x + xDiff / scale, viewBounds.left, viewBounds.right),
|
||||
y: inBounds(prevPosition.current.y + yDiff / scale, viewBounds.top, viewBounds.bottom),
|
||||
};
|
||||
setState((state) => ({
|
||||
...state,
|
||||
position: {
|
||||
...currentPosition.current,
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const ref = panRef.current;
|
||||
if (ref) {
|
||||
ref.addEventListener('mousedown', onPanStart);
|
||||
ref.addEventListener('touchstart', onPanStart);
|
||||
}
|
||||
return () => {
|
||||
if (ref) {
|
||||
ref.removeEventListener('mousedown', onPanStart);
|
||||
ref.removeEventListener('touchstart', onPanStart);
|
||||
}
|
||||
};
|
||||
}, [scale, viewBounds, isMounted]);
|
||||
|
||||
const previousFocus = usePrevious(focus);
|
||||
|
||||
// We need to update the state in case need to focus on something but we want to do it only once when the focus
|
||||
// changes to something new.
|
||||
useEffect(() => {
|
||||
if (focus && previousFocus?.x !== focus.x && previousFocus?.y !== focus.y) {
|
||||
const position = {
|
||||
x: inBounds(focus.x, viewBounds.left, viewBounds.right),
|
||||
y: inBounds(focus.y, viewBounds.top, viewBounds.bottom),
|
||||
};
|
||||
setState({
|
||||
position,
|
||||
isPanning: false,
|
||||
});
|
||||
|
||||
currentPosition.current = position;
|
||||
prevPosition.current = position;
|
||||
}
|
||||
}, [focus, previousFocus, viewBounds, currentPosition, prevPosition]);
|
||||
|
||||
let position = state.position;
|
||||
// This part prevents an ugly jump from initial position to the focused one as the set state in the effects is after
|
||||
// initial render.
|
||||
if (focus && previousFocus?.x !== focus.x && previousFocus?.y !== focus.y) {
|
||||
position = focus;
|
||||
}
|
||||
|
||||
return {
|
||||
state: {
|
||||
...state,
|
||||
position: {
|
||||
x: inBounds(position.x, viewBounds.left, viewBounds.right),
|
||||
y: inBounds(position.y, viewBounds.top, viewBounds.bottom),
|
||||
},
|
||||
},
|
||||
ref: panRef,
|
||||
};
|
||||
}
|
||||
|
||||
function inBounds(value: number, min: number | undefined, max: number | undefined) {
|
||||
return Math.min(Math.max(value, min ?? -Infinity), max ?? Infinity);
|
||||
}
|
||||
|
||||
function getEventXY(event: Event): { x: number; y: number } {
|
||||
if ((event as any).changedTouches) {
|
||||
const e = event as TouchEvent;
|
||||
return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
|
||||
} else {
|
||||
const e = event as MouseEvent;
|
||||
return { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user