feat: working WebGL graph with selection/highlighting
This commit is contained in:
		
							
								
								
									
										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',
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user