Skip to content

Commit

Permalink
Merge pull request #186 from CODEX-CELIDA/feature/execution-graph-plot
Browse files Browse the repository at this point in the history
Feature/execution graph plot
  • Loading branch information
glichtner authored Aug 6, 2024
2 parents 40f302d + a672e21 commit 911070a
Show file tree
Hide file tree
Showing 19 changed files with 536 additions and 22 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ repos:
- id: pretty-format-json
args: ['--autofix', '--no-sort-keys']
- repo: https://github.com/ambv/black
rev: 24.4.2
rev: 24.8.0
hooks:
- id: black
language_version: python3.11
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.10.0'
rev: 'v1.11.1'
hooks:
- id: mypy
name: mypy
Expand All @@ -36,12 +36,12 @@ repos:
exclude: tests/
args: [--select, "D101,D102,D103,D105,D106"]
- repo: https://github.com/PyCQA/bandit
rev: '1.7.8'
rev: '1.7.9'
hooks:
- id: bandit
args: [--skip, "B101,B303,B110,B311"]
- repo: https://github.com/PyCQA/flake8
rev: '7.0.0'
rev: '7.1.1'
hooks:
- id: flake8
- repo: https://github.com/myint/autoflake
Expand Down
26 changes: 26 additions & 0 deletions apps/graph/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os

from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

load_dotenv()

app = FastAPI()

templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")

EE_API_URL = os.getenv("EE_API_URL")


@app.get("/", response_class=HTMLResponse)
async def get_index(request: Request) -> HTMLResponse:
"""
Render the index page.
"""
return templates.TemplateResponse(
"index.html", {"request": request, "ee_api_url": EE_API_URL}
)
3 changes: 3 additions & 0 deletions apps/graph/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fastapi[all]==0.105.0
uvicorn==0.20.0
python-dotenv==0.21.1
200 changes: 200 additions & 0 deletions apps/graph/static/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// script.js

async function loadRecommendations() {
const response = await fetch(`${eeApiUrl}/recommendation/list`);
const recommendations = await response.json();
const recommendationList = document.getElementById('recommendation-list');
recommendationList.innerHTML = '';
recommendations.forEach(rec => {
const div = document.createElement('div');
div.className = 'recommendation-item';
div.innerHTML = `
<div class="recommendation-title">${rec.recommendation_id}: ${rec.recommendation_name}</div>
<div class="recommendation-detail">Version: ${rec.recommendation_version}</div>
<div class="recommendation-detail">Package Version: ${rec.recommendation_package_version}</div>
`;
div.onclick = () => loadGraph(rec.recommendation_id);
recommendationList.appendChild(div);
});
}

async function loadGraph(recommendationId) {
const response = await fetch(`${eeApiUrl}/recommendation/${recommendationId}/execution_graph`);
const data = await response.json();
const graphData = data.recommendation_execution_graph;

// Extract unique node types
const nodeTypes = [...new Set(graphData.nodes.map(node => node.data.type))];
const nodeCategories = [...new Set(graphData.nodes.map(node => node.data.category))];

// Generate colors for each type
const nodeColors = {
"BASE": "#ff0000",
"POPULATION": "#00ff00",
"INTERVENTION": "#9999ff",
"POPULATION_INTERVENTION": "#ff00ff",
};
const nodeShapes = {
"Symbol": "round-rectangle",
"&": "rhomboid",
"|": "diamond",
"Not": "triangle",
"NoDataPreservingAnd": "rhomboid",
"NoDataPreservingOr": "diamond",
"NonSimplifiableAnd": "rhomboid",
"NonSimplifiableOr": "diamond",
"LeftDependentToggle": "octagon",
}

// Initialize Cytoscape
var cy = cytoscape({
container: document.getElementById('cy'),
elements: [...graphData.nodes, ...graphData.edges],
style: [
{
selector: 'node',
style: {
'label': function(ele) {
if (ele.data('type') === 'Symbol') {
if (ele.data('category') == 'BASE') {
return ele.data('class')
}
var label;
label = ele.data('concept')["concept_name"];
var value = ele.data('value');
var dosage = ele.data('dosage');
var timing = ele.data('timing');
var route = ele.data('route');

if (value) {
label += " " + value;
}
if (dosage) {
label += "\n" + dosage;
}
if (timing) {
label += "\n" + timing;
}
if (route) {
label += "\n[" + route + "]";
}
return label;

}
if (ele.data("is_sink")) {
return ele.data('category') + " [SINK]"
}
return ele.data('class')
},
'background-color': function(ele) {
return nodeColors[ele.data('category')] || '#666'; // Assign color based on 'type', with a default
},
'shape': function(ele) {
return nodeShapes[ele.data('type')] || 'star'; // Assign color based on 'type', with a default
},
'text-valign': 'center',
'color': '#000000',
'width': function(ele) {
return ele.data('type') === 'Symbol' ? '120px': '40px';
},
'height': function(ele) {
return ele.data('type') === 'Symbol' ? '80px': '40px';
},
'font-size': '10px',
'text-wrap': 'wrap',
'text-max-width': '120px' // Adjust width as needed
}
},
{
selector: 'edge',
style: {
'width': 2,
'target-arrow-shape': 'triangle', // Set arrow shape to triangle
'curve-style': 'bezier' // Makes the edge curved for better visibility of direction
}
}
],
layout: {
name: 'klay', // Use 'klay' layout for better visualization
nodeDimensionsIncludeLabels: true,
fit: true,
padding: 20,
animate: true,
animationDuration: 500,
klay: {
spacing: 20,
direction: 'DOWN',
}
}
});

// Add event listener for node click
cy.on('tap', 'node', function(evt) {
hideTippys(cy);
const node = evt.target;
if (!node.tippy) {
node.tippy = createTippy(node);
}
node.tippy.show();
});

// Hide popper when clicking on the canvas
cy.on('tap', function(evt) {
if (evt.target === cy) {
hideTippys(cy);
}
});
}

