diff --git a/src/App.css b/src/App.css index c3cf7c7..29c2ae2 100644 --- a/src/App.css +++ b/src/App.css @@ -23,8 +23,27 @@ footer { font-size: x-small; } +.fixed-footer { + position: absolute; + bottom: 4px; + left: 8px; +} + + /*=========Network Controls=========*/ +.infobutton { + position: fixed; + right: 8px; + bottom: 8px; + padding: 0.4em; + border-radius: 1em; + background-color: rgba(0, 0, 0, 0.3); + font-size: medium; + margin-bottom: 16px; + margin-right: 16px; +} + .controls { z-index: 9; position: absolute; diff --git a/src/Footer.tsx b/src/Footer.tsx index ca4bb61..307afee 100644 --- a/src/Footer.tsx +++ b/src/Footer.tsx @@ -1,23 +1,31 @@ import { Link } from "react-router"; export default function Footer() { - return + return ( + + ); } diff --git a/src/Network.tsx b/src/Network.tsx index ef33c3b..07ca2ab 100644 --- a/src/Network.tsx +++ b/src/Network.tsx @@ -1,11 +1,19 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { apiAuth } from "./api"; -import { GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, SelectionProps, SelectionResult, useSelection } from "reagraph"; +import { + GraphCanvas, + GraphCanvasRef, + GraphEdge, + GraphNode, + SelectionProps, + SelectionResult, + useSelection, +} from "reagraph"; import { customTheme } from "./NetworkTheme"; interface NetworkData { - nodes: GraphNode[], - edges: GraphEdge[], + nodes: GraphNode[]; + edges: GraphEdge[]; } interface CustomSelectionProps extends SelectionProps { ignore: (GraphEdge | undefined)[]; @@ -13,11 +21,19 @@ interface CustomSelectionProps extends SelectionProps { 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 -} + 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 = () => { const [data, setData] = useState({ nodes: [], edges: [] } as NetworkData); @@ -26,31 +42,41 @@ export const GraphComponent = () => { const [likes, setLikes] = useState(2); const [popularity, setPopularity] = useState(false); const [mutuality, setMutuality] = useState(false); - const logo = document.getElementById("logo") + + const logo = document.getElementById("logo"); if (logo) { logo.className = "logo networkroute"; } + const footer = document.getElementsByTagName("footer"); + if (footer) { + (footer.item(0) as HTMLElement).className = "fixed-footer"; + } async function loadData() { setLoading(true); await apiAuth("analysis/graph_json", null) - .then(json => json as Promise).then(json => { setData(json) }) + .then((json) => json as Promise) + .then((json) => { + setData(json); + }); setLoading(false); } - useEffect(() => { loadData() }, []) + useEffect(() => { + loadData(); + }, []); const graphRef = useRef(null); function handleThreed() { - setThreed(!threed) + setThreed(!threed); graphRef.current?.fitNodesInView(); graphRef.current?.centerGraph(); graphRef.current?.resetControls(); } function handlePopularity() { - setPopularity(!popularity) + setPopularity(!popularity); } function handleMutuality() { @@ -60,15 +86,22 @@ export const GraphComponent = () => { function showLabel() { switch (likes) { - case 0: return "dislike"; - case 1: return "both"; - case 2: return "like"; + 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 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), []) @@ -76,31 +109,55 @@ export const GraphComponent = () => { 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 } }) + 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 } }) + 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 }) + setData({ nodes: data.nodes, edges: newEdges }); } useEffect(() => { if (mutuality) colorMatches(false); - colorMatches(mutuality) - }, [likes]) + colorMatches(mutuality); + }, [likes]); - const { selections, actives, onNodeClick, onCanvasClick } = useCustomSelection({ + 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', - type: 'multiModifier' + ignore: data.edges.map((edge) => { + if (likes === 1 && edge.data.relation !== 2) return edge; + }), + pathSelectionType: "out", + pathHoverType: "in", + type: "multiModifier", }); - return ( -
-
- +
+
@@ -147,39 +204,82 @@ export const GraphComponent = () => {
- popularity* + + popularity* +
-
- {popularity &&
- *popularity meassured by rank-weighted in-degree -
} + {popularity && ( +
+ + *popularity meassured by rank-weighted in-degree + +
+ )} - { - loading ? : - edge.data.relation === likes || likes === 1)} - selections={selections} - actives={actives} - onCanvasClick={onCanvasClick} - onNodeClick={onNodeClick} - /> - } -
+ {loading ? ( + + ) : ( + 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 +
+
); -} - +};