Skip to content

Commit

Permalink
Very basic integration of table and graph view
Browse files Browse the repository at this point in the history
  • Loading branch information
mstimberg committed Jun 5, 2024
1 parent ab6b49e commit f3e1180
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 78 deletions.
40 changes: 40 additions & 0 deletions assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,43 @@ button {
color: var(--color-bg);
height: 1em;
}

table {
border-collapse: collapse;
overflow-x: visible;
border: 0;
}
thead.simulator_table {
background-color: var(--color-bg);
color: var(--color-text);
}
th {
text-align: right ;
}
td.has_feature:after {
content: "🗹";
}

td.match:after {
content: "🗹";
font-weight: bold;
color: darkgreen;
}

.mismatch:after {
content: "✗";
font-weight: bold;
color: darkred;
}
.header_space {
height: 150px;
position: relative;
}
.header {
bottom: 5px ;
left: 50% ;
position: absolute ;
transform: rotate( -30deg ) ;
transform-origin: center left ;
white-space: nowrap ;
}
84 changes: 10 additions & 74 deletions graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,8 @@ var elements = [];
var cy;
var cy_layout;
var removed = [];
const PRESELECTED = ["Arbor", "Brian", "NEST", "Neuron"];
var SIMULATORS = [];

function selectionChanged() {
const selected = [];
for (const name of SIMULATORS) {
const checkbox = document.getElementById(name);
if (checkbox.checked)
selected.push(name);
}
removed.toReversed().forEach(eles => eles.restore());
removed = [];
removed.push(cy.filter(function(element, i){
Expand All @@ -25,7 +17,10 @@ function selectionChanged() {
removed.push(cy.filter(function(element, i){
return element.isNode() && !element.data("features").includes("simulator") && !element.connectedEdges().some(edge => edge.visible());
}).remove());
layoutNodes();
if (selected.length > 0) {
console.log(selected);
layoutNodes();
}
}

function layoutNodes() {
Expand Down Expand Up @@ -105,37 +100,10 @@ function highlightNode(node) {
connected_edges.forEach(n => n.style("opacity", 1));
connected_nodes.forEach(n => n.style("opacity", 1));

// Show details about the simulator
const details = document.getElementById("details");
// Basic description
details.innerHTML = "<h2>" + node.data("full_name") + "</h2>";
details.innerHTML += "<p>" + node.data("description") + "</p>";
// Relations
const outgoingEdges = node.outgoers("edge");
if (outgoingEdges.length > 0) {
details.innerHTML += "<h3>Relations</h3>";
const list = document.createElement("ul");
for (let edge of outgoingEdges) {
const listItem = document.createElement("li");
const targetLink = document.createElement("a");
targetLink.href = "#";
targetLink.addEventListener("click",function(e) { node.unselect(); edge.target().select(); });
targetLink.innerHTML = edge.target().id();
const label = document.createElement("i");
label.innerHTML = " " + edge.data("label") + " ";
listItem.appendChild(label);
listItem.appendChild(targetLink);
showDetails(node.data(), node.outgoers("edge").map((edge) => {
return {target: edge.target().id(), label: edge.data("label"), source: edge.source().id()};

list.appendChild(listItem);
}
details.appendChild(list);
}
// URLs
if (node.data("urls") !== undefined) {
for (let [text, url] of Object.entries(node.data("urls"))) {
details.appendChild(urlButton(text, url));
}
}
}));
}

function highlightEdge(edge) {
Expand Down Expand Up @@ -211,7 +179,7 @@ function newNode(name, description) {
group: 'nodes',
data: {
id: name,
full_name: description["name"],
full_name: description["full_name"],
description: description["summary"],
features: description["features"],
urls: description["urls"]
Expand All @@ -233,31 +201,7 @@ function newEdge(name, relation) {
}
}

// Load style and data from JSON files
Promise.all([
fetch('assets/cy-style.json')
.then(function(res) {
return res.json();
}),
fetch('simtools/simtools.json')
.then(function(res) {
return res.json();
})
])
.then(function(dataArray) {
const style = dataArray[0];
const data = dataArray[1];
// Fill the list of simulators with all items that have "simulator" in their features
for (const [name, description] of Object.entries(data)) {
if (description["features"].includes("simulator")) {
SIMULATORS.push(name);
}
}
create_checkboxes(SIMULATORS);
for (const name of PRESELECTED) {
const checkbox = document.getElementById(name);
checkbox.checked = true;
}
function create_cy_elements(data, style) {
for (const [name, description] of Object.entries(data)) {
elements.push(newNode(name, description));
if (description["relations"] !== undefined) {
Expand All @@ -275,14 +219,6 @@ Promise.all([
style: style
});
selectionChanged();
layoutNodes();
cy.on("select", "*", highlightElement);
cy.on("unselect", "*", unhighlightNode);
// cy.on("dragfree", "*", function(event) {
// cy_layout.stop();
// event.target.lock();
// cy_layout.run();
// event.target.unlock();
// });
}
);
}
19 changes: 15 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,25 @@
<script src="https://unpkg.com/webcola@3.3.8/WebCola/cola.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-cola@2.5.1/cytoscape-cola.min.js"></script>
<script src="graph.js"></script>
<script src="table.js"></script>
<script src="index.js"></script>
</head>
<body>
<h1>Graph view</h1>
Using <a href="https://js.cytoscape.org">cytoscape</a>.
<h1>Simselect</h1>
<div id="simulators"></div>
<form>
<input type="radio" id="select_table" name="view" value="table" onclick="document.getElementById('table').style=''; document.getElementById('cy').style='display:none'">
<label for="table">Table</label>
<input type="radio" id="select_graph" name="view" value="graph" disabled onclick="selectionChanged(); document.getElementById('cy').style=''; document.getElementById('table').style='display:none'">
<label for="graph" id="graph_label" style="color:gray">Graph (selected at least one simulator)</label>
</form>
<div style="display: flex;">
<div id="cy" style="flex: 50%"></div>
<div id="details" style="flex: 50%; padding: 1em"></div>
<div id="left_container" style="flex: 50%">
<div id="cy" style="display: none"></div>
<div id="table"></div>
</div>
<div id="details" style="flex: 50%; padding: 1em">
</div>
</div>
</body>
</html>
72 changes: 72 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
var SIMULATORS = [];
var TOOL_DESCRIPTIONS = {};

function showDetails(data, outgoers) {
// Show details about the simulator
const details = document.getElementById("details");
// Basic description
details.innerHTML = "<h2>" + data["full_name"] + "</h2>";
details.innerHTML += "<p>" + data["description"] + "</p>";
// Relations
if (outgoers.length > 0) {
details.innerHTML += "<h3>Relations</h3>";
const list = document.createElement("ul");
for (let edge of outgoers) {
const listItem = document.createElement("li");
const targetLink = document.createElement("a");
// targetLink.href = "#";
// targetLink.addEventListener("click",function(e) { node.unselect(); edge.target().select(); });
targetLink.innerHTML = edge["target"];
const label = document.createElement("i");
label.innerHTML = " " + edge["label"] + " ";
listItem.appendChild(label);
listItem.appendChild(targetLink);

list.appendChild(listItem);
}
details.appendChild(list);
}
// URLs
if (data["urls"] !== undefined) {
for (let [text, url] of Object.entries(data["urls"])) {
details.appendChild(urlButton(text, url));
}
}
}

// Load style and data from JSON files
Promise.all([
fetch('assets/cy-style.json')
.then(function(res) {
return res.json();
}),
fetch('simtools/simtools.json')
.then(function(res) {
return res.json();
})
])
.then(function(dataArray) {
const style = dataArray[0];
const data = dataArray[1];
// Fill the list of simulators with all items that have "simulator" in their features
for (const [name, description] of Object.entries(data)) {
if (description["features"].includes("simulator")) {
SIMULATORS.push(name);
}
description["computing_scale"] = description["processing_support"];
delete description["processing_support"];
for (const name of ["biological_level", "computing_scale"]) {
if (description[name] === undefined)
description[name] = [];
else
description[name] = description[name].split(",").map(x => x.trim());
}
description["full_name"] = description["name"];
description["description"] = description["summary"];
TOOL_DESCRIPTIONS[name] = description;
}

create_cy_elements(data, style);
update_table();
}
);
114 changes: 114 additions & 0 deletions table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
var criteria = []; // selected criteria

const bio_levels = ["Population Model", "Single-Compartment (Simple) Model",
"Single-Compartment (Complex) Model", "Multi-Compartment Model"]
const comp_levels = ["GPU", "Single Machine", "Cluster", "Supercomputer"];
const colors = ['#a6cee3','#1f78b4','#b2df8a','#33a02c','#fb9a99','#e31a1c','#fdbf6f','#ff7f00'];
const selected = [];

const width = 1200;
const colsize = (width - 150) / (bio_levels.length + 1);
const height = 600;
const rowsize = (height - bio_levels.length*20) / (comp_levels.length + 1);

function toggle(feature) {
const checkbox = document.getElementById("select_" + feature);
if (checkbox.checked) {
criteria.push(feature);
} else {
const index = criteria.indexOf(feature);
if (index > -1) { // only splice array when item is found
criteria.splice(index, 1); // 2nd parameter means remove one item only
}
}
update_table();
}

function update_table() {
let header = "<thead class='simulator_table'>\n<th></th><th></th>"
bio_levels.forEach(bio_level => {
const is_selected = criteria.indexOf(bio_level) >= 0 ? "checked" : "";
header += `<th><div class="header_space"><div class="header"><input type="checkbox" id="select_${bio_level}" name="select_${bio_level}" onclick="toggle('${bio_level}');"/ ${is_selected}><label for="select_${bio_level}">${bio_level}</label></div></div></th>`;
});

// Add a separator line
header += `<th><div class="header_space"><div class="header"><div class="separator"></div></div></div></th>`;

comp_levels.forEach(comp_level => {
const is_selected = criteria.indexOf(comp_level) >= 0 ? "checked" : "";
header += `<th><div class="header_space"><div class="header"><input type="checkbox" id="select_${comp_level}" name="select_${comp_level}" onclick="toggle('${comp_level}');" ${is_selected}/><label for="select_${comp_level}">${comp_level}</label></div></div></th>`;;
});
header += "\n</thead>";
let rows = [];
for (const simulator of SIMULATORS) {
const sim_description = TOOL_DESCRIPTIONS[simulator];
let matches = 0;
let cells = [];
bio_levels.forEach(bio_level => {
const cell_class = get_cell_class(criteria, bio_level, sim_description["biological_level"]);
if (cell_class == "match")
matches++;
cells.push(`<td class=${cell_class}></td>`);
})
cells.push(`<td class="separator"></td>`);
comp_levels.forEach(comp_level => {
const cell_class = get_cell_class(criteria, comp_level, sim_description["computing_scale"]);
if (cell_class == "match")
matches++;
cells.push(`<td class=${cell_class}></td>`);
})
if (criteria.length == 0)
match_class = "";
else if (criteria.length == matches)
match_class = "good_match";
else if (matches > 0)
match_class = "medium_match";
else
match_class = "bad_match"
const is_checked = selected.includes(simulator) ? "checked" : "";
const checkbox = `<input type="checkbox" id="select_${simulator}" name="select_${simulator}" onclick="toggle_selection('${simulator}');" ${is_checked}/>`;
const row = `<tr class="simulator_row ${match_class}"><td>${checkbox}</td><th scope="row" class='simulator_name'><span onclick="showDetails(TOOL_DESCRIPTIONS['${simulator}'], []);">${simulator}</span></td>` + cells.join(" ") + "</tr>";
rows.push({row: row, matches: matches});
}
rows.sort((a, b) => b['matches']-a['matches'])
let table_div = document.getElementById("table");
table_div.innerHTML = "<table>\n" + header + "\n<tbody>" + rows.map(r => r["row"]).join("\n") + "</tbody></table>"
}

function toggle_selection(simulator) {
const checkbox = document.getElementById("select_" + simulator);
if (checkbox.checked) {
selected.push(simulator);
} else {
const index = selected.indexOf(simulator);
if (index > -1) { // only splice array when item is found
selected.splice(index, 1); // 2nd parameter means remove one item only
}
}
if (selected.length > 0) {
document.getElementById("graph_label").innerText = "Graph";
document.getElementById("graph_label").style = "";
document.getElementById("select_graph").disabled = false;
} else {
document.getElementById("graph_label").innerText = "Graph (select at least one simulator)";
document.getElementById("select_graph").disabled = true;
document.getElementById("graph_label").style = "color: gray;";
}
}

function get_cell_class(criteria, feature, category_description) {
if (criteria.indexOf(feature) >= 0) {
if (category_description.indexOf(feature) >= 0)
cell_class = "match";

else
cell_class = "mismatch";
} else {
if (category_description.indexOf(feature) >= 0)
cell_class = "has_feature";

else
cell_class = "no_feature";
}
return cell_class;
}

0 comments on commit f3e1180

Please sign in to comment.