From 2c31a60fcca8800535abcb101fda727541841e75 Mon Sep 17 00:00:00 2001 From: Nikolas Rauscher Date: Tue, 18 Jun 2024 23:30:50 +0200 Subject: [PATCH 1/6] feat(graph vizulalisation): add toolbar Signed-off-by: Nikolas Rauscher --- .../components/Graph/FloatingControlCard.jsx | 298 ++++++++++++++++++ .../src/components/Graph/index_visjs.tsx | 269 +++++++++------- 2 files changed, 460 insertions(+), 107 deletions(-) create mode 100644 Project/frontend/src/components/Graph/FloatingControlCard.jsx diff --git a/Project/frontend/src/components/Graph/FloatingControlCard.jsx b/Project/frontend/src/components/Graph/FloatingControlCard.jsx new file mode 100644 index 0000000..8e27845 --- /dev/null +++ b/Project/frontend/src/components/Graph/FloatingControlCard.jsx @@ -0,0 +1,298 @@ +import React from 'react'; +import { Card, CardContent, FormControl, InputLabel, Select, MenuItem, Slider, Typography, Box } from '@mui/material'; + +const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsChange }) => { + const renderSliders = () => { + switch (layout) { + case 'barnesHut': + return ( + + Gravitational Constant + handlePhysicsChange('gravitationalConstant', value)} + min={-30000} + max={0} + step={1000} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Spring Length + handlePhysicsChange('springLength', value)} + min={50} + max={300} + step={10} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Spring Constant + handlePhysicsChange('springConstant', value)} + min={0.01} + max={0.5} + step={0.01} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Damping + handlePhysicsChange('damping', value)} + min={0.01} + max={1} + step={0.01} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + + ); + case 'forceAtlas2Based': + return ( + + Gravitational Constant + handlePhysicsChange('gravitationalConstant', value)} + min={-200} + max={0} + step={10} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Spring Length + handlePhysicsChange('springLength', value)} + min={50} + max={300} + step={10} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Spring Constant + handlePhysicsChange('springConstant', value)} + min={0.01} + max={0.5} + step={0.01} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Damping + handlePhysicsChange('damping', value)} + min={0.01} + max={1} + step={0.01} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + + ); + case 'hierarchical': + return ( + + Level Separation + handlePhysicsChange('levelSeparation', value)} + min={50} + max={500} + step={10} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Node Spacing + handlePhysicsChange('nodeSpacing', value)} + min={50} + max={200} + step={10} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Tree Spacing + handlePhysicsChange('treeSpacing', value)} + min={50} + max={500} + step={10} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Block Shifting + handlePhysicsChange('blockShifting', value === 1)} + min={0} + max={1} + step={1} + marks={[{ value: 0, label: 'Off' }, { value: 1, label: 'On' }]} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Edge Minimization + handlePhysicsChange('edgeMinimization', value === 1)} + min={0} + max={1} + step={1} + marks={[{ value: 0, label: 'Off' }, { value: 1, label: 'On' }]} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Parent Centralization + handlePhysicsChange('parentCentralization', value === 1)} + min={0} + max={1} + step={1} + marks={[{ value: 0, label: 'Off' }, { value: 1, label: 'On' }]} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Direction + + Sort Method + + Shake Towards + + + ); + case 'repulsion': + return ( + + Node Distance + handlePhysicsChange('nodeDistance', value)} + min={50} + max={500} + step={10} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Central Gravity + handlePhysicsChange('centralGravity', value)} + min={0} + max={1} + step={0.01} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Spring Length + handlePhysicsChange('springLength', value)} + min={50} + max={300} + step={10} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Spring Constant + handlePhysicsChange('springConstant', value)} + min={0.01} + max={0.5} + step={0.01} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + Damping + handlePhysicsChange('damping', value)} + min={0.01} + max={1} + step={0.01} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + + ); + default: + return null; + } + }; + + return ( + + + + Layout + + + {renderSliders()} + Stabilization Iterations + handlePhysicsChange('iterations', value)} + min={0} + max={5000} + step={100} + valueLabelDisplay="auto" + style={{ color: '#fff' }} + /> + + + ); +}; + +export default FloatingControlCard; \ No newline at end of file diff --git a/Project/frontend/src/components/Graph/index_visjs.tsx b/Project/frontend/src/components/Graph/index_visjs.tsx index ac9dbbe..c571de3 100644 --- a/Project/frontend/src/components/Graph/index_visjs.tsx +++ b/Project/frontend/src/components/Graph/index_visjs.tsx @@ -1,11 +1,15 @@ import React, { useEffect, useState, useRef } from 'react'; import { Network } from 'vis-network/standalone/esm/vis-network'; import { useParams } from 'react-router-dom'; +import FloatingControlCard from './FloatingControlCard'; import './index.css'; import { VISUALIZE_API_PATH } from '../../constant'; +import { CircularProgress, Typography, Box } from '@mui/material'; const VisGraph = ({ graphData, options }) => { const containerRef = useRef(null); + const [stabilizationProgress, setStabilizationProgress] = useState(0); + const [stabilizationComplete, setStabilizationComplete] = useState(false); useEffect(() => { if (!graphData) return; @@ -40,25 +44,46 @@ const VisGraph = ({ graphData, options }) => { const network = new Network(containerRef.current, data, options); - network.on('selectNode', function (params) { - network.setSelection({ - nodes: params.nodes, - edges: network.getConnectedEdges(params.nodes[0]), - }); + network.on('stabilizationProgress', function (params) { + const progress = (params.iterations / params.total) * 100; + setStabilizationProgress(progress); + }); + + network.on('stabilizationIterationsDone', function () { + setStabilizationComplete(true); + setStabilizationProgress(100); + }); + + network.on('stabilized', function () { + setStabilizationComplete(true); + setStabilizationProgress(100); }); return () => network.destroy(); }, [graphData, options]); return ( -
+
+ {!stabilizationComplete && ( + + + + Stabilizing... {Math.round(stabilizationProgress)}% + + + )} +
+
); }; @@ -67,6 +92,22 @@ const GraphVisualization = () => { const [graphData, setGraphData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [layout, setLayout] = useState('barnesHut'); + const [physicsOptions, setPhysicsOptions] = useState({ + gravitationalConstant: -20000, + springLength: 100, + springConstant: 0.1, + damping: 0.09, + levelSeparation: 150, + nodeSpacing: 100, + treeSpacing: 200, + blockShifting: true, + edgeMinimization: true, + parentCentralization: true, + direction: 'UD', + sortMethod: 'hubsize', + shakeTowards: 'leaves', + iterations: 1000, // Stabilization iterations + }); useEffect(() => { const fetchGraphData = async () => { @@ -86,6 +127,77 @@ const GraphVisualization = () => { fetchGraphData(); }, [fileId]); + useEffect(() => { + // Update physics options based on the selected layout + switch (layout) { + case 'barnesHut': + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + gravitationalConstant: -20000, + springLength: 100, + springConstant: 0.1, + damping: 0.09, + })); + break; + case 'forceAtlas2Based': + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + gravitationalConstant: -50, + springLength: 100, + springConstant: 0.08, + damping: 0.4, + })); + break; + case 'hierarchicalRepulsion': + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + gravitationalConstant: 0, + springLength: 120, + springConstant: 0, + damping: 0, + })); + break; + case 'repulsion': + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + gravitationalConstant: 0.2, + springLength: 200, + springConstant: 0.05, + damping: 0.09, + })); + break; + case 'hierarchical': + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + levelSeparation: 150, + nodeSpacing: 100, + treeSpacing: 200, + blockShifting: true, + edgeMinimization: true, + parentCentralization: true, + direction: 'UD', + sortMethod: 'hubsize', + shakeTowards: 'leaves', + })); + break; + default: + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + gravitationalConstant: -20000, + springLength: 100, + springConstant: 0.1, + damping: 0.09, + })); + } + }, [layout]); + + const handlePhysicsChange = (name, value) => { + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + [name]: value, + })); + }; + const options = { nodes: { shape: 'dot', @@ -122,85 +234,31 @@ const GraphVisualization = () => { dragView: true, selectConnectedEdges: false, }, - physics: - layout === 'barnesHut' - ? { - enabled: true, - barnesHut: { - gravitationalConstant: -20000, - springLength: 100, - springConstant: 0.1, - }, - stabilization: { - iterations: 2500, - }, - } - : layout === 'forceAtlas2Based' - ? { - enabled: true, - forceAtlas2Based: { - gravitationalConstant: -50, - centralGravity: 0.01, - springConstant: 0.08, - springLength: 100, - damping: 0.4, - }, - stabilization: { - iterations: 2500, - }, - solver: 'forceAtlas2Based', - } - : layout === 'hierarchicalRepulsion' - ? { - enabled: true, - hierarchicalRepulsion: { - nodeDistance: 120, - }, - stabilization: { - iterations: 2500, - }, - } - : layout === 'repulsion' - ? { - enabled: true, - repulsion: { - nodeDistance: 200, - centralGravity: 0.2, - springLength: 50, - springConstant: 0.05, - damping: 0.09, - }, - stabilization: { - iterations: 2500, - }, - } - : layout === 'hierarchical' - ? { - enabled: true, - hierarchical: { - direction: 'UD', // UD, DU, LR, RL - sortMethod: 'hubsize', - }, - stabilization: { - iterations: 2500, - }, - } - : layout === 'grid' - ? { - enabled: false, - layout: { - hierarchical: false, - randomSeed: undefined, - improvedLayout: true, - }, - physics: { - enabled: false, - }, - } - : { - enabled: true, - randomSeed: 2, - }, + physics: { + enabled: true, + barnesHut: layout === 'barnesHut' ? physicsOptions : {}, + forceAtlas2Based: layout === 'forceAtlas2Based' ? physicsOptions : {}, + hierarchicalRepulsion: layout === 'hierarchicalRepulsion' ? { nodeDistance: physicsOptions.springLength } : {}, + repulsion: layout === 'repulsion' ? { nodeDistance: physicsOptions.springLength, centralGravity: physicsOptions.gravitationalConstant, springLength: physicsOptions.springLength, springConstant: physicsOptions.springConstant, damping: physicsOptions.damping } : {}, + solver: layout, + stabilization: { + iterations: physicsOptions.iterations, // Live Anpassung der Stabilisierung + }, + }, + layout: layout === 'hierarchical' ? { + hierarchical: { + direction: physicsOptions.direction, + sortMethod: physicsOptions.sortMethod, + levelSeparation: physicsOptions.levelSeparation, + nodeSpacing: physicsOptions.nodeSpacing, + treeSpacing: physicsOptions.treeSpacing, + blockShifting: physicsOptions.blockShifting, + edgeMinimization: physicsOptions.edgeMinimization, + parentCentralization: physicsOptions.parentCentralization, + shakeTowards: physicsOptions.shakeTowards, + improvedLayout: false // Hier korrekt platziert + } + } : {} }; if (isLoading) { @@ -214,18 +272,15 @@ const GraphVisualization = () => { return (

Graph Visualization

- - -
- ); -}; - -export default GraphVisualization; + + + + ); + }; + + export default GraphVisualization; \ No newline at end of file From 7858ca51f4f3c424e23163c5dd3eae98cc92222a Mon Sep 17 00:00:00 2001 From: Nikolas Rauscher Date: Wed, 19 Jun 2024 02:00:57 +0200 Subject: [PATCH 2/6] fix(graph vizulalisation): correct updating when I change the parameters Signed-off-by: Nikolas Rauscher --- .../components/Graph/FloatingControlCard.jsx | 167 ++++++++++-------- .../src/components/Graph/index_visjs.tsx | 64 ++++--- 2 files changed, 130 insertions(+), 101 deletions(-) diff --git a/Project/frontend/src/components/Graph/FloatingControlCard.jsx b/Project/frontend/src/components/Graph/FloatingControlCard.jsx index 8e27845..d66b185 100644 --- a/Project/frontend/src/components/Graph/FloatingControlCard.jsx +++ b/Project/frontend/src/components/Graph/FloatingControlCard.jsx @@ -1,7 +1,12 @@ import React from 'react'; import { Card, CardContent, FormControl, InputLabel, Select, MenuItem, Slider, Typography, Box } from '@mui/material'; -const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsChange }) => { +const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsChange, restartStabilization }) => { + const handleSliderChange = (name) => (event, value) => { + handlePhysicsChange(name, value); + restartStabilization(); + }; + const renderSliders = () => { switch (layout) { case 'barnesHut': @@ -10,7 +15,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Gravitational Constant handlePhysicsChange('gravitationalConstant', value)} + onChange={handleSliderChange('gravitationalConstant')} min={-30000} max={0} step={1000} @@ -20,7 +25,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Spring Length handlePhysicsChange('springLength', value)} + onChange={handleSliderChange('springLength')} min={50} max={300} step={10} @@ -30,7 +35,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Spring Constant handlePhysicsChange('springConstant', value)} + onChange={handleSliderChange('springConstant')} min={0.01} max={0.5} step={0.01} @@ -40,7 +45,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Damping handlePhysicsChange('damping', value)} + onChange={handleSliderChange('damping')} min={0.01} max={1} step={0.01} @@ -55,7 +60,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Gravitational Constant handlePhysicsChange('gravitationalConstant', value)} + onChange={handleSliderChange('gravitationalConstant')} min={-200} max={0} step={10} @@ -65,7 +70,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Spring Length handlePhysicsChange('springLength', value)} + onChange={handleSliderChange('springLength')} min={50} max={300} step={10} @@ -75,7 +80,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Spring Constant handlePhysicsChange('springConstant', value)} + onChange={handleSliderChange('springConstant')} min={0.01} max={0.5} step={0.01} @@ -85,7 +90,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Damping handlePhysicsChange('damping', value)} + onChange={handleSliderChange('damping')} min={0.01} max={1} step={0.01} @@ -100,7 +105,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Level Separation handlePhysicsChange('levelSeparation', value)} + onChange={handleSliderChange('levelSeparation')} min={50} max={500} step={10} @@ -110,7 +115,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Node Spacing handlePhysicsChange('nodeSpacing', value)} + onChange={handleSliderChange('nodeSpacing')} min={50} max={200} step={10} @@ -120,7 +125,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Tree Spacing handlePhysicsChange('treeSpacing', value)} + onChange={handleSliderChange('treeSpacing')} min={50} max={500} step={10} @@ -130,7 +135,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Block Shifting handlePhysicsChange('blockShifting', value === 1)} + onChange={handleSliderChange('blockShifting')} min={0} max={1} step={1} @@ -141,7 +146,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Edge Minimization handlePhysicsChange('edgeMinimization', value === 1)} + onChange={handleSliderChange('edgeMinimization')} min={0} max={1} step={1} @@ -152,7 +157,7 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Parent Centralization handlePhysicsChange('parentCentralization', value === 1)} + onChange={handleSliderChange('parentCentralization')} min={0} max={1} step={1} @@ -163,7 +168,10 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Direction handlePhysicsChange('sortMethod', e.target.value)} + onChange={(e) => { + handlePhysicsChange('sortMethod', e.target.value); + restartStabilization(); + }} style={{ color: '#fff' }} > Hubsize @@ -183,7 +194,10 @@ const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsC Shake Towards { + setLayout(e.target.value); + restartStabilization(); + }} + style={{ color: '#fff' }} + > + Barnes Hut + Force Atlas 2 Based + Hierarchical Repulsion + Repulsion + Hierarchical + + + {renderSliders()} + Stabilization Iterations + - - ); - default: - return null; - } - }; - - return ( - - - - Layout - - - {renderSliders()} - Stabilization Iterations - handlePhysicsChange('iterations', value)} - min={0} - max={5000} - step={100} - valueLabelDisplay="auto" - style={{ color: '#fff' }} - /> - - - ); -}; - -export default FloatingControlCard; \ No newline at end of file + + + ); + }; + + export default FloatingControlCard; \ No newline at end of file diff --git a/Project/frontend/src/components/Graph/index_visjs.tsx b/Project/frontend/src/components/Graph/index_visjs.tsx index c571de3..02bf4a7 100644 --- a/Project/frontend/src/components/Graph/index_visjs.tsx +++ b/Project/frontend/src/components/Graph/index_visjs.tsx @@ -6,10 +6,10 @@ import './index.css'; import { VISUALIZE_API_PATH } from '../../constant'; import { CircularProgress, Typography, Box } from '@mui/material'; -const VisGraph = ({ graphData, options }) => { +const VisGraph = ({ graphData, options, setStabilizationComplete }) => { const containerRef = useRef(null); + const networkRef = useRef(null); const [stabilizationProgress, setStabilizationProgress] = useState(0); - const [stabilizationComplete, setStabilizationComplete] = useState(false); useEffect(() => { if (!graphData) return; @@ -42,7 +42,12 @@ const VisGraph = ({ graphData, options }) => { })), }; + if (networkRef.current) { + networkRef.current.destroy(); + } + const network = new Network(containerRef.current, data, options); + networkRef.current = network; network.on('stabilizationProgress', function (params) { const progress = (params.iterations / params.total) * 100; @@ -50,13 +55,13 @@ const VisGraph = ({ graphData, options }) => { }); network.on('stabilizationIterationsDone', function () { - setStabilizationComplete(true); setStabilizationProgress(100); + setStabilizationComplete(true); }); network.on('stabilized', function () { - setStabilizationComplete(true); setStabilizationProgress(100); + setStabilizationComplete(true); }); return () => network.destroy(); @@ -64,7 +69,7 @@ const VisGraph = ({ graphData, options }) => { return (
- {!stabilizationComplete && ( + {stabilizationProgress < 100 && ( { const [graphData, setGraphData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [layout, setLayout] = useState('barnesHut'); + const [stabilizationComplete, setStabilizationComplete] = useState(false); const [physicsOptions, setPhysicsOptions] = useState({ gravitationalConstant: -20000, springLength: 100, @@ -109,21 +115,21 @@ const GraphVisualization = () => { iterations: 1000, // Stabilization iterations }); - useEffect(() => { - const fetchGraphData = async () => { - try { - const response = await fetch( - `${import.meta.env.VITE_BACKEND_HOST}${VISUALIZE_API_PATH.replace(':fileId', fileId)}`, - ); - const data = await response.json(); - setGraphData(data); - } catch (error) { - console.error('Error fetching graph data:', error); - } finally { - setIsLoading(false); - } - }; + const fetchGraphData = async () => { + try { + const response = await fetch( + `${import.meta.env.VITE_BACKEND_HOST}${VISUALIZE_API_PATH.replace(':fileId', fileId)}`, + ); + const data = await response.json(); + setGraphData(data); + } catch (error) { + console.error('Error fetching graph data:', error); + } finally { + setIsLoading(false); + } + }; + useEffect(() => { fetchGraphData(); }, [fileId]); @@ -196,6 +202,7 @@ const GraphVisualization = () => { ...prevOptions, [name]: value, })); + setStabilizationComplete(false); }; const options = { @@ -277,10 +284,15 @@ const GraphVisualization = () => { setLayout={setLayout} physicsOptions={physicsOptions} handlePhysicsChange={handlePhysicsChange} - /> - - - ); - }; - - export default GraphVisualization; \ No newline at end of file + restartStabilization={() => setStabilizationComplete(false)} + /> + + + ); +}; + +export default GraphVisualization; \ No newline at end of file From d515366643296118d2c9dee00a30c818118ba7c7 Mon Sep 17 00:00:00 2001 From: Nikolas Rauscher Date: Sat, 22 Jun 2024 19:30:00 +0200 Subject: [PATCH 3/6] feat(graph vizulalisation): Improve tool panel and added other visualization varianten Signed-off-by: Nikolas Rauscher --- .../frontend/src/components/Graph/D3Graph.jsx | 145 +++++++++++++++ .../Graph/FloatingControlCard_d3.jsx | 35 ++++ .../Graph/FloatingControlCard_sigma.jsx | 113 ++++++++++++ .../src/components/Graph/index_sigma.tsx | 23 +-- .../src/components/Graph/index_visjs.tsx | 68 +++++-- .../src/components/Graph/index_visjs_d3.tsx | 68 +++++++ .../components/Graph/index_visjs_sigma.tsx | 167 ++++++++++++++++++ 7 files changed, 592 insertions(+), 27 deletions(-) create mode 100644 Project/frontend/src/components/Graph/D3Graph.jsx create mode 100644 Project/frontend/src/components/Graph/FloatingControlCard_d3.jsx create mode 100644 Project/frontend/src/components/Graph/FloatingControlCard_sigma.jsx create mode 100644 Project/frontend/src/components/Graph/index_visjs_d3.tsx create mode 100644 Project/frontend/src/components/Graph/index_visjs_sigma.tsx diff --git a/Project/frontend/src/components/Graph/D3Graph.jsx b/Project/frontend/src/components/Graph/D3Graph.jsx new file mode 100644 index 0000000..c0e14a7 --- /dev/null +++ b/Project/frontend/src/components/Graph/D3Graph.jsx @@ -0,0 +1,145 @@ +import React, { useEffect, useRef } from 'react'; +import * as d3 from 'd3'; + +const D3Graph = ({ graphData, layout }) => { + const containerRef = useRef(null); + + useEffect(() => { + if (!graphData) return; + + // Set up SVG dimensions + const width = window.innerWidth; + const height = window.innerHeight - 50; + + // Clear previous graph + d3.select(containerRef.current).select('svg').remove(); + + // Create SVG element + const svg = d3.select(containerRef.current) + .append('svg') + .attr('width', width) + .attr('height', height) + .call(d3.zoom().on('zoom', (event) => { + svg.attr('transform', event.transform); + })) + .append('g'); + + // Set up the simulation + const simulation = d3.forceSimulation(graphData.nodes) + .force('link', d3.forceLink(graphData.edges).id(d => d.id).distance(100)) + .force('charge', d3.forceManyBody().strength(-300)) + .force('center', d3.forceCenter(width / 2, height / 2)); + + // Apply different layout algorithms + if (layout === 'hierarchical') { + simulation.force('y', d3.forceY().strength(0.1)); + simulation.force('x', d3.forceX().strength(0.1)); + } + + // Create links + const link = svg.append('g') + .attr('class', 'links') + .selectAll('line') + .data(graphData.edges) + .enter() + .append('line') + .attr('stroke-width', 2) + .attr('stroke', '#fff'); + + // Create link labels + const linkLabels = svg.append('g') + .attr('class', 'link-labels') + .selectAll('text') + .data(graphData.edges) + .enter() + .append('text') + .attr('class', 'link-label') + .attr('dx', 15) + .attr('dy', '.35em') + .text(d => d.label) // Correctly reading the label property for edges + .attr('fill', '#fff'); + + // Create nodes + const node = svg.append('g') + .attr('class', 'nodes') + .selectAll('circle') + .data(graphData.nodes) + .enter() + .append('circle') + .attr('r', 25) + .attr('fill', '#69b3a2') + .attr('stroke', '#508e7f') + .call(d3.drag() + .on('start', dragstarted) + .on('drag', dragged) + .on('end', dragended)); + + // Node labels + const nodeLabels = svg.append('g') + .attr('class', 'node-labels') + .selectAll('text') + .data(graphData.nodes) + .enter() + .append('text') + .attr('class', 'node-label') + .attr('dx', 15) + .attr('dy', '.35em') + .text(d => d.label) // Correctly reading the label property for nodes + .attr('fill', '#fff'); + + // Update simulation + simulation + .nodes(graphData.nodes) + .on('tick', ticked); + + simulation.force('link') + .links(graphData.edges); + + function ticked() { + link + .attr('x1', d => d.source.x) + .attr('y1', d => d.source.y) + .attr('x2', d => d.target.x) + .attr('y2', d => d.target.y); + + node + .attr('cx', d => d.x) + .attr('cy', d => d.y); + + nodeLabels + .attr('x', d => d.x) + .attr('y', d => d.y); + + linkLabels + .attr('x', d => (d.source.x + d.target.x) / 2) + .attr('y', d => (d.source.y + d.target.y) / 2); + } + + function dragstarted(event, d) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + function dragended(event, d) { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + // Stabilize nodes after a certain time + setTimeout(() => { + simulation.alphaTarget(0).restart(); + }, 5000); // 5 seconds stabilization time + + }, [graphData, layout]); + + return
; +}; + +export default D3Graph; \ No newline at end of file diff --git a/Project/frontend/src/components/Graph/FloatingControlCard_d3.jsx b/Project/frontend/src/components/Graph/FloatingControlCard_d3.jsx new file mode 100644 index 0000000..901b2db --- /dev/null +++ b/Project/frontend/src/components/Graph/FloatingControlCard_d3.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Card, CardContent, FormControl, InputLabel, Select, MenuItem, Box } from '@mui/material'; + +const FloatingControlCard = ({ layout, setLayout }) => { + return ( + + + + Layout + + + + + ); +}; + +export default FloatingControlCard; \ No newline at end of file diff --git a/Project/frontend/src/components/Graph/FloatingControlCard_sigma.jsx b/Project/frontend/src/components/Graph/FloatingControlCard_sigma.jsx new file mode 100644 index 0000000..628a9f0 --- /dev/null +++ b/Project/frontend/src/components/Graph/FloatingControlCard_sigma.jsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { Card, CardContent, FormControl, InputLabel, Select, MenuItem, Slider, Typography, Box } from '@mui/material'; + +const FloatingControlCard = ({ layout, setLayout, physicsOptions, handlePhysicsChange }) => { + const handleSliderChange = (name) => (event, value) => { + handlePhysicsChange(name, value); + }; + + const renderSliders = () => { + return ( + + Iterations + + Barnes Hut Theta + + Gravity + + Scaling Ratio + + Edge Weight Influence + + Edge Length + + + ); + }; + + return ( + + + + Layout + + + {renderSliders()} + + + ); +}; + +export default FloatingControlCard; \ No newline at end of file diff --git a/Project/frontend/src/components/Graph/index_sigma.tsx b/Project/frontend/src/components/Graph/index_sigma.tsx index 01399be..6a081b5 100644 --- a/Project/frontend/src/components/Graph/index_sigma.tsx +++ b/Project/frontend/src/components/Graph/index_sigma.tsx @@ -3,10 +3,7 @@ import { MultiDirectedGraph } from 'graphology'; import { SigmaContainer, useSigma } from '@react-sigma/core'; import { useParams } from 'react-router-dom'; import '@react-sigma/core/lib/react-sigma.min.css'; -import EdgeCurveProgram, { - DEFAULT_EDGE_CURVATURE, - indexParallelEdgesIndex, -} from '@sigma/edge-curve'; +import EdgeCurveProgram, { DEFAULT_EDGE_CURVATURE, indexParallelEdgesIndex } from '@sigma/edge-curve'; import { EdgeArrowProgram } from 'sigma/rendering'; import forceAtlas2 from 'graphology-layout-forceatlas2'; import './index.css'; @@ -44,8 +41,8 @@ const ForceAtlas2Layout = ({ maxIterations }) => { return null; }; -export default function Graph() { - const [graphData, setGraphData] = useState(null); +export default function GraphVisualization() { + const [graphData, setGraphData] = useState(null); const { fileId = '' } = useParams(); const [isLoading, setIsLoading] = useState(true); @@ -56,10 +53,7 @@ export default function Graph() { .then((graphData) => { const graph = new MultiDirectedGraph(); graphData?.nodes?.forEach( - (node: { - id: string; - [key: string]: string | number | boolean | null; - }) => { + (node) => { const { id, ...rest } = node; graph.addNode(id, { ...rest, @@ -70,10 +64,7 @@ export default function Graph() { }, ); graphData?.edges?.forEach( - (edge: { - id: string; - [key: string]: string | number | boolean | null; - }) => { + (edge) => { const { id, source, target, ...rest } = edge; graph.addEdgeWithKey(id, source, target, { ...rest, @@ -113,7 +104,7 @@ export default function Graph() { } if (!graphData) { return ( -
Sorry error has been occurred!
+
Sorry, an error has occurred!
); } return ( @@ -141,4 +132,4 @@ export default function Graph() { ); -} +} \ No newline at end of file diff --git a/Project/frontend/src/components/Graph/index_visjs.tsx b/Project/frontend/src/components/Graph/index_visjs.tsx index 02bf4a7..82f7479 100644 --- a/Project/frontend/src/components/Graph/index_visjs.tsx +++ b/Project/frontend/src/components/Graph/index_visjs.tsx @@ -10,6 +10,8 @@ const VisGraph = ({ graphData, options, setStabilizationComplete }) => { const containerRef = useRef(null); const networkRef = useRef(null); const [stabilizationProgress, setStabilizationProgress] = useState(0); + const [isStabilizing, setIsStabilizing] = useState(false); + const isStabilizingRef = useRef(false); useEffect(() => { if (!graphData) return; @@ -49,6 +51,10 @@ const VisGraph = ({ graphData, options, setStabilizationComplete }) => { const network = new Network(containerRef.current, data, options); networkRef.current = network; + setIsStabilizing(true); + setStabilizationProgress(0); + isStabilizingRef.current = true; + network.on('stabilizationProgress', function (params) { const progress = (params.iterations / params.total) * 100; setStabilizationProgress(progress); @@ -56,20 +62,29 @@ const VisGraph = ({ graphData, options, setStabilizationComplete }) => { network.on('stabilizationIterationsDone', function () { setStabilizationProgress(100); + setIsStabilizing(false); setStabilizationComplete(true); + isStabilizingRef.current = false; }); network.on('stabilized', function () { - setStabilizationProgress(100); - setStabilizationComplete(true); + if (isStabilizingRef.current) { + setStabilizationProgress(100); + setIsStabilizing(false); + setStabilizationComplete(true); + isStabilizingRef.current = false; + } }); - return () => network.destroy(); + return () => { + network.destroy(); + networkRef.current = null; + }; }, [graphData, options]); return (
- {stabilizationProgress < 100 && ( + {isStabilizing && ( { }, physics: { enabled: true, - barnesHut: layout === 'barnesHut' ? physicsOptions : {}, - forceAtlas2Based: layout === 'forceAtlas2Based' ? physicsOptions : {}, - hierarchicalRepulsion: layout === 'hierarchicalRepulsion' ? { nodeDistance: physicsOptions.springLength } : {}, - repulsion: layout === 'repulsion' ? { nodeDistance: physicsOptions.springLength, centralGravity: physicsOptions.gravitationalConstant, springLength: physicsOptions.springLength, springConstant: physicsOptions.springConstant, damping: physicsOptions.damping } : {}, - solver: layout, + barnesHut: layout === 'barnesHut' ? { + gravitationalConstant: physicsOptions.gravitationalConstant, + centralGravity: 0.3, + springLength: physicsOptions.springLength, + springConstant: physicsOptions.springConstant, + damping: physicsOptions.damping, + avoidOverlap: 0.5, + } : undefined, + forceAtlas2Based: layout === 'forceAtlas2Based' ? { + gravitationalConstant: physicsOptions.gravitationalConstant, + centralGravity: 0.01, + springConstant: physicsOptions.springConstant, + springLength: physicsOptions.springLength, + damping: physicsOptions.damping, + } : undefined, + repulsion: layout === 'repulsion' ? { + nodeDistance: physicsOptions.nodeDistance, + centralGravity: physicsOptions.centralGravity, + springLength: physicsOptions.springLength, + springConstant: physicsOptions.springConstant, + damping: physicsOptions.damping, + } : undefined, + hierarchicalRepulsion: layout === 'hierarchicalRepulsion' ? { + nodeDistance: physicsOptions.nodeDistance, + centralGravity: 0.0, + springLength: physicsOptions.springLength, + springConstant: physicsOptions.springConstant, + damping: physicsOptions.damping, + } : undefined, + solver: layout === 'hierarchical' ? 'hierarchicalRepulsion' : layout, stabilization: { - iterations: physicsOptions.iterations, // Live Anpassung der Stabilisierung + enabled: true, + iterations: physicsOptions.iterations, + updateInterval: 50, + onlyDynamicEdges: false, + fit: true, + adaptiveTimestep: true, }, + timestep: 0.5, }, layout: layout === 'hierarchical' ? { hierarchical: { @@ -263,7 +309,7 @@ const GraphVisualization = () => { edgeMinimization: physicsOptions.edgeMinimization, parentCentralization: physicsOptions.parentCentralization, shakeTowards: physicsOptions.shakeTowards, - improvedLayout: false // Hier korrekt platziert + improvedLayout: false, } } : {} }; diff --git a/Project/frontend/src/components/Graph/index_visjs_d3.tsx b/Project/frontend/src/components/Graph/index_visjs_d3.tsx new file mode 100644 index 0000000..0a0e5f9 --- /dev/null +++ b/Project/frontend/src/components/Graph/index_visjs_d3.tsx @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import FloatingControlCard from './FloatingControlCard'; +import './index.css'; +import { VISUALIZE_API_PATH } from '../../constant'; +import D3Graph from './D3Graph'; +import { CircularProgress, Typography, Box } from '@mui/material'; + +const GraphVisualization = () => { + const { fileId = '' } = useParams(); + const [graphData, setGraphData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [layout, setLayout] = useState('barnesHut'); + const [physicsOptions, setPhysicsOptions] = useState({ + gravitationalConstant: -20000, + springLength: 100, + springConstant: 0.1, + damping: 0.09, + }); + + const fetchGraphData = async () => { + try { + const response = await fetch( + `${import.meta.env.VITE_BACKEND_HOST}${VISUALIZE_API_PATH.replace(':fileId', fileId)}`, + ); + const data = await response.json(); + setGraphData(data); + } catch (error) { + console.error('Error fetching graph data:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchGraphData(); + }, [fileId]); + + const handlePhysicsChange = (name, value) => { + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + [name]: value, + })); + }; + + if (isLoading) { + return
Loading graph...
; + } + + if (!graphData) { + return
Sorry, an error has occurred!
; + } + + return ( +
+

Graph Visualization

+ + +
+ ); +}; + +export default GraphVisualization; \ No newline at end of file diff --git a/Project/frontend/src/components/Graph/index_visjs_sigma.tsx b/Project/frontend/src/components/Graph/index_visjs_sigma.tsx new file mode 100644 index 0000000..fc0821d --- /dev/null +++ b/Project/frontend/src/components/Graph/index_visjs_sigma.tsx @@ -0,0 +1,167 @@ +import { useEffect, useState } from 'react'; +import { MultiDirectedGraph } from 'graphology'; +import { SigmaContainer, useSigma } from '@react-sigma/core'; +import { useParams } from 'react-router-dom'; +import '@react-sigma/core/lib/react-sigma.min.css'; +import EdgeCurveProgram, { DEFAULT_EDGE_CURVATURE, indexParallelEdgesIndex } from '@sigma/edge-curve'; +import { EdgeArrowProgram } from 'sigma/rendering'; +import forceAtlas2 from 'graphology-layout-forceatlas2'; +import FloatingControlCard from './FloatingControlCard_sigma'; +import './index.css'; +import { VISUALIZE_API_PATH } from '../../constant'; + +const ForceAtlas2Layout = ({ settings, onIteration, restart }) => { + const sigma = useSigma(); + const graph = sigma.getGraph(); + const [iterations, setIterations] = useState(0); + + useEffect(() => { + if (restart) { + setIterations(0); + } + + const applyLayout = () => { + forceAtlas2.assign(graph, { ...settings, adjustSizes: true }); + setIterations((prev) => { + const newIteration = prev + 1; + onIteration(newIteration); + return newIteration; + }); + }; + + if (iterations < settings.iterations) { + const interval = setInterval(applyLayout, 10); // Reduce interval for faster calculations + return () => clearInterval(interval); + } + }, [graph, iterations, settings, onIteration, restart]); + + return null; +}; + +export default function GraphVisualization() { + const [graphData, setGraphData] = useState(null); + const { fileId = '' } = useParams(); + const [isLoading, setIsLoading] = useState(true); + const [layout, setLayout] = useState('forceAtlas2Based'); + const [physicsOptions, setPhysicsOptions] = useState({ + iterations: 200, + barnesHutOptimize: true, + barnesHutTheta: 0.5, + slowDown: 0.1, // Faster calculations + gravity: 5, // Stronger gravity + scalingRatio: 10, + edgeWeightInfluence: 1, + strongGravityMode: true, + adjustSizes: true, + edgeLength: 100, // Added for edge length + }); + const [restart, setRestart] = useState(false); + + useEffect(() => { + const API = `${import.meta.env.VITE_BACKEND_HOST}${VISUALIZE_API_PATH.replace(':fileId', fileId)}`; + fetch(API) + .then((res) => res.json()) + .then((graphData) => { + const graph = new MultiDirectedGraph(); + graphData?.nodes?.forEach( + (node) => { + const { id, ...rest } = node; + graph.addNode(id, { + ...rest, + size: 15, // just for testing, i am making all the same size + x: Math.random() * 1000, + y: Math.random() * 1000, + }); + }, + ); + graphData?.edges?.forEach( + (edge) => { + const { id, source, target, ...rest } = edge; + graph.addEdgeWithKey(id, source, target, { + ...rest, + size: 2, // edge + length: physicsOptions.edgeLength, // Set edge length + }); + }, + ); + indexParallelEdgesIndex(graph, { + edgeIndexAttribute: 'parallelIndex', + edgeMaxIndexAttribute: 'parallelMaxIndex', + }); + graph.forEachEdge((edge, { parallelIndex, parallelMaxIndex }) => { + if (typeof parallelIndex === 'number') { + graph.mergeEdgeAttributes(edge, { + type: 'curved', + curvature: + DEFAULT_EDGE_CURVATURE + + (3 * DEFAULT_EDGE_CURVATURE * parallelIndex) / + (parallelMaxIndex || 1), + }); + } else { + graph.setEdgeAttribute(edge, 'type', 'straight'); + } + }); + setGraphData(graph); + }) + .catch((error) => { + console.log('Error fetching graphData:', error); + }) + .finally(() => { + setIsLoading(false); + }); + }, [fileId, physicsOptions.edgeLength]); // Re-fetch data when edge length changes + + const handlePhysicsChange = (name, value) => { + setPhysicsOptions((prevOptions) => ({ + ...prevOptions, + [name]: value, + })); + setRestart(true); + }; + + useEffect(() => { + if (restart) { + setRestart(false); + } + }, [restart]); + + if (isLoading) { + return
Loading graph...
; + } + if (!graphData) { + return ( +
Sorry, an error has occurred!
+ ); + } + return ( +
+

Graph Visualization

+ + + console.log(`Iteration: ${iteration}`)} restart={restart} /> + +
+ ); +} \ No newline at end of file From 676ca9e3097e8f26dc4f049d1f9467220737ac59 Mon Sep 17 00:00:00 2001 From: Nikolas Rauscher Date: Wed, 26 Jun 2024 10:46:45 +0200 Subject: [PATCH 4/6] split view in graph visualization page with placeholders Signed-off-by: Irem Ozseker Signed-off-by: Nikolas Rauscher --- .../frontend/src/components/Graph/index.css | 35 +++++++- .../src/components/Graph/index_visjs.tsx | 87 ++++++++++++++++--- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/Project/frontend/src/components/Graph/index.css b/Project/frontend/src/components/Graph/index.css index ec3e66e..b03cf16 100644 --- a/Project/frontend/src/components/Graph/index.css +++ b/Project/frontend/src/components/Graph/index.css @@ -1,4 +1,11 @@ -.graph_container { +.container { + display: flex; + height: 100vh; + width: 100vw; +} + +.main_graph_container { + flex: 1; display: flex; flex-direction: column; gap: 50px; @@ -10,6 +17,32 @@ /* overflow: hidden; */ } +.graph_container { + flex: 1; + display: flex; + flex-direction: row; + gap: 50px; + text-align: center; + justify-content: flex-start; + height: 100vh; + width: 100vw; + /* overflow: hidden; */ +} + +.graph_info { + width: 25%; + padding: 20px; + overflow-y: auto; +} + +.graph_info h1 { + margin-bottom: 30px; /* Adds space between the heading and the file ID */ +} + +.graph_info p { + margin-bottom: 50px; +} + .sigma_container { background: var(--mui-palette-primary-main); } diff --git a/Project/frontend/src/components/Graph/index_visjs.tsx b/Project/frontend/src/components/Graph/index_visjs.tsx index 82f7479..836f38b 100644 --- a/Project/frontend/src/components/Graph/index_visjs.tsx +++ b/Project/frontend/src/components/Graph/index_visjs.tsx @@ -314,6 +314,35 @@ const GraphVisualization = () => { } : {} }; + const searchGraph = (event) => { + if (event.key === 'Enter') { + // Perform search logic based on searchQuery + } + }; + + const inputStyle = { + borderRadius: '8px', + padding: '8px', + width: '100%', + marginBottom: '10px', + border: '1px solid #ccc', + boxSizing: 'border-box', + fontSize: '16px', + fontFamily: 'Arial, sans-serif', + }; + + const textareaStyle = { + borderRadius: '8px', + padding: '8px', + width: '100%', + minHeight: '200px', + border: '1px solid #ccc', + boxSizing: 'border-box', + fontSize: '16px', + fontFamily: 'Arial, sans-serif', + resize: 'none', + }; + if (isLoading) { return
Loading graph...
; } @@ -323,20 +352,52 @@ const GraphVisualization = () => { } return ( -
+

Graph Visualization

- setStabilizationComplete(false)} - /> - + +
+
+

Graph Information

+

+ File ID:
{fileId} +

+ Created at:
xx.xx.xxxx +

+ +