Skip to content

Commit

Permalink
feat: handle rendering of edges from node to itself (#425)
Browse files Browse the repository at this point in the history
* feat: handle rendering of edges from node to itself

* fix: multi self-edge positions and rotate start theta so as to avoid node and edge label overlap

* fix: handle changes in zoom for mouse events consistently

* fix: remove useless constructors, wrap reducers in usecallback
  • Loading branch information
urangel authored Feb 23, 2024
1 parent 4f580b6 commit 47bdcba
Show file tree
Hide file tree
Showing 14 changed files with 588 additions and 124 deletions.
69 changes: 49 additions & 20 deletions cmd/ui/src/components/GraphEdgeEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
getEdgeLabelTextLength,
getEdgeSourceAndTargetDisplayData,
} from 'src/ducks/graph/utils';
import { getBackgroundBoundInfo, getSelfEdgeStartingPoint } from 'src/rendering/programs/edge-label';
import { getControlPointsFromGroupSize } from 'src/rendering/programs/edge.self';
import { bezier } from 'src/rendering/utils/bezier';
import { useAppDispatch, useAppSelector } from 'src/store';

Expand All @@ -32,8 +34,6 @@ const GraphEdgeEvents: FC = () => {
const graphState = useAppSelector((state) => state.explore);

const sigma = useSigma();
const camera = sigma.getCamera();
const ratio = camera.getState().ratio;
const canvases = sigma.getCanvases();
const sigmaContainer = document.getElementById('sigma-container');
const mouseCanvas = canvases.mouse;
Expand Down Expand Up @@ -73,12 +73,14 @@ const GraphEdgeEvents: FC = () => {

const handleEdgeEvents = useCallback(
(event: any) => {
const context = edgeLabelsCanvas.getContext('2d');
if (!context) return;
if (event.type === 'click' || event.type === 'mousemove') {
const camera = sigma.getCamera();
const ratio = camera.getState().ratio;
const inverseSqrtZoomRatio = 1 / Math.sqrt(ratio);

const size = sigma.getSettings().edgeLabelSize;
const graph = sigma.getGraph();
const size = sigma.getSettings().edgeLabelSize * inverseSqrtZoomRatio;
const context = edgeLabelsCanvas.getContext('2d');
const edges = graph.edges();

for (let i = 0; i < edges.length; i++) {
Expand All @@ -89,6 +91,7 @@ const GraphEdgeEvents: FC = () => {
if (edgeData === null) continue;
const nodeDisplayData = getEdgeSourceAndTargetDisplayData(edgeData.source, edgeData.target, sigma);
if (nodeDisplayData === null) continue;

const { source, target } = nodeDisplayData;
const sourceCoordinates = { x: source.x, y: source.y };
const targetCoordinates = { x: target.x, y: target.y };
Expand All @@ -99,9 +102,7 @@ const GraphEdgeEvents: FC = () => {
{ ...sourceCoordinatesViewport, size: source.size },
{ ...targetCoordinatesViewport, size: target.size }
);
if (!edgeDistance.distance) continue;

const textLength = getEdgeLabelTextLength(context!, attributes.label, edgeDistance.distance);
const textLength = getEdgeLabelTextLength(context, attributes.label, edgeDistance.distance);
if (!textLength) continue;

let point = { x: edgeDistance.cx, y: edgeDistance.cy };
Expand All @@ -111,24 +112,52 @@ const GraphEdgeEvents: FC = () => {
attributes.groupPosition,
attributes.direction
);
const control = bezier.getControlAtMidpoint(curveHeight, sourceCoordinates, targetCoordinates);
const control = sigma.framedGraphToViewport(
bezier.getControlAtMidpoint(curveHeight, sourceCoordinates, targetCoordinates)
);

point = bezier.getCoordinatesAlongCurve(
point = bezier.getCoordinatesAlongQuadraticBezier(
sourceCoordinatesViewport,
targetCoordinatesViewport,
sigma.framedGraphToViewport(control),
control,
0.5
);
} else if (attributes.type === 'self') {
const radius = bezier.getLineLength(
{ x: 0, y: 0 },
{
x: source.size * Math.pow(inverseSqrtZoomRatio, 3),
y: target.size * Math.pow(inverseSqrtZoomRatio, 3),
}
);

const { control2, control3 } = getControlPointsFromGroupSize(
attributes.groupPosition,
radius * 3,
sourceCoordinatesViewport,
false,
true
);

point = getSelfEdgeStartingPoint(
sourceCoordinatesViewport,
control2,
control3,
sourceCoordinatesViewport
);
}

//Determine label dimensions and position
const xPadding = 4 * inverseSqrtZoomRatio;
const labelWidth = textLength * 1.4 * inverseSqrtZoomRatio + 2 * xPadding;
const labelHeight = size * 1.4;
const x1 = point.x + (-(textLength / 2) * inverseSqrtZoomRatio - xPadding);
const y1 = point.y + ((attributes.size / 2) * inverseSqrtZoomRatio - size);
const x2 = x1 + labelWidth;
const y2 = y1 + labelHeight;
const { deltaX, deltaY, width, height } = getBackgroundBoundInfo(
inverseSqrtZoomRatio,
textLength,
attributes.size * inverseSqrtZoomRatio,
size
);

const x1 = point.x + deltaX;
const y1 = point.y + deltaY;
const x2 = x1 + width;
const y2 = y1 + height;

const offsetY = edgeLabelsCanvas.getBoundingClientRect().y;
const { x: viewportX, y: viewportY } = {
Expand Down Expand Up @@ -162,7 +191,7 @@ const GraphEdgeEvents: FC = () => {
mouseCanvas.dispatchEvent(customEvent);
sigma.scheduleRefresh();
},
[sigma, mouseCanvas, edgeLabelsCanvas, onClickEdge, ratio, sigmaContainer]
[sigma, mouseCanvas, edgeLabelsCanvas, onClickEdge, sigmaContainer]
);

return (
Expand Down
94 changes: 65 additions & 29 deletions cmd/ui/src/components/GraphEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@
import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core';
import { setSelectedEdge } from 'bh-shared-ui';
import { AbstractGraph, Attributes } from 'graphology-types';
import { FC, useEffect, useRef, useState } from 'react';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { SigmaNodeEventPayload } from 'sigma/sigma';
import { getEdgeDataFromKey, getEdgeSourceAndTargetDisplayData, resetCamera } from 'src/ducks/graph/utils';
import {
getEdgeDataFromKey,
getEdgeSourceAndTargetDisplayData,
graphToFramedGraph,
resetCamera,
} from 'src/ducks/graph/utils';
import { bezier } from 'src/rendering/utils/bezier';
import { getNodeRadius } from 'src/rendering/utils/utils';
import { useAppDispatch, useAppSelector } from 'src/store';

export interface GraphEventProps {
Expand Down Expand Up @@ -59,6 +65,7 @@ export const GraphEvents: FC<GraphEventProps> = ({
const prevent = useRef(false);

const sigmaContainer = document.getElementById('sigma-container');
const { getControlAtMidpoint, getLineLength, calculateCurveHeight } = bezier;

const clearDraggedNode = () => {
setDraggedNode(null);
Expand All @@ -67,6 +74,48 @@ export const GraphEvents: FC<GraphEventProps> = ({
isLongPress.current = false;
};

const curvedEdgeReducer = useCallback(
(edge: string, data: Attributes, newData: Attributes) => {
// We calculate control points for all curved edges here and pass those along as edge attributes in both viewport and framed graph
// coordinates. We can then use those control points in our edge, edge label, and edge arrow programs.
const edgeData = getEdgeDataFromKey(edge);
if (edgeData !== null) {
const nodeDisplayData = getEdgeSourceAndTargetDisplayData(edgeData.source, edgeData.target, sigma);
if (nodeDisplayData !== null) {
const sourceCoordinates = { x: nodeDisplayData.source.x, y: nodeDisplayData.source.y };
const targetCoordinates = { x: nodeDisplayData.target.x, y: nodeDisplayData.target.y };

const height = calculateCurveHeight(data.groupSize, data.groupPosition, data.direction);
const control = getControlAtMidpoint(height, sourceCoordinates, targetCoordinates);

newData.control = control;
newData.controlInViewport = sigma.framedGraphToViewport(control);
}
}
},
[sigma, calculateCurveHeight, getControlAtMidpoint]
);

const selfEdgeReducer = useCallback(
(edge: string, newData: Attributes) => {
const edgeData = getEdgeDataFromKey(edge);
if (edgeData !== null) {
const nodeDisplayData = getEdgeSourceAndTargetDisplayData(edgeData.source, edgeData.target, sigma);
if (nodeDisplayData !== null) {
const nodeRadius = getNodeRadius(false, newData.inverseSqrtZoomRatio, nodeDisplayData.source.size);

const framedGraphNodeRadius = getLineLength(
graphToFramedGraph(sigma, { x: 0, y: 0 }),
graphToFramedGraph(sigma, { x: nodeRadius, y: nodeRadius })
);

newData.framedGraphNodeRadius = framedGraphNodeRadius;
}
}
},
[sigma, getLineLength]
);

useEffect(() => {
registerEvents({
enterNode: (event) => {
Expand Down Expand Up @@ -188,39 +237,26 @@ export const GraphEvents: FC<GraphEventProps> = ({
newData.selected = false;
}

// We calculate control points for all curved edges here and pass those along as edge attributes in both viewport and framed graph
// coordinates. We can then use those control points in our edge, edge label, and edge arrow programs.
if (data.type === 'curved') {
const edgeData = getEdgeDataFromKey(edge);
if (edgeData !== null) {
const nodeDisplayData = getEdgeSourceAndTargetDisplayData(
edgeData.source,
edgeData.target,
sigma
);
if (nodeDisplayData !== null) {
const sourceCoordinates = { x: nodeDisplayData.source.x, y: nodeDisplayData.source.y };
const targetCoordinates = { x: nodeDisplayData.target.x, y: nodeDisplayData.target.y };

const height = bezier.calculateCurveHeight(
data.groupSize,
data.groupPosition,
data.direction
);
const control = bezier.getControlAtMidpoint(height, sourceCoordinates, targetCoordinates);

newData.control = control;
newData.controlInViewport = sigma.framedGraphToViewport(control);
}
}
}
if (data.type === 'curved') curvedEdgeReducer(edge, data, newData);

if (data.type === 'self') selfEdgeReducer(edge, newData);

if (edgeReducer) return edgeReducer(edge, newData, graph);

return newData;
},
});
}, [hoveredNode, draggedNode, highlightedNode, selectedEdge, edgeReducer, setSettings, sigma]);
}, [
hoveredNode,
draggedNode,
highlightedNode,
selectedEdge,
curvedEdgeReducer,
selfEdgeReducer,
edgeReducer,
setSettings,
sigma,
]);

// Toggle off edge labels when dragging a node. Since these are rendered on a 2d canvas, dragging nodes with lots of edges
// can tank performance
Expand Down
7 changes: 6 additions & 1 deletion cmd/ui/src/components/SigmaChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { RankDirection } from 'src/hooks/useLayoutDagre/useLayoutDagre';
import drawEdgeLabel from 'src/rendering/programs/edge-label';
import EdgeArrowProgram from 'src/rendering/programs/edge.arrow';
import CurvedEdgeArrowProgram from 'src/rendering/programs/edge.curvedArrow';
import SelfEdgeArrowProgram from 'src/rendering/programs/edge.selfArrow';
import drawHover from 'src/rendering/programs/node-hover';
import drawLabel from 'src/rendering/programs/node-label';
import getNodeCombinedProgram from 'src/rendering/programs/node.combined';
Expand Down Expand Up @@ -88,7 +89,11 @@ const SigmaChart: FC<Partial<SigmaChartProps>> = ({
combined: getNodeCombinedProgram(),
glyphs: getNodeGlyphsProgram(),
},
edgeProgramClasses: { curved: CurvedEdgeArrowProgram, arrow: EdgeArrowProgram },
edgeProgramClasses: {
curved: CurvedEdgeArrowProgram,
self: SelfEdgeArrowProgram,
arrow: EdgeArrowProgram,
},
renderEdgeLabels: true,
hoverRenderer: drawHover,
edgeLabelRenderer: drawEdgeLabel,
Expand Down
11 changes: 5 additions & 6 deletions cmd/ui/src/ducks/graph/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ export type LinkedNode = {
y: number;
};

export const calculateEdgeDistanceForLabel = (
source: LinkedNode,
target: LinkedNode
): { distance: number; cx: number; cy: number } => {
export type EdgeDistanceProperties = { distance: number; cx: number; cy: number };
//This is for a single linear edge
export const calculateEdgeDistanceForLabel = (source: LinkedNode, target: LinkedNode): EdgeDistanceProperties => {
// Computing positions without considering nodes sizes:
const sSize = source.size;
const tSize = target.size;
Expand Down Expand Up @@ -112,7 +111,7 @@ export const getEdgeLabelTextLength = (
let label = edgeLabel;
let textLength = context.measureText(label).width;

if (textLength > edgeDistance) {
if (textLength > edgeDistance && edgeDistance !== 0) {
const ellipsis = '…';
label = label + ellipsis;
textLength = context.measureText(label).width;
Expand All @@ -127,7 +126,7 @@ export const getEdgeLabelTextLength = (
return textLength;
};

const graphToFramedGraph = (
export const graphToFramedGraph = (
sigma: Sigma<AbstractGraph<Attributes, Attributes, Attributes>>,
coordinates: Coordinates
): Coordinates => {
Expand Down
Loading

0 comments on commit 47bdcba

Please sign in to comment.