diff --git a/backend/app/routers/cytoscape.py b/backend/app/routers/cytoscape.py index a8c4e15..a99d917 100644 --- a/backend/app/routers/cytoscape.py +++ b/backend/app/routers/cytoscape.py @@ -2,7 +2,7 @@ from sqlalchemy import select from pathlib import Path -from fastapi import APIRouter, Depends, HTTPException, Path as FastAPIPath +from fastapi import APIRouter, Depends, HTTPException from ..services.cytoscape_service import CytoscapeService from sqlalchemy.ext.asyncio import AsyncSession @@ -15,13 +15,16 @@ class CytoscapeRequest(BaseModel): graph_name: str @router.post("/cytoscape/{set_id}") -async def cytoscape_visualization( - set_id: int, +async def get_cytoscape_graph_data( + set_id: int, req: CytoscapeRequest, current_user=Depends(get_current_user), db: AsyncSession = Depends(get_db), ): - + """ + Retrieves a graph in Cytoscape JSON format. + This data is intended to be consumed by a local application that interacts with Cytoscape. + """ try: from ..models import Annotation result = await db.execute( @@ -33,13 +36,21 @@ async def cytoscape_visualization( raise HTTPException(status_code=404, detail="Processed annotation not found.") 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) + cytoscape_json_data, error = await cytoscape_service.get_cytoscape_json( + annotation, graph_dir + ) + + if error: + raise HTTPException(status_code=500, detail=error) - if result["success"]: - return {"message": result["message"]} - else: - raise HTTPException(status_code=500, detail=result["message"]) + return { + "graph_data": cytoscape_json_data, + "network_name": req.graph_name + } + except HTTPException as he: + raise he except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") \ No newline at end of file diff --git a/backend/app/services/cytoscape_service.py b/backend/app/services/cytoscape_service.py index d9f2ca5..88c4cdc 100644 --- a/backend/app/services/cytoscape_service.py +++ b/backend/app/services/cytoscape_service.py @@ -1,7 +1,6 @@ +import json 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 @@ -11,19 +10,41 @@ 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): + async def get_cytoscape_json(self, annotations: models.Annotation, graph_dir: Path): 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 None, error + + if not pygraph.nodes() and not pygraph.edges(): + return None, "The generated graph is empty (no nodes or edges)." + + raw_graph = cytoscape.convert_graph_to_json(pygraph) + elements_only = raw_graph.get("elements") + cytoscape_json_data = {"elements": elements_only} + + print("Returning Cytoscape JSON:", json.dumps(cytoscape_json_data, indent=2)) + return cytoscape_json_data, None + except Exception as e: + return None, f"Error preparing graph data for Cytoscape: {str(e)}" + + """ + Generates Cytoscape-compatible JSON graph data. + This function no longer interacts directly with Cytoscape or attempts to get styling from pyBiodatafuse. + """ + try: + # Create the NetworkX graph pygraph, error = GraphService.create_pygraph(annotations, graph_dir) if error: - return {"success": False, "message": error} + return None, error + + if not pygraph.nodes() and not pygraph.edges(): + return None, "The generated graph is empty (no nodes or edges)." + + cytoscape_json_data = cytoscape.convert_graph_to_json(pygraph) + + return cytoscape_json_data, None - cytoscape.load_graph(pygraph, network_name=graph_name) - return {"success": True, "message": f"Graph loaded into Cytoscape as '{graph_name}'."} except Exception as e: - return {"success": False, "message": f"Error loading graph into Cytoscape: {str(e)}"} + return None, f"Error preparing graph data for Cytoscape: {str(e)}" \ No newline at end of file diff --git a/cytoscape_bridge.py b/cytoscape_bridge.py new file mode 100644 index 0000000..ff04d5c --- /dev/null +++ b/cytoscape_bridge.py @@ -0,0 +1,262 @@ +# cytoscape_bridge.py +from flask import Flask, request, jsonify +from flask_cors import CORS +import py4cytoscape as p4c +import json +import logging +import os +from networkx.readwrite.json_graph import node_link_graph + +app = Flask(__name__) +CORS(app, supports_credentials=True) + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +try: + import pyBiodatafuse.constants as const + logger.info("Successfully imported constants from pyBiodatafuse.") + + _GENE_NODE_LABELS = const.GENE_NODE_LABEL + _ANATOMICAL_NODE_LABELS = const.ANATOMICAL_NODE_LABEL + _DISEASE_NODE_LABELS = const.DISEASE_NODE_LABEL + _GO_BP_NODE_LABELS = const.GO_BP_NODE_LABEL + _GO_MF_NODE_LABELS = const.GO_MF_NODE_LABEL + _GO_CC_NODE_LABELS = const.GO_CC_NODE_LABEL + _PATHWAY_NODE_LABELS = const.PATHWAY_NODE_LABEL + _COMPOUND_NODE_LABELS = const.COMPOUND_NODE_LABEL + _SIDE_EFFECT_NODE_LABELS = const.SIDE_EFFECT_NODE_LABEL + _HOMOLOG_NODE_LABELS = const.HOMOLOG_NODE_LABEL + +except AttributeError as e: + logger.error(f"AttributeError: {e}. This likely means the expected constants are not directly available in 'pyBiodatafuse.constants'.") + +except ImportError as e: + logger.warning(f"Could not import constants from pyBiodatafuse: {e}. " + "Ensure 'pyBiodatafuse' is installed and its constants.py " + "contains the expected variable names. " + "Reverting to hardcoded values (styling may be inaccurate).") + + +def apply_cytoscape_style(network_name: str): + default_style = { + "title": "BioDataFuse_style", + "defaults": [ + {"visualProperty": "NODE_FILL_COLOR", "value": "#FF0000"}, + {"visualProperty": "EDGE_COLOR", "value": "#000000"}, + {"visualProperty": "NODE_SIZE", "value": 30}, + {"visualProperty": "EDGE_WIDTH", "value": 2}, + {"visualProperty": "NODE_LABEL_FONT_SIZE", "value": 10}, + ], + "mappings": [], + } + + p4c.styles.create_visual_style(default_style) + logger.info("Base Cytoscape style created.") + + column = "labels" + values = [ + [_GENE_NODE_LABELS], + [_ANATOMICAL_NODE_LABELS], + [_DISEASE_NODE_LABELS], + [_GO_BP_NODE_LABELS], + [_GO_MF_NODE_LABELS], + [_GO_CC_NODE_LABELS], + [_PATHWAY_NODE_LABELS], + [_COMPOUND_NODE_LABELS], + [_SIDE_EFFECT_NODE_LABELS], + [_HOMOLOG_NODE_LABELS], + ] + shapes = [ + "ELLIPSE", # Genes + "HEXAGON", # Anatomical + "VEE", # Diseases + "PARALLELOGRAM", # GO BP + "ROUND_RECTANGLE", # GO MF + "RECTANGLE", # GO CC + "OCTAGON", # Pathways + "DIAMOND", # Compounds + "TRIANGLE", # Side Effects + "Ellipse", # Homologs + ] + colors = [ + "#42d4f4", # Cyan for Genes + "#4363d8", # Blue for Anatomical + "#e6194B", # Red for Diseases + "#ff7b00", # Orange for GO BP + "#ffa652", # Orange for GO MF + "#ffcd90", # Orange for GO CC + "#3cb44b", # Green for Pathways + "#ffd700", # Gold for Compounds + "#aaffc3", # Mint for Side Effects + "#9b59b6", # Purple for Homologs + ] + + p4c.set_node_color_mapping( + table_column=column, + table_column_values=values, + colors=colors, + mapping_type="d", + style_name="BioDataFuse_style", + network=network_name + ) + logger.info("Node color mapping applied.") + + p4c.set_node_shape_mapping( + table_column=column, + table_column_values=values, + shapes=shapes, + style_name="BioDataFuse_style", + network=network_name + ) + logger.info("Node shape mapping applied.") + + p4c.layouts.apply_layout("force-directed", network=network_name) + logger.info(f"Layout 'force-directed' applied to '{network_name}'.") + +@app.route('/load_graph_local', methods=['POST']) +def load_graph_local(): + try: + data = request.get_json() + graph_data = data.get('graph_data') + network_name = data.get('network_name', 'BioDataFuse Network') + + if not graph_data: + return jsonify({"status": "error", "message": "No graph data provided"}), 400 + + # Ensure the correct structure + if "elements" not in graph_data: + return jsonify({"status": "error", "message": "'elements' key missing in graph data."}), 400 + + elements = graph_data["elements"] + nodes = elements.get("nodes", []) + edges = elements.get("edges", []) + + # Convert graph data into the correct structure for node_link_graph + # This should be a dict with keys: nodes and links (without 'elements') + nx_data = { + "nodes": [], + "links": [] # Use 'links' instead of 'edges' + } + + # Extract nodes + for node in nodes: + nx_data["nodes"].append({ + "id": node["data"]["id"], # Ensure that each node has an 'id' field + **node["data"] # Add any other properties from node["data"] + }) + + # Extract edges + for edge in edges: + nx_data["links"].append({ + "source": edge["data"]["source"], # Ensure there is a source + "target": edge["data"]["target"], # Ensure there is a target + **edge["data"] # Add other edge properties + }) + + # Now call node_link_graph with the correct structure + adj_g = node_link_graph(nx_data) + + # Call load_graph to apply style to the Cytoscape network + load_graph(adj_g, network_name) + + return jsonify({"status": "success", "message": f"Graph '{network_name}' loaded into Cytoscape."}), 200 + + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + +def load_graph(g, network_name): + """Load the obtained graph into a running instance of Cytoscape.""" + adj_g = g + + # Step 1: Create the network in Cytoscape + p4c.networks.create_network_from_networkx( + adj_g, + title=network_name, + collection="BioDataFuse", + ) + + # Step 2: Define the visual style as a dictionary + default = { + "title": "BioDataFuse_style", + "defaults": [ + {"visualProperty": "NODE_FILL_COLOR", "value": "#FF0000"}, + {"visualProperty": "EDGE_COLOR", "value": "#000000"}, + {"visualProperty": "NODE_SIZE", "value": 30}, + {"visualProperty": "EDGE_WIDTH", "value": 2}, + {"visualProperty": "NODE_LABEL_FONT_SIZE", "value": 10}, + ], + "mappings": [], + } + + # Step 3: Create the visual style if not already created + try: + p4c.styles.create_visual_style(default) + print(f"Visual style 'BioDataFuse_style' created successfully.") + except Exception as e: + print(f"Error creating visual style: {e}") + + # Step 4: Set the visual style to the network + style_name = "BioDataFuse_style" + try: + # Set visual style to the network + p4c.styles.set_visual_style(style_name, network=network_name) + print(f"Visual style '{style_name}' applied to network '{network_name}' successfully.") + except Exception as e: + print(f"Error applying visual style '{style_name}' to network '{network_name}': {e}") + + # Step 5: Define node shape and color mapping + column = "label" # Column name from the node data (adjust based on your data) + values = [ + "GENE", "ANATOMICAL", "DISEASE", "GO_BP", "GO_MF", "GO_CC", "PATHWAY", + "COMPOUND", "SIDE_EFFECT", "HOMOLOG", "KEY_EVENT", "MIE", "AOP", "AO" + ] + shapes = [ + "ELLIPSE", "HEXAGON", "VEE", "PARALLELOGRAM", "ROUND_RECTANGLE", + "RECTANGLE", "OCTAGON", "DIAMOND", "TRIANGLE", "Ellipse", "TRIANGLE", + "TRIANGLE", "VEE", "OCTAGON" + ] + colors = [ + "#42d4f4", "#4363d8", "#e6194B", "#ff7b00", "#ffa652", "#ffcd90", + "#3cb44b", "#ffd700", "#aaffc3", "#9b59b6", "#aaffc3", "#3cb44b", + "#000075", "#e6194B" + ] + + # Step 6: Apply node color mapping + try: + p4c.set_node_color_mapping( + column, + values, + colors, + mapping_type="d", # Apply as discrete values + style_name=style_name, # Reference to the visual style + network=network_name + ) + print("Node color mapping applied successfully.") + except Exception as e: + print(f"Error applying node color mapping: {e}") + + # Step 7: Apply node shape mapping + try: + p4c.set_node_shape_mapping( + column, + values, + shapes, + style_name=style_name, # Reference to the visual style + network=network_name + ) + print("Node shape mapping applied successfully.") + except Exception as e: + print(f"Error applying node shape mapping: {e}") + + # Step 8: Apply layout (optional) + try: + p4c.layouts.apply_layout("force-directed", network=network_name) + print("Layout applied successfully.") + except Exception as e: + print(f"Error applying layout: {e}") + + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=5001, debug=True) diff --git a/vue-app/src/views/CytoscapeView.vue b/vue-app/src/views/CytoscapeView.vue index 9acc4c9..ea6bf43 100644 --- a/vue-app/src/views/CytoscapeView.vue +++ b/vue-app/src/views/CytoscapeView.vue @@ -1,7 +1,6 @@