function createTippy(node) {
let content = '';

function formatData(data, prefix = '') {
for (let key in data) {
if (data.hasOwnProperty(key)) {
if (Array.isArray(data[key])) {
content += `<strong>${prefix}${key}:</strong><br>`;
data[key].forEach((item, index) => {
content += `<strong>${prefix}${key}[${index}]:</strong><br>`;
formatData(item, prefix + '&nbsp;&nbsp;&nbsp;');
});
} else if (typeof data[key] === 'object' && data[key] !== null) {
content += `<strong>${prefix}${key}:</strong><br>`;
formatData(data[key], prefix + '&nbsp;&nbsp;&nbsp;');
} else {
content += `<strong>${prefix}${key}:</strong> ${data[key]}<br>`;
}
}
}
}

formatData(node.data());

let ref = node.popperRef(); // used only for positioning
let dummyDomEle = document.createElement('div');
document.body.appendChild(dummyDomEle); // Ensure dummyDomEle has a parent

return tippy(dummyDomEle, {
content: () => {
let div = document.createElement('div');
div.innerHTML = content;
return div;
},
placement: 'top',
hideOnClick: true,
interactive: true,
trigger: 'manual',
allowHTML: true,
getReferenceClientRect: () => ref.getBoundingClientRect()
});
}

function hideTippys(cy) {
cy.elements().forEach(ele => {
if (ele.tippy) {
ele.tippy.hide();
}
});
}

loadRecommendations();
61 changes: 61 additions & 0 deletions apps/graph/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CELIDA Execution Graphs</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.21.0/cytoscape.min.js"></script>
<script src="https://unpkg.com/[email protected]/klay.js"></script>
<script src="https://unpkg.com/[email protected]/dist/dagre.js"></script>
<script src="https://unpkg.com/[email protected]/cytoscape-dagre.js"></script>
<script src="https://unpkg.com/[email protected]/cytoscape-klay.js"></script>
<script src="https://unpkg.com/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/tippy-bundle.umd.min.js"></script>
<!-- cy libs -->
<script src="https://unpkg.com/[email protected]/cytoscape-popper.js"></script>
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/dist/tippy.css" />
<style>
body {
display: flex;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
}
#recommendation-list {
width: 250px; /* Adjust width as necessary */
overflow-y: auto;
border-right: 1px solid #ccc;
padding: 10px;
}
.recommendation-item {
cursor: pointer;
padding: 10px;
border-bottom: 1px solid #ccc;
display: flex;
flex-direction: column;
}
.recommendation-item:hover {
background-color: #f0f0f0;
}
#cy {
flex-grow: 1;
display: block;
}
.recommendation-title {
font-weight: bold;
}
.recommendation-detail {
font-size: 0.9em;
color: #555;
}
</style>
</head>
<body>
<div id="recommendation-list"></div>
<div id="cy"></div>
<div class="popover" id="popover"></div>
<script>
const eeApiUrl = "{{ ee_api_url }}";
</script>
<script src="/static/script.js"></script>
</body>
</html>
12 changes: 9 additions & 3 deletions apps/viz-backend/app/database.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
from settings import get_config
from urllib.parse import quote

from settings import config
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

connection_dict = config.omop.model_dump()
connection_dict["user"] = quote(connection_dict["user"])
connection_dict["password"] = quote(connection_dict["password"])

connection_string = (
"postgresql+psycopg://{user}:{password}@{host}:{port}/{database}".format(
**get_config().omop.model_dump()
**connection_dict
)
)

engine = create_engine(
connection_string,
pool_pre_ping=True,
connect_args={
"options": "-csearch_path={}".format(get_config().omop.db_schema),
"options": "-csearch_path={}".format(config.omop.data_schema),
},
)

Expand Down
Loading

0 comments on commit 911070a

Please sign in to comment.