feat: add footer back to Network page
This commit is contained in:
parent
9d65c1d1df
commit
104ec70695
19
src/App.css
19
src/App.css
@ -23,8 +23,27 @@ footer {
|
|||||||
font-size: x-small;
|
font-size: x-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*=========Network Controls=========*/
|
/*=========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 {
|
.controls {
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return <footer>
|
return (
|
||||||
<div className="navbar">
|
<footer>
|
||||||
<Link to="/" ><span>Form</span></Link>
|
<div className="navbar">
|
||||||
<span>|</span>
|
<a href="/">
|
||||||
<Link to="/network" ><span>Trainer Analysis</span></Link>
|
<span>Form</span>
|
||||||
<span>|</span>
|
</a>
|
||||||
<Link to="/mvp" ><span>MVP</span></Link>
|
<span>|</span>
|
||||||
</div>
|
<a href="/network">
|
||||||
<p className="grey extra-margin">
|
<span>Trainer Analysis</span>
|
||||||
something not working?
|
</a>
|
||||||
<br />
|
<span>|</span>
|
||||||
message <a href="https://t.me/x0124816">me</a>.
|
<a href="/mvp">
|
||||||
<br />
|
<span>MVP</span>
|
||||||
or fix it here:{" "}
|
</a>
|
||||||
<a href="https://git.0124816.xyz/julius/cutt" key="gitea">
|
</div>
|
||||||
<img src="gitea.svg" alt="gitea" height="16" />
|
<p className="grey extra-margin">
|
||||||
</a>
|
something not working?
|
||||||
</p>
|
<br />
|
||||||
</footer>
|
message <a href="https://t.me/x0124816">me</a>.
|
||||||
|
<br />
|
||||||
|
or fix it here:{" "}
|
||||||
|
<a href="https://git.0124816.xyz/julius/cutt" key="gitea">
|
||||||
|
<img src="gitea.svg" alt="gitea" height="16" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
226
src/Network.tsx
226
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 { 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";
|
import { customTheme } from "./NetworkTheme";
|
||||||
|
|
||||||
interface NetworkData {
|
interface NetworkData {
|
||||||
nodes: GraphNode[],
|
nodes: GraphNode[];
|
||||||
edges: GraphEdge[],
|
edges: GraphEdge[];
|
||||||
}
|
}
|
||||||
interface CustomSelectionProps extends SelectionProps {
|
interface CustomSelectionProps extends SelectionProps {
|
||||||
ignore: (GraphEdge | undefined)[];
|
ignore: (GraphEdge | undefined)[];
|
||||||
@ -13,11 +21,19 @@ interface CustomSelectionProps extends SelectionProps {
|
|||||||
|
|
||||||
const useCustomSelection = (props: CustomSelectionProps): SelectionResult => {
|
const useCustomSelection = (props: CustomSelectionProps): SelectionResult => {
|
||||||
var result = useSelection(props);
|
var result = useSelection(props);
|
||||||
result.actives = result.actives.filter((s) => !props.ignore.map((edge) => edge?.id).includes(s))
|
result.actives = result.actives.filter(
|
||||||
const ignored_nodes = props.ignore.map((edge) => (edge && result.selections?.includes(edge.source) && !result.selections?.includes(edge.target)) ? edge.target : "")
|
(s) => !props.ignore.map((edge) => edge?.id).includes(s)
|
||||||
result.actives = result.actives.filter((s) => !ignored_nodes.includes(s))
|
);
|
||||||
return result
|
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 = () => {
|
export const GraphComponent = () => {
|
||||||
const [data, setData] = useState({ nodes: [], edges: [] } as NetworkData);
|
const [data, setData] = useState({ nodes: [], edges: [] } as NetworkData);
|
||||||
@ -26,31 +42,41 @@ export const GraphComponent = () => {
|
|||||||
const [likes, setLikes] = useState(2);
|
const [likes, setLikes] = useState(2);
|
||||||
const [popularity, setPopularity] = useState(false);
|
const [popularity, setPopularity] = useState(false);
|
||||||
const [mutuality, setMutuality] = useState(false);
|
const [mutuality, setMutuality] = useState(false);
|
||||||
const logo = document.getElementById("logo")
|
|
||||||
|
const logo = document.getElementById("logo");
|
||||||
if (logo) {
|
if (logo) {
|
||||||
logo.className = "logo networkroute";
|
logo.className = "logo networkroute";
|
||||||
}
|
}
|
||||||
|
const footer = document.getElementsByTagName("footer");
|
||||||
|
if (footer) {
|
||||||
|
(footer.item(0) as HTMLElement).className = "fixed-footer";
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await apiAuth("analysis/graph_json", null)
|
await apiAuth("analysis/graph_json", null)
|
||||||
.then(json => json as Promise<NetworkData>).then(json => { setData(json) })
|
.then((json) => json as Promise<NetworkData>)
|
||||||
|
.then((json) => {
|
||||||
|
setData(json);
|
||||||
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [])
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const graphRef = useRef<GraphCanvasRef | null>(null);
|
const graphRef = useRef<GraphCanvasRef | null>(null);
|
||||||
|
|
||||||
function handleThreed() {
|
function handleThreed() {
|
||||||
setThreed(!threed)
|
setThreed(!threed);
|
||||||
graphRef.current?.fitNodesInView();
|
graphRef.current?.fitNodesInView();
|
||||||
graphRef.current?.centerGraph();
|
graphRef.current?.centerGraph();
|
||||||
graphRef.current?.resetControls();
|
graphRef.current?.resetControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePopularity() {
|
function handlePopularity() {
|
||||||
setPopularity(!popularity)
|
setPopularity(!popularity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMutuality() {
|
function handleMutuality() {
|
||||||
@ -60,15 +86,22 @@ export const GraphComponent = () => {
|
|||||||
|
|
||||||
function showLabel() {
|
function showLabel() {
|
||||||
switch (likes) {
|
switch (likes) {
|
||||||
case 0: return "dislike";
|
case 0:
|
||||||
case 1: return "both";
|
return "dislike";
|
||||||
case 2: return "like";
|
case 1:
|
||||||
|
return "both";
|
||||||
|
case 2:
|
||||||
|
return "like";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMatches(edges: GraphEdge[]) {
|
function findMatches(edges: GraphEdge[]) {
|
||||||
const adjacencyList = edges.map((edge) => edge.source + edge.target + edge.data.relation);
|
const adjacencyList = edges.map(
|
||||||
return edges.filter((edge) => adjacencyList.includes(edge.target + edge.source + edge.data.relation))
|
(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), [])
|
//const matches = useMemo(() => findMatches(data.edges), [])
|
||||||
|
|
||||||
@ -76,31 +109,55 @@ export const GraphComponent = () => {
|
|||||||
const matches = findMatches(data.edges);
|
const matches = findMatches(data.edges);
|
||||||
const newEdges = data.edges;
|
const newEdges = data.edges;
|
||||||
if (mutuality) {
|
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 {
|
} 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(() => {
|
useEffect(() => {
|
||||||
if (mutuality) colorMatches(false);
|
if (mutuality) colorMatches(false);
|
||||||
colorMatches(mutuality)
|
colorMatches(mutuality);
|
||||||
}, [likes])
|
}, [likes]);
|
||||||
|
|
||||||
const { selections, actives, onNodeClick, onCanvasClick } = useCustomSelection({
|
const {
|
||||||
|
selections,
|
||||||
|
actives,
|
||||||
|
onNodeClick,
|
||||||
|
onCanvasClick,
|
||||||
|
onNodePointerOver,
|
||||||
|
onNodePointerOut,
|
||||||
|
} = useCustomSelection({
|
||||||
ref: graphRef,
|
ref: graphRef,
|
||||||
nodes: data.nodes,
|
nodes: data.nodes,
|
||||||
edges: data.edges.filter((edge) => edge.data.relation === likes),
|
edges: data.edges.filter((edge) => edge.data.relation === likes),
|
||||||
ignore: data.edges.map((edge) => { if (likes === 1 && edge.data.relation !== 2) return edge }),
|
ignore: data.edges.map((edge) => {
|
||||||
pathSelectionType: 'out',
|
if (likes === 1 && edge.data.relation !== 2) return edge;
|
||||||
type: 'multiModifier'
|
}),
|
||||||
|
pathSelectionType: "out",
|
||||||
|
pathHoverType: "in",
|
||||||
|
type: "multiModifier",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }}>
|
<div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: 0 }}>
|
||||||
<div className="controls" >
|
<div className="controls">
|
||||||
|
|
||||||
<div className="control" onClick={handleMutuality}>
|
<div className="control" onClick={handleMutuality}>
|
||||||
<div className="switch">
|
<div className="switch">
|
||||||
<input type="checkbox" checked={mutuality} />
|
<input type="checkbox" checked={mutuality} />
|
||||||
@ -147,39 +204,82 @@ export const GraphComponent = () => {
|
|||||||
<input type="checkbox" checked={popularity} />
|
<input type="checkbox" checked={popularity} />
|
||||||
<span className="slider round"></span>
|
<span className="slider round"></span>
|
||||||
</div>
|
</div>
|
||||||
<span>popularity<sup>*</sup></span>
|
<span>
|
||||||
|
popularity<sup>*</sup>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{popularity && <div style={{ position: 'absolute', bottom: 0, right: "10px", zIndex: 10 }}>
|
{popularity && (
|
||||||
<span className="grey" style={{ fontSize: "70%" }}><sup>*</sup>popularity meassured by rank-weighted in-degree</span>
|
<div
|
||||||
</div>}
|
style={{ position: "absolute", bottom: 0, right: "10px", zIndex: 10 }}
|
||||||
|
>
|
||||||
|
<span className="grey" style={{ fontSize: "70%" }}>
|
||||||
|
<sup>*</sup>popularity meassured by rank-weighted in-degree
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{
|
{loading ? (
|
||||||
loading ? <span className="loader" /> :
|
<span className="loader" />
|
||||||
<GraphCanvas
|
) : (
|
||||||
draggable
|
<GraphCanvas
|
||||||
cameraMode={threed ? "rotate" : "pan"}
|
draggable
|
||||||
layoutType={threed ? "forceDirected3d" : "forceDirected2d"}
|
cameraMode={threed ? "rotate" : "pan"}
|
||||||
layoutOverrides={{
|
layoutType={threed ? "forceDirected3d" : "forceDirected2d"}
|
||||||
nodeStrength: -200,
|
layoutOverrides={{
|
||||||
linkDistance: 100
|
nodeStrength: -200,
|
||||||
}}
|
linkDistance: 100,
|
||||||
labelType="nodes"
|
}}
|
||||||
sizingType="attribute"
|
labelType="nodes"
|
||||||
sizingAttribute={popularity ? "inDegree" : undefined}
|
sizingType="attribute"
|
||||||
ref={graphRef}
|
sizingAttribute={popularity ? "inDegree" : undefined}
|
||||||
theme={customTheme}
|
ref={graphRef}
|
||||||
nodes={data.nodes}
|
theme={customTheme}
|
||||||
edges={data.edges.filter((edge) => edge.data.relation === likes || likes === 1)}
|
nodes={data.nodes}
|
||||||
selections={selections}
|
edges={data.edges.filter(
|
||||||
actives={actives}
|
(edge) => edge.data.relation === likes || likes === 1
|
||||||
onCanvasClick={onCanvasClick}
|
)}
|
||||||
onNodeClick={onNodeClick}
|
selections={selections}
|
||||||
/>
|
actives={actives}
|
||||||
}
|
onCanvasClick={onCanvasClick}
|
||||||
</div >
|
onNodeClick={onNodeClick}
|
||||||
|
onNodePointerOut={onNodePointerOut}
|
||||||
|
onNodePointerOver={onNodePointerOver}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="infobutton"
|
||||||
|
onClick={() => {
|
||||||
|
const dialog = document.querySelector("dialog[id='InfoDialog']");
|
||||||
|
(dialog as HTMLDialogElement).showModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
info
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
id="InfoDialog"
|
||||||
|
style={{ textAlign: "left" }}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.currentTarget.close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
scroll to zoom
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<b>hover</b>: show inbound links
|
||||||
|
<br />
|
||||||
|
<b>click</b>: show outward links
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
multi-selection possible
|
||||||
|
<br />
|
||||||
|
with <i>Ctrl</i> or <i>Shift</i>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
drag to pan/rotate
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user