Merge pull request #14766 from xtrojak/visualisation_visjs_transition_system

[Visualization] visjs-based transition system visualization
This commit is contained in:
Dannon
2022-10-11 09:06:59 -04:00
committed by GitHub
9 changed files with 568 additions and 0 deletions
+1
View File
@@ -32,6 +32,7 @@ const STATIC_PLUGIN_BUILD_IDS = [
"pv",
"nora",
"venn",
"ts_visjs"
];
const DIST_PLUGIN_BUILD_IDS = ["new_user"];
const PLUGIN_BUILD_IDS = Array.prototype.concat(DIST_PLUGIN_BUILD_IDS, STATIC_PLUGIN_BUILD_IDS);
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="Transition system visualization" embeddable="false">
<description>- visjs-based interactive visualization of transition system in graph form.</description>
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">text.BCSLts</test>
<to_param param_attr="id">dataset_id</to_param>
</data_source>
</data_sources>
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
</params>
<entry_point entry_point_type="mako">ts_visjs.mako</entry_point>
</visualization>
@@ -0,0 +1,11 @@
{
"name": "ts_visjs",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"vis": "^4.21.0"
},
"scripts": {
"build": "mkdir -p static/dist && cp -r node_modules/vis/dist/img node_modules/vis/dist/vis.min.js node_modules/vis/dist/vis.min.css static/dist"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,168 @@
var options = {
layout: {improvedLayout: true},
physics: {
enabled: true,
barnesHut: {
gravitationalConstant: -25000,
centralGravity: 0.5,
springConstant: 0.5,
springLength: 200,
damping: 0.15
},
maxVelocity: 50,
minVelocity: 7.5,
solver: 'barnesHut',
timestep: 0.5,
stabilization: {
enabled: true,
iterations: def_iterations,
updateInterval: def_updateInterval
},
},
nodes: {
size: 15,
font: {
size: 20
},
borderWidth: 2,
borderWidthSelected: 4,
color:{highlight:{border: '#B20F0F', background: 'red'}}
},
edges: {
width: 4,
selectionWidth: function (width) {return width*2.5;},
color:{color:'#2B7CE9', hover:'#2B7CE9', highlight: 'red'}
},
interaction: {
navigationButtons: true,
keyboard: true,
hover: true,
tooltipDelay: 500,
multiselect: true
}
};
var container = document.getElementById('mynetwork');
// data filters
hell_filter = document.getElementById('hell_node')
loops_filter = document.getElementById('loop_edges')
border_filter = document.getElementById('border_nodes')
let noHell = false
const nodesFilter = (node) => {
if (noHell) {
return (node.class != 'hell')
} else {
return true
}
}
var nodesView = new vis.DataView(nodes, { filter: nodesFilter })
// remove hell
hell_filter.addEventListener('change', (e) => {
if (hell_filter.checked){
noHell = false
} else {
noHell = true
}
nodesView.refresh()
});
data = {
nodes: nodesView,
edges: edges
};
loops_filter.addEventListener('change', (e) => {
if (loops_filter.checked){
for (index = 0; index < self_loops.length; index++) {
edges.add(self_loops[index]);
}
} else {
for (index = 0; index < self_loops.length; index++) {
edges.remove({id: self_loops[index].id});
}
}
});
border_filter.addEventListener('change', (e) => {
if (border_filter.checked){
for (index = 0; index < border_nodes.length; index++) {
nodes.update({id: border_nodes[index].id, shape: "box"});
}
} else {
for (index = 0; index < border_nodes.length; index++) {
nodes.update({id: border_nodes[index].id, shape: "ellipse"});
}
}
});
var stabil = true;
clickedNode = nodes.get(fromNode);
clickedNode.color = {
border: 'orange',
background: 'orange',
highlight: {
border: 'orange',
background: 'orange'
}
}
nodes.update(clickedNode);
window.onresize = function() {
var network = new vis.Network(container, data, options);
network.on("click", function (params) {
params.event = "[original event]";
var tmp = " ";
for (var i = 1; i <= nodes.length; i++) {
if (nodes.get(i).id == params.nodes) {
tmp = nodes.get(i).text;
};
};
if(params.nodes.length === 0 && params.edges.length > 0) {
for (var i = 1; i <= edges.length; i++) {
if (edges.get(i).id == params.edges) {
tmp = edges.get(i).text;
};
};
};
document.getElementById('rectangle').innerHTML = '<div style="width:100%;height:100%;text-align:center;border:0px solid #000;">' + tmp + '</div>';
});
network.on("stabilized", function (params) {
if(stabil) {
network.fit();
stabil = false;
};
});
network.once("stabilizationIterationsDone", function() {
document.getElementById('text').innerHTML = '100%';
document.getElementById('bar').style.width = '496px';
document.getElementById('loadingBar').style.opacity = 0;
setTimeout(function () {document.getElementById('loadingBar').style.display = 'none';}, 0);
});
network.on("doubleClick", function (params) {
params.event = "[original event]";
network.focus(params.nodes);
});
network.on("stabilizationProgress", function(params) {
var iterations = def_iterations;
var updateInterval = def_updateInterval;
var width = 5*(params.iterations/updateInterval);
var widthFactor = params.iterations/iterations;
document.getElementById('bar').style.width = width + 'px';
document.getElementById('text').innerHTML = Math.round(widthFactor*100) + '%';
});
};
@@ -0,0 +1,174 @@
#mynetwork {
width: 94%;
height: 100%;
border: 1px solid #000;
float: left;
}
#rectangle {
text-align: center;
font-weight: bold;
}
#loadingBar {
position:absolute;
top:1px;
left:1px;
width: 94%;
height: 93%;
background-color:rgba(200,200,200,0.8);
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-ms-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
opacity:1;
}
#wrapper {
position:relative;
width:900px;
height:900px;
}
#text {
position:absolute;
top:8px;
left:530px;
width:30px;
height:50px;
margin:auto auto auto auto;
font-size:22px;
color: #000000;
}
div.outerBorder {
position:relative;
top:400px;
width:600px;
height:44px;
margin:auto auto auto auto;
border:8px solid rgba(0,0,0,0.1);
background: rgb(252,252,252); /* Old browsers */
background: -moz-linear-gradient(top, rgba(252,252,252,1) 0%, rgba(237,237,237,1) 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(252,252,252,1)), color-stop(100%,rgba(237,237,237,1))); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* IE10+ */
background: linear-gradient(to bottom, rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fcfcfc', endColorstr='#ededed',GradientType=0 ); /* IE6-9 */
border-radius:72px;
box-shadow: 0px 0px 10px rgba(0,0,0,0.2);
}
#border {
position:absolute;
top:10px;
left:10px;
width:500px;
height:23px;
margin:auto auto auto auto;
box-shadow: 0px 0px 4px rgba(0,0,0,0.2);
border-radius:10px;
}
#bar {
position:absolute;
top:0px;
left:0px;
width:20px;
height:20px;
margin:auto auto auto auto;
border-radius:11px;
border:2px solid rgba(30,30,30,0.05);
background: rgb(0, 173, 246); /* Old browsers */
box-shadow: 2px 0px 4px rgba(0,0,0,0.4);
}
html {
height: 100%;
}
body {
height: 90%;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
aside {
float: right;
width: 5%;
}
table {
border-collapse: collapse;
}
td.switch_td {
padding-bottom: 6px;
}
.switch_button {
border-bottom: 2px solid black;
}
@@ -0,0 +1,198 @@
<%!
import collections
import json
import sys
from numpy import inf
%>
<%
def to_counter(state, ordering):
"""Transforms given state to Counter using given ordering."""
state = eval(state)
if state[0] == inf:
return inf
return +collections.Counter({ordering[i]: state[i] for i in range(len(ordering))})
def node_to_string(node):
"""Creates string representation of Counter of agents."""
if node == inf:
return "inf"
return "<br>".join([str(int(v)) + " " + str(k) for k, v in node.items()])
def side_to_string(side):
"""Another string representation of Counter, this time used as side in reaction."""
if side == inf:
return "inf"
return " + ".join([str(int(v)) + " " + str(k) for k, v in side.items()])
def create_sides(lhs, rhs):
"""From given substrates and products counters creates their mutual differences."""
if lhs == inf:
return inf, inf
if rhs == inf:
return collections.Counter(), inf
left = lhs - rhs
right = rhs - lhs
return left, right
def write_node(ID, label, node_class):
"""Creates string representation of a node."""
if label == "inf":
node_class = "hell"
return "\t\t{{id: {0}, label: '{0}', class: '{2}', shape: 'ellipse', title: '{0}', text: '{1}'}},\n".format(
ID, label, node_class)
def write_reaction(edge_id, left_index, right_index, substrates, products, rate, label):
"""Creates string representation of a reaction."""
rate = " @ " + str(rate) if rate else ""
label = label + " ~ " if label else ""
return "\t\t{{id: {}, from: {}, to: {}, arrows: 'to', text: '{}{} => {}{}'}},\n".format(
edge_id, left_index, right_index, label, side_to_string(substrates), side_to_string(products), rate)
def load_data():
output = {"nodes": "", "border_nodes": "", "edges": "", "edges": "", "self_loops": ""}
data = ''.join(list(hda.datatype.dataprovider(hda, 'line', strip_lines=True, strip_newlines=True)))
data = json.loads(data)
ordering = data['ordering']
nodes = {int(key): to_counter(data['nodes'][key], ordering) for key in data['nodes'].keys()}
border_nodes = set()
edges = []
self_loops = []
for edge_id, edge in enumerate(data['edges'], 1):
substrates, products = create_sides(nodes[edge['s']], nodes[edge['t']])
if edge['s'] == edge['t']:
self_loops.append((edge_id, edge['s'], edge['t'], substrates,
products, edge.get('p', None), edge.get('label', None)))
edges.append((edge_id, edge['s'], edge['t'], substrates, products,
edge.get('p', None), edge.get('label', None)))
if products == inf and substrates != inf:
border_nodes.add(edge['s'])
border_nodes_write = []
for id, state in nodes.items():
if id in border_nodes:
node_class = "border"
border_nodes_write.append((id, node_to_string(state), node_class))
else:
node_class = "default"
output["nodes"] += write_node(id, node_to_string(state), node_class)
for node in border_nodes_write:
output["border_nodes"] += write_node(*node)
for edge in edges:
output["edges"] += write_reaction(*edge)
for edge in self_loops:
output["self_loops"] += write_reaction(*edge)
iterations = (len(nodes)//100+1) * 100
output["iterations"] = iterations
output["step"] = iterations//100
output["fromNode"] = str(int(data['initial']))
return output
root = h.url_for("/static/")
app_root = root + "plugins/visualizations/ts_visjs/static/"
output = load_data()
%>
## ----------------------------------------------------------------------------
<!DOCTYPE HTML>
<html>
<head>
<title>TS network visualisation</title>
${h.javascript_link(app_root + "dist/vis.min.js")}
${h.stylesheet_link(app_root + "dist/vis.min.css")}
${h.stylesheet_link(app_root + "style.css")}
</head>
<body>
<div id="mynetwork"></div>
<aside>
<table>
<tbody>
<tr>
<td>
<img src="${app_root + 'icons/border.png'}" alt="border states" style="display: block; width: 60px;">
</td>
</tr>
<tr class="switch_button">
<td class="switch_td">
<label class="switch">
<input type="checkbox" name="check" id='border_nodes'>
<span class="slider round"></span>
</label>
</td>
</tr>
<tr>
<td>
<img src="${app_root + 'icons/loop.png'}" alt="self-loop" style="display: block; width: 60px;">
</td>
</tr>
<tr class="switch_button">
<td class="switch_td">
<label class="switch">
<input type="checkbox" name="check" id="loop_edges" checked>
<span class="slider round"></span>
</label>
</td>
</tr>
<tr>
<td>
<img src="${app_root + 'icons/fire.png'}" alt="hell state" style="display: block; width: 60px;">
</td>
</tr>
<tr>
<td class="switch_td">
<label class="switch">
<input type="checkbox" name="check" id="hell_node" checked>
<span class="slider round"></span>
</label>
</td>
</tr>
</tbody>
</table>
</aside>
<div id="rectangle"style="width:94%;border:1px solid #000;"> </div>
<div id="loadingBar">
<div class="outerBorder">
<div id="text">0%</div>
<div id="border">
<div id="bar"></div>
</div>
</div>
</div>
<script type="text/javascript">
var nodes = new vis.DataSet([ ${output["nodes"]} ]);
var border_nodes = [ ${output["border_nodes"]} ];
var edges = new vis.DataSet([ ${output["edges"]} ]);
var self_loops = [ ${output["self_loops"]} ];
var fromNode = ${output["fromNode"]};
var def_iterations = ${output["iterations"]};
var def_updateInterval = ${output["step"]};
</script>
${h.javascript_link(app_root + "script.js")}
</body>
</html>