Skip to content

Commit

Permalink
#648 Display graph in ontology view
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps authored and joepio committed Nov 2, 2023
1 parent d2d65ec commit e807e1e
Show file tree
Hide file tree
Showing 11 changed files with 1,152 additions and 22 deletions.
2 changes: 2 additions & 0 deletions browser/data-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@bugsnag/core": "^7.16.1",
"@bugsnag/js": "^7.16.5",
"@bugsnag/plugin-react": "^7.16.5",
"@dagrejs/dagre": "^1.0.2",
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
Expand Down Expand Up @@ -35,6 +36,7 @@
"react-router-dom": "^6.9.0",
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.7",
"reactflow": "^11.8.3",
"remark-gfm": "^3.0.1",
"styled-components": "^6.0.7",
"stylis": "4.3.0",
Expand Down
123 changes: 123 additions & 0 deletions browser/data-browser/src/chunks/GraphViewer/FloatingEdge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { useCallback } from 'react';
import {
useStore as useFlowStore,
getBezierPath,
EdgeText,
EdgeProps,
Node,
} from 'reactflow';
import styled, { useTheme } from 'styled-components';
import { getEdgeParams, getSelfReferencePath } from './getEdgeParams';
import { EdgeData } from './buildGraph';

const getPathData = (
sourceNode: Node,
targetNode: Node,
overlapping: boolean,
) => {
// Self referencing edges use a custom path.
if (sourceNode.id === targetNode.id) {
return getSelfReferencePath(sourceNode);
}

const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
sourceNode,
targetNode,
overlapping,
);

return getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: sourcePos,
targetPosition: targetPos,
targetX: tx,
targetY: ty,
});
};

function Label({ text }: { text: string }): JSX.Element | string {
const parts = text.split('\n');

if (parts.length === 1) {
return text;
}

// SVG does not have any auto word wrap so we split the lines manually and offset them.
return (
<>
{parts.map((part, i) => (
<tspan x={0} dy={i === 0 ? '-0.3em' : '1.2em'} key={part}>
{part}
</tspan>
))}
</>
);
}

/**
* A custom edge that doesn't clutter the graph as mutch as the default edge.
* It casts a ray from the center of the source node to the center of the target node then draws a bezier curve between the two intersecting border of the nodes.
*/
export function FloatingEdge({
id,
source,
target,
markerEnd,
style,
label,
data,
}: EdgeProps<EdgeData>) {
const theme = useTheme();
const sourceNode = useFlowStore(
useCallback(store => store.nodeInternals.get(source), [source]),
);
const targetNode = useFlowStore(
useCallback(store => store.nodeInternals.get(target), [target]),
);

if (!sourceNode || !targetNode) {
return null;
}

const [path, labelX, labelY] = getPathData(
sourceNode,
targetNode,
!!data?.overlapping,
);

return (
<>
<Path
id={id}
className='react-flow__edge-path'
d={path}
markerEnd={markerEnd}
style={style}
/>
<EdgeText
x={labelX}
y={labelY}
label={<Label text={label as string} />}
labelStyle={{
fill: theme.colors.text,
}}
labelShowBg
labelBgStyle={{ fill: theme.colors.bg1 }}
labelBgPadding={[2, 4]}
labelBgBorderRadius={2}
/>
</>
);
}

const Path = styled.path`
flex-direction: column;
display: flex;
flex-grow: 1;
height: 100%;
& .react-flow__handle {
opacity: 0;
}
`;
75 changes: 75 additions & 0 deletions browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Resource, useStore } from '@tomic/react';
import React, { useCallback } from 'react';
import ReactFlow, {
Controls,
useReactFlow,
Node,
ReactFlowProvider,
} from 'reactflow';
import 'reactflow/dist/style.css';
import './reactFlowOverrides.css';
import { buildGraph } from './buildGraph';
import { FloatingEdge } from './FloatingEdge';
import { useGraph } from './useGraph';
import { useEffectOnce } from '../../hooks/useEffectOnce';
import { toAnchorId } from '../../views/OntologyPage/toAnchorId';

const edgeTypes = {
floating: FloatingEdge,
};

interface OntologyGraphProps {
ontology: Resource;
}

/**
* !ASYNC COMPONENT, DO NOT IMPORT DIRECTLY!
* Displays an ontology as a graph.
*/
export default function OntologyGraph({
...props
}: OntologyGraphProps): JSX.Element {
return (
<ReactFlowProvider>
<OntologyGraphInner {...props} />
</ReactFlowProvider>
);
}

function OntologyGraphInner({ ontology }: OntologyGraphProps): JSX.Element {
const store = useStore();
const { fitView } = useReactFlow();

const { nodes, edges, setGraph, handleNodeChange, handleNodeDoubleClick } =
useGraph(ontology);

useEffectOnce(() => {
buildGraph(ontology, store).then(([n, e]) => {
setGraph(n, e);

requestAnimationFrame(() => {
fitView();
});
});
});

const handleClick = useCallback((_: React.MouseEvent, node: Node) => {
const domId = toAnchorId(node.id);

document.getElementById(domId)?.scrollIntoView({ behavior: 'smooth' });
}, []);

return (
<ReactFlow
fitView
nodes={nodes}
edges={edges}
edgeTypes={edgeTypes}
onNodesChange={handleNodeChange}
onNodeClick={handleClick}
onNodeDoubleClick={handleNodeDoubleClick}
>
<Controls position='top-left' showInteractive={false} />
</ReactFlow>
);
}
Loading

0 comments on commit e807e1e

Please sign in to comment.