import { ReactNode, useEffect, useRef, useState } from "react"; import { apiAuth } from "./api"; import { GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, SelectionProps, SelectionResult, useSelection, } from "reagraph"; import { customTheme } from "./NetworkTheme"; import { useSession } from "./Session"; import { useNavigate } from "react-router"; interface NetworkData { nodes: GraphNode[]; edges: GraphEdge[]; } interface CustomSelectionProps extends SelectionProps { ignore: (GraphEdge | undefined)[]; } const useCustomSelection = (props: CustomSelectionProps): SelectionResult => { var result = useSelection(props); result.actives = result.actives.filter( (s) => !props.ignore.map((edge) => edge?.id).includes(s) ); const ignored_nodes = props.ignore.map((edge) => edge && result.selections?.includes(edge.source) && !result.selections?.includes(edge.target) ? edge.target : "" ); result.actives = result.actives.filter((s) => !ignored_nodes.includes(s)); return result; }; export const GraphComponent = () => { let initialData = { nodes: [], edges: [] } as NetworkData; const [data, setData] = useState(initialData); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [threed, setThreed] = useState(false); const [likes, setLikes] = useState(2); const [popularity, setPopularity] = useState(false); const [mutuality, setMutuality] = useState(false); const { user, teams } = useSession(); const navigate = useNavigate(); useEffect(() => { user?.scopes.includes(`team:${teams?.activeTeam}`) || navigate("/", { replace: true }); }, [user]); async function loadData() { setLoading(true); if (teams) { await apiAuth(`analysis/graph_json/${teams?.activeTeam}`, null) .then((data) => { if (data.detail) { setError(data.detail); return initialData; } else { setError(""); return data as Promise; } }) .then((data) => setData(data)) .catch(() => setError("no access")); setLoading(false); } else setError("team unknown"); } useEffect(() => { loadData(); }, [teams]); const graphRef = useRef(null); function handleThreed() { setThreed(!threed); graphRef.current?.fitNodesInView(); graphRef.current?.centerGraph(); graphRef.current?.resetControls(); } function handlePopularity() { popularityLabel(!popularity); setPopularity(!popularity); } function handleMutuality() { colorMatches(!mutuality); setMutuality(!mutuality); } function showLabel() { switch (likes) { case 0: return "dislike"; case 1: return "both"; case 2: return "like"; } } function findMatches(edges: GraphEdge[]) { const adjacencyList = edges.map( (edge) => edge.source + edge.target + edge.data.relation ); return edges.filter((edge) => adjacencyList.includes(edge.target + edge.source + edge.data.relation) ); } //const matches = useMemo(() => findMatches(data.edges), []) function colorMatches(mutuality: boolean) { const matches = findMatches(data.edges); const newEdges = data.edges; if (mutuality) { newEdges.forEach((edge) => { if ( (likes === 1 || edge.data.relation === likes) && matches.map((edge) => edge.id).includes(edge.id) ) { edge.fill = "#9c3"; if (edge.size) edge.size = edge.size * 1.5; } }); } else { newEdges.forEach((edge) => { if ( (likes === 1 || edge.data.relation === likes) && matches.map((edge) => edge.id).includes(edge.id) ) { edge.fill = edge.data.origFill; edge.size = edge.data.origSize; } }); } setData({ nodes: data.nodes, edges: newEdges }); } function popularityLabel(popularity: boolean) { const newNodes = data.nodes; console.log(data.nodes); if (popularity) { newNodes.forEach( (node) => (node.subLabel = `pop.: ${node.data.inDegree.toFixed(1)}`) ); } else { newNodes.forEach((node) => (node.subLabel = undefined)); } setData({ nodes: newNodes, edges: data.edges }); } useEffect(() => { if (mutuality) colorMatches(false); colorMatches(mutuality); }, [likes]); const { selections, actives, onNodeClick, onCanvasClick, onNodePointerOver, onNodePointerOut, } = useCustomSelection({ ref: graphRef, nodes: data.nodes, edges: data.edges.filter((edge) => edge.data.relation === likes), ignore: data.edges.map((edge) => { if (likes === 1 && edge.data.relation !== 2) return edge; }), pathSelectionType: "out", pathHoverType: "in", type: "multiModifier", }); let content: ReactNode; if (loading) { content = ; } else if (error) { content = {error}; } else { content = ( <> edge.data.relation === likes || likes === 1 )} selections={selections} actives={actives} onCanvasClick={onCanvasClick} onNodeClick={onNodeClick} onNodePointerOut={onNodePointerOut} onNodePointerOver={onNodePointerOver} /> { event.currentTarget.close(); }} > scroll to zoom

hover: show inbound links
click: show outward links

multi-selection possible
with Ctrl or Shift

drag to pan/rotate
); } return (
{}} />
mutuality
2D
{}} />
3D
setLikes(Number(evt.target.value))} />
{showLabel()}
{}} />
popularity*
{popularity && (
*popularity meassured by rank-weighted in-degree
)} {content}
); };