Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions backend/app/routers/cytoscape.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Updated FastAPI router
from pydantic import BaseModel
from sqlalchemy import select
from pathlib import Path
import json

from fastapi import APIRouter, Depends, HTTPException, Path as FastAPIPath
from ..services.cytoscape_service import CytoscapeService
Expand All @@ -21,7 +23,9 @@ async def cytoscape_visualization(
current_user=Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):

"""
Generate graph data and return it as JSON for client-side Cytoscape loading
"""
try:
from ..models import Annotation
result = await db.execute(
Expand All @@ -34,10 +38,16 @@ async def cytoscape_visualization(

graph_dir = Path(f"./data/processed/{set_id}")
cytoscape_service = CytoscapeService(db)
result = await cytoscape_service.load_graph_into_cytoscape(annotation, graph_dir, req.graph_name)

# Generate graph data instead of loading directly
result = await cytoscape_service.generate_graph_data(annotation, graph_dir, req.graph_name)

if result["success"]:
return {"message": result["message"]}
return {
"message": "Graph data generated successfully",
"graph_data": result["graph_data"],
"graph_name": req.graph_name
}
else:
raise HTTPException(status_code=500, detail=result["message"])

Expand Down
29 changes: 15 additions & 14 deletions backend/app/services/cytoscape_service.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
from pathlib import Path
from pyBiodatafuse.graph import cytoscape
from py4cytoscape import cytoscape_ping

from sqlalchemy.ext.asyncio import AsyncSession
from .graph_service import GraphService
from .. import models


class CytoscapeService:
def __init__(self, db: AsyncSession):
self.db = db

async def load_graph_into_cytoscape(self, annotations: models.Annotation, graph_dir: Path, graph_name: str):
"""
Generate graph data in Cytoscape format for client-side loading
"""
try:
if cytoscape_ping() != "You are connected to Cytoscape!":
return {
"success": False,
"message": "Cytoscape is not running or REST API is unreachable. Please ensure Cytoscape desktop is open."
}

pygraph, error = GraphService.create_pygraph(annotations, graph_dir)
if error:
return {"success": False, "message": error}

cytoscape.load_graph(pygraph, network_name=graph_name)
return {"success": True, "message": f"Graph loaded into Cytoscape as '{graph_name}'."}

# Use pyBiodatafuse's convert_graph_to_json function
cytoscape_data = cytoscape.convert_graph_to_json(pygraph)

return {
"success": True,
"message": f"Graph data generated for '{graph_name}'",
"graph_data": cytoscape_data
}

except Exception as e:
return {"success": False, "message": f"Error loading graph into Cytoscape: {str(e)}"}
return {"success": False, "message": f"Error generating graph data: {str(e)}"}
150 changes: 142 additions & 8 deletions vue-app/src/views/CytoscapeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,27 @@
<div class="p-6 text-gray-700">
<p class="text-xl text-gray-600 mb-6">
<strong>Instructions:</strong><br><br>
• Ensure <strong>Cytoscape Desktop</strong> is installed and running.<br>
• Ensure <strong>Cytoscape Desktop</strong> is installed and running on your local machine.<br>
• The <strong>REST API</strong> must be enabled (default setting).<br>
• Complete the query building step before visualization.<br>
• A graph with no edges will result in an error.
</p>

<!-- Cytoscape Status Check -->
<div class="mb-6 p-4 rounded-lg" :class="cytoscapeStatus.class">
<div class="flex items-center">
<span class="mr-2">{{ cytoscapeStatus.icon }}</span>
<span class="font-medium">{{ cytoscapeStatus.message }}</span>
<button
@click="checkCytoscapeConnection"
class="ml-auto px-3 py-1 text-sm bg-white bg-opacity-20 rounded hover:bg-opacity-30"
:disabled="checkingConnection"
>
{{ checkingConnection ? 'Checking...' : 'Recheck' }}
</button>
</div>
</div>

<!-- Graph Name Input -->
<div class="mb-6">
<label class="block text-gray-700 font-medium mb-2">Custom Graph Name</label>
Expand All @@ -51,11 +66,11 @@

<button
@click="loadCytoscapeGraph"
:disabled="loading"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white font-semibold rounded-lg shadow hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-400"
:disabled="loading || !cytoscapeConnected"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white font-semibold rounded-lg shadow hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-400 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span v-if="loading" class="animate-spin mr-2">🔄</span>
<span>{{ loading ? 'Loading...' : 'Load your graph into Cytoscape' }}</span>
<span>{{ getLoadButtonText() }}</span>
</button>
</div>
</div>
Expand All @@ -65,16 +80,70 @@
</template>

<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'

const router = useRouter()
const statusMessage = ref('')
const errorMessage = ref('')
const loading = ref(false)
const checkingConnection = ref(false)
const cytoscapeConnected = ref(false)
const graphName = ref('')
const identifierSetId = localStorage.getItem('currentIdentifierSetId')
const cytoscapeBaseUrl = 'http://localhost:1234'

const cytoscapeStatus = ref({
message: 'Checking Cytoscape connection...',
icon: '🔄',
class: 'bg-yellow-100 text-yellow-800'
})

const checkCytoscapeConnection = async () => {
checkingConnection.value = true

// Try multiple possible endpoints
const endpoints = [
'/v1',
'/v1/version',
'/version',
''
]

for (const endpoint of endpoints) {
try {
const response = await fetch(`${cytoscapeBaseUrl}${endpoint}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
})

if (response.ok) {
cytoscapeConnected.value = true
cytoscapeStatus.value = {
message: `Cytoscape is running and ready! (endpoint: ${endpoint || '/'})`,
icon: '✅',
class: 'bg-green-100 text-green-800'
}
checkingConnection.value = false
return
}
} catch (error) {
continue
}
}

// If we get here, none of the endpoints worked
cytoscapeConnected.value = false
cytoscapeStatus.value = {
message: 'Cytoscape not detected. Please start Cytoscape Desktop and ensure CyREST app is installed.',
icon: '❌',
class: 'bg-red-100 text-red-800'
}
checkingConnection.value = false
}

const loadCytoscapeGraph = async () => {
statusMessage.value = ''
Expand All @@ -90,20 +159,85 @@ const loadCytoscapeGraph = async () => {
return
}

if (!cytoscapeConnected.value) {
errorMessage.value = 'Cytoscape is not running. Please start Cytoscape Desktop and try again.'
return
}

loading.value = true

try {
// Step 1: Get graph data from your FastAPI server
statusMessage.value = 'Generating graph data...'
const response = await axios.post(`/api/visualize&analysis/cytoscape/${identifierSetId}`, {
graph_name: graphName.value.trim()
})
statusMessage.value = response.data.message

const graphData = response.data.graph_data
const networkName = response.data.graph_name

// Step 2: Load graph into local Cytoscape
statusMessage.value = 'Loading graph into Cytoscape...'

// Try the networks endpoint
const cytoscapeResponse = await fetch(`${cytoscapeBaseUrl}/v1/networks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: graphData,
name: networkName,
collection: "BioDataFuse"
})
})

if (cytoscapeResponse.ok) {
const result = await cytoscapeResponse.json()
statusMessage.value = `Graph "${networkName}" loaded successfully into Cytoscape!`

// Try to apply layout
if (result.networkSUID) {
try {
await fetch(`${cytoscapeBaseUrl}/v1/apply/layouts/force-directed/${result.networkSUID}`, {
method: 'GET'
})
} catch (layoutError) {
console.warn('Could not apply layout:', layoutError)
}
}

} else {
const errorText = await cytoscapeResponse.text()
throw new Error(`Failed to load graph into Cytoscape: ${errorText}`)
}

} catch (error) {
errorMessage.value = error.response?.data?.detail || 'Failed to connect to Cytoscape.'
console.error('Error loading graph:', error)
if (error.response?.data?.detail) {
errorMessage.value = error.response.data.detail
} else if (error.message) {
errorMessage.value = error.message
} else {
errorMessage.value = 'Failed to load graph. Please check your connection and try again.'
}
} finally {
loading.value = false
}
}

const getLoadButtonText = () => {
if (loading.value) return 'Loading...'
if (!cytoscapeConnected.value) return 'Cytoscape not detected'
return 'Load your graph into Cytoscape'
}

const goBack = () => {
router.push('/visualize&analysis')
}
</script>

// Check Cytoscape connection when component mounts
onMounted(() => {
checkCytoscapeConnection()
})
</script>