Node Graph: Emphasize hovered or connected nodes (#51925)

* Node Graph: Emphasize hovered or connected nodes

* Add tests and refactor into util functions
This commit is contained in:
Connor Lindsey
2022-07-12 06:14:45 -06:00
committed by GitHub
parent 4155dc8eca
commit db9c9b5354
5 changed files with 107 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import cx from 'classnames';
import React, { memo, MouseEvent, MutableRefObject, useCallback, useMemo, useState } from 'react';
import React, { memo, MouseEvent, MutableRefObject, useCallback, useEffect, useMemo, useState } from 'react';
import useMeasure from 'react-use/lib/useMeasure';
import { DataFrame, GrafanaTheme2, LinkModel } from '@grafana/data';
@@ -21,7 +21,7 @@ import { useFocusPositionOnLayout } from './useFocusPositionOnLayout';
import { useHighlight } from './useHighlight';
import { usePanning } from './usePanning';
import { useZoom } from './useZoom';
import { processNodes, Bounds } from './utils';
import { processNodes, Bounds, findConnectedNodesForEdge, findConnectedNodesForNode } from './utils';
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css`
@@ -120,10 +120,6 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit }: Props) {
const [measureRef, { width, height }] = useMeasure();
const [config, setConfig] = useState<Config>(defaultConfig);
// We need hover state here because for nodes we also highlight edges and for edges have labels separate to make
// sure they are visible on top of everything else
const { nodeHover, setNodeHover, clearNodeHover, edgeHover, setEdgeHover, clearEdgeHover } = useHover();
const firstNodesDataFrame = nodesDataFrames[0];
const firstEdgesDataFrame = edgesDataFrames[0];
@@ -136,6 +132,20 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit }: Props) {
[firstEdgesDataFrame, firstNodesDataFrame, theme]
);
// We need hover state here because for nodes we also highlight edges and for edges have labels separate to make
// sure they are visible on top of everything else
const { nodeHover, setNodeHover, clearNodeHover, edgeHover, setEdgeHover, clearEdgeHover } = useHover();
const [hoveringIds, setHoveringIds] = useState<string[]>([]);
useEffect(() => {
let linked: string[] = [];
if (nodeHover) {
linked = findConnectedNodesForNode(processed.nodes, processed.edges, nodeHover);
} else if (edgeHover) {
linked = findConnectedNodesForEdge(processed.nodes, processed.edges, edgeHover);
}
setHoveringIds(linked);
}, [nodeHover, edgeHover, processed]);
// This is used for navigation from grid to graph view. This node will be centered and briefly highlighted.
const [focusedNodeId, setFocusedNodeId] = useState<string>();
const setFocused = useCallback((e: MouseEvent, m: NodesMarker) => setFocusedNodeId(m.node.id), [setFocusedNodeId]);
@@ -216,7 +226,7 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit }: Props) {
onMouseEnter={setNodeHover}
onMouseLeave={clearNodeHover}
onClick={onNodeOpen}
hoveringId={nodeHover || highlightId}
hoveringIds={hoveringIds || [highlightId]}
/>
<Markers markers={markers || []} onClick={setFocused} />
@@ -274,14 +284,16 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit }: Props) {
);
}
// These components are here as a perf optimisation to prevent going through all nodes and edges on every pan/zoom.
// Active -> emphasized, inactive -> de-emphasized, and default -> normal styling
export type HoverState = 'active' | 'inactive' | 'default';
// These components are here as a perf optimisation to prevent going through all nodes and edges on every pan/zoom.
interface NodesProps {
nodes: NodeDatum[];
onMouseEnter: (id: string) => void;
onMouseLeave: (id: string) => void;
onClick: (event: MouseEvent<SVGElement>, node: NodeDatum) => void;
hoveringId?: string;
hoveringIds?: string[];
}
const Nodes = memo(function Nodes(props: NodesProps) {
return (
@@ -293,7 +305,13 @@ const Nodes = memo(function Nodes(props: NodesProps) {
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
onClick={props.onClick}
hovering={props.hoveringId === n.id}
hovering={
!props.hoveringIds || props.hoveringIds.length === 0
? 'default'
: props.hoveringIds?.includes(n.id)
? 'active'
: 'inactive'
}
/>
))}
</>