feat: working WebGL graph with selection/highlighting
This commit is contained in:
parent
13bb965b28
commit
47fd9bd859
17
analysis.py
17
analysis.py
@ -67,25 +67,32 @@ def graph_json():
|
|||||||
subquery, (C.user == subquery.c.user) & (C.time == subquery.c.latest)
|
subquery, (C.user == subquery.c.user) & (C.time == subquery.c.latest)
|
||||||
)
|
)
|
||||||
for c in session.exec(statement2):
|
for c in session.exec(statement2):
|
||||||
for p in c.love:
|
for i, p in enumerate(c.love):
|
||||||
edges.append(
|
edges.append(
|
||||||
{
|
{
|
||||||
"id": f"{c.user}->{p}",
|
"id": f"{c.user}->{p}",
|
||||||
"source": c.user,
|
"source": c.user,
|
||||||
"target": p,
|
"target": p,
|
||||||
"relation": "likes",
|
"size": max(1.0 - 0.1 * i, 0.3),
|
||||||
|
"data": {"relation": 2},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
continue
|
|
||||||
for p in c.hate:
|
for p in c.hate:
|
||||||
edges.append(
|
edges.append(
|
||||||
{
|
{
|
||||||
id: f"{c.user}-x>{p}",
|
"id": f"{c.user}-x>{p}",
|
||||||
"source": c.user,
|
"source": c.user,
|
||||||
"target": p,
|
"target": p,
|
||||||
"relation": "dislikes",
|
"size": 0.3,
|
||||||
|
"data": {"relation": 0},
|
||||||
|
"fill": "#ff7c7c",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
G = nx.DiGraph()
|
||||||
|
G.add_weighted_edges_from([(e["source"], e["target"], e["size"]) for e in edges])
|
||||||
|
in_degrees = G.in_degree(weight="weight")
|
||||||
|
nodes = [dict(node, **{"inDegree": in_degrees[node["id"]]}) for node in nodes]
|
||||||
return JSONResponse({"nodes": nodes, "edges": edges})
|
return JSONResponse({"nodes": nodes, "edges": edges})
|
||||||
|
|
||||||
|
|
||||||
|
35
src/App.css
35
src/App.css
@ -23,28 +23,38 @@ footer {
|
|||||||
font-size: x-small;
|
font-size: x-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*=========Network Controls=========*/
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 24;
|
width: 240px;
|
||||||
left: 24;
|
right: 24px;
|
||||||
|
top: 1vh;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
.control {
|
.control {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
margin: 4px 2px;
|
||||||
|
background-color: aliceblue;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#three-slider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The switch - the box around the slider */
|
/* The switch - the box around the slider */
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 68px;
|
width: 68px;
|
||||||
height: 34px;
|
height: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide default HTML checkbox */
|
/* Hide default HTML checkbox */
|
||||||
@ -239,13 +249,19 @@ button,
|
|||||||
#control-panel {
|
#control-panel {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
@media only screen and (max-width: 768px) {
|
||||||
#control-panel {
|
#control-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.networkroute {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.submit_text {
|
.submit_text {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -312,6 +328,8 @@ button,
|
|||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*======LOGO=======*/
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -339,6 +357,15 @@ button,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.networkroute {
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
left: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*======SPINNER=======*/
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { baseUrl } from "./api";
|
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
return <div className="logo">
|
return <div className="logo" id="logo">
|
||||||
<a href={"/"}>
|
<a href={"/"}>
|
||||||
<img alt="logo" height="66%" src="logo.svg" />
|
<img alt="logo" height="66%" src="logo.svg" />
|
||||||
<h3 className="centered">cutt</h3>
|
<h3 className="centered">cutt</h3>
|
||||||
|
109
src/Network.tsx
109
src/Network.tsx
@ -1,17 +1,31 @@
|
|||||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { apiAuth } from "./api";
|
import { apiAuth } from "./api";
|
||||||
import { GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, recommendLayout, 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 {
|
||||||
|
ignore: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCustomSelection = (props: CustomSelectionProps): SelectionResult => {
|
||||||
|
var result = useSelection(props);
|
||||||
|
result.actives = result.actives.filter((s) => !props.ignore.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);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [threed, setThreed] = useState(false);
|
const [threed, setThreed] = useState(false);
|
||||||
|
const [likes, setLikes] = useState(2);
|
||||||
|
const logo = document.getElementById("logo")
|
||||||
|
if (logo) {
|
||||||
|
logo.className = "logo networkroute";
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -24,12 +38,6 @@ export const GraphComponent = () => {
|
|||||||
|
|
||||||
const graphRef = useRef<GraphCanvasRef | null>(null);
|
const graphRef = useRef<GraphCanvasRef | null>(null);
|
||||||
|
|
||||||
const { selections, actives, onNodeClick, onCanvasClick } = useSelection({
|
|
||||||
ref: graphRef,
|
|
||||||
nodes: data.nodes,
|
|
||||||
edges: data.edges,
|
|
||||||
pathSelectionType: 'out',
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleThreed() {
|
function handleThreed() {
|
||||||
setThreed(!threed)
|
setThreed(!threed)
|
||||||
@ -38,9 +46,28 @@ export const GraphComponent = () => {
|
|||||||
graphRef.current?.resetControls();
|
graphRef.current?.resetControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showLabel() {
|
||||||
|
switch (likes) {
|
||||||
|
case 0: return "dislike";
|
||||||
|
case 1: return "both";
|
||||||
|
case 2: return "like";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { selections, actives, onNodeClick, onCanvasClick } = useCustomSelection({
|
||||||
|
ref: graphRef,
|
||||||
|
nodes: data.nodes,
|
||||||
|
edges: data.edges.filter((edge) => edge.data.relation === likes),
|
||||||
|
ignore: data.edges.map((edge) => { return (likes === 1 && edge.data.relation !== 2) ? edge.id : "" }),
|
||||||
|
pathSelectionType: 'out',
|
||||||
|
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={handleThreed}>
|
<div className="control" onClick={handleThreed}>
|
||||||
<span>2D</span>
|
<span>2D</span>
|
||||||
<div className="switch">
|
<div className="switch">
|
||||||
@ -49,26 +76,56 @@ export const GraphComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
<span>3D</span>
|
<span>3D</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="control">
|
||||||
|
<div className="stack column">
|
||||||
|
<datalist id="markers">
|
||||||
|
<option value="0"></option>
|
||||||
|
<option value="1"></option>
|
||||||
|
<option value="2"></option>
|
||||||
|
</datalist>
|
||||||
|
<div id="three-slider">
|
||||||
|
<label>😬</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
list="markers"
|
||||||
|
min="0"
|
||||||
|
max="2"
|
||||||
|
step="1"
|
||||||
|
width="16px"
|
||||||
|
onChange={(evt) => setLikes(Number(evt.target.value))}
|
||||||
|
/>
|
||||||
|
<label>😍</label>
|
||||||
|
</div>
|
||||||
|
{showLabel()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<GraphCanvas
|
|
||||||
draggable
|
{loading ? <span className="loader" /> :
|
||||||
cameraMode={threed ? "rotate" : "pan"}
|
<GraphCanvas
|
||||||
layoutType={threed ? "forceDirected3d" : "forceDirected2d"}
|
draggable
|
||||||
layoutOverrides={{
|
cameraMode={threed ? "rotate" : "pan"}
|
||||||
linkDistance: 200
|
layoutType={threed ? "forceDirected3d" : "forceDirected2d"}
|
||||||
}}
|
layoutOverrides={{
|
||||||
labelType="auto"
|
nodeStrength: -200,
|
||||||
ref={graphRef}
|
linkDistance: 100
|
||||||
theme={customTheme}
|
}}
|
||||||
nodes={data.nodes}
|
defaultNodeSize={1}
|
||||||
edges={data.edges}
|
labelType="nodes"
|
||||||
selections={selections}
|
sizingType="attribute"
|
||||||
actives={actives}
|
sizingAttribute="inDegree"
|
||||||
onCanvasClick={onCanvasClick}
|
ref={graphRef}
|
||||||
onNodeClick={onNodeClick}
|
theme={customTheme}
|
||||||
/>
|
nodes={data.nodes}
|
||||||
|
edges={data.edges.filter((edge) => edge.data.relation === likes || likes === 1)}
|
||||||
|
selections={selections}
|
||||||
|
actives={actives}
|
||||||
|
onCanvasClick={onCanvasClick}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
/>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ export const customTheme: Theme = {
|
|||||||
activeFill: '#36C',
|
activeFill: '#36C',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
selectedOpacity: 1,
|
selectedOpacity: 1,
|
||||||
inactiveOpacity: 0.2,
|
inactiveOpacity: 0.333,
|
||||||
label: {
|
label: {
|
||||||
stroke: '#fff',
|
stroke: '#fff',
|
||||||
color: '#2A6475',
|
color: '#2A6475',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user