diff --git a/client/gulpfile.js b/client/gulpfile.js index d0fadf4ca4f..3f54027ed25 100644 --- a/client/gulpfile.js +++ b/client/gulpfile.js @@ -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); diff --git a/config/plugins/visualizations/ts_visjs/config/ts_visjs.xml b/config/plugins/visualizations/ts_visjs/config/ts_visjs.xml new file mode 100644 index 00000000000..697ee498551 --- /dev/null +++ b/config/plugins/visualizations/ts_visjs/config/ts_visjs.xml @@ -0,0 +1,16 @@ + + + + - visjs-based interactive visualization of transition system in graph form. + + + HistoryDatasetAssociation + text.BCSLts + dataset_id + + + + dataset_id + + ts_visjs.mako + diff --git a/config/plugins/visualizations/ts_visjs/package.json b/config/plugins/visualizations/ts_visjs/package.json new file mode 100644 index 00000000000..5b2a179fcaf --- /dev/null +++ b/config/plugins/visualizations/ts_visjs/package.json @@ -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" + } +} diff --git a/config/plugins/visualizations/ts_visjs/static/icons/border.png b/config/plugins/visualizations/ts_visjs/static/icons/border.png new file mode 100644 index 00000000000..674c63baeb1 Binary files /dev/null and b/config/plugins/visualizations/ts_visjs/static/icons/border.png differ diff --git a/config/plugins/visualizations/ts_visjs/static/icons/fire.png b/config/plugins/visualizations/ts_visjs/static/icons/fire.png new file mode 100644 index 00000000000..149d157d9fd Binary files /dev/null and b/config/plugins/visualizations/ts_visjs/static/icons/fire.png differ diff --git a/config/plugins/visualizations/ts_visjs/static/icons/loop.png b/config/plugins/visualizations/ts_visjs/static/icons/loop.png new file mode 100644 index 00000000000..ef277244b26 Binary files /dev/null and b/config/plugins/visualizations/ts_visjs/static/icons/loop.png differ diff --git a/config/plugins/visualizations/ts_visjs/static/script.js b/config/plugins/visualizations/ts_visjs/static/script.js new file mode 100644 index 00000000000..42abe67869e --- /dev/null +++ b/config/plugins/visualizations/ts_visjs/static/script.js @@ -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 = '
' + tmp + '
'; + }); + + 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) + '%'; + }); +}; diff --git a/config/plugins/visualizations/ts_visjs/static/style.css b/config/plugins/visualizations/ts_visjs/static/style.css new file mode 100644 index 00000000000..53edbbc8552 --- /dev/null +++ b/config/plugins/visualizations/ts_visjs/static/style.css @@ -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; +} diff --git a/config/plugins/visualizations/ts_visjs/templates/ts_visjs.mako b/config/plugins/visualizations/ts_visjs/templates/ts_visjs.mako new file mode 100644 index 00000000000..7504667c534 --- /dev/null +++ b/config/plugins/visualizations/ts_visjs/templates/ts_visjs.mako @@ -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 "
".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() +%> + +## ---------------------------------------------------------------------------- + + + + + TS network visualisation + + ${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")} + + + +
+ + + +
+ +
+
+
0%
+
+
+
+
+
+ + + + ${h.javascript_link(app_root + "script.js")} + + +