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..ada8e3d8b11
--- /dev/null
+++ b/config/plugins/visualizations/ts_visjs/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "ts_visjs",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "plotly.js": "^2.11.1",
+ "pca-js": "^1.0.2",
+ "d3": "^7.4.4"
+ },
+ "scripts": {
+ "build": "mkdir -p static/dist && cp node_modules/pca-js/pca.min.js node_modules/plotly.js/dist/plotly.min.js node_modules/d3/dist/d3.min.js static/dist"
+ }
+}
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..ae6be9d77b8
--- /dev/null
+++ b/config/plugins/visualizations/ts_visjs/templates/ts_visjs.mako
@@ -0,0 +1,462 @@
+<%!
+import collections
+import json
+import sys
+from numpy import inf
+%>
+
+<%
+def to_counter(state, ordering):
+ """
+ Transforms given state to Counter using given ordering
+
+ :param state: str representation of state
+ :param ordering: enumeration of agents
+ :return: Counter representing the state
+ """
+ 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.
+
+ :param node: given Counter of agents.
+ :return: string representation of Counter
+ """
+ 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
+ :param side: given Counter
+ :return: string representation of Counter
+ """
+ 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
+
+ :param lhs: dict of substrates
+ :param rhs: dict of products
+ :return: two counters representing 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
+
+ :param ID: ID of given node
+ :param label: enumeration of agents
+ :return: string representation
+ """
+ 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
+
+ :param edge_id: ID of given reaction
+ :param left_index: ID of FROM node
+ :param right_index: ID of TO node
+ :param substrates: enumeration of substrates
+ :param products: enumeration of products
+ :param rate: rate of the reaction (if any)
+ :param label: used rule label
+ :return: string representation
+ """
+ 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 create_HTML_graph(): #data):
+ output_file = firstpart
+
+ data = ''.join(list(hda.datatype.dataprovider(hda, 'line', strip_lines=True, strip_newlines=True)))
+ data = json.loads(data)
+
+ # data = json.load(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_file += write_node(id, node_to_string(state), node_class)
+
+ output_file += mid_1
+
+ for node in border_nodes_write:
+ output_file += write_node(*node)
+
+ output_file += mid_2
+
+ for edge in edges:
+ output_file += write_reaction(*edge)
+
+ output_file += mid_3
+
+ for edge in self_loops:
+ output_file += write_reaction(*edge)
+
+ initial = data['initial']
+
+ iterations = (len(nodes)//100+1) * 100
+ step = iterations//100
+
+ output_file += secondpart_1_1
+ output_file += stabil_physics.format(iterations, step)
+ output_file += secondpart_1_2
+
+ output_file += "\tvar fromNode = " + str(int(initial)) + ";\n"
+
+ output_file += secondpart_2_1
+ output_file += stabil_bar.format(iterations, step)
+ output_file += secondpart_2_2
+ return output_file
+
+
+firstpart = \
+'''
+
+
+ TS network visualisation
+
+ ${h.javascript_link(app_root + "dist/vis.min.js")}
+ # da sa to?
+ ${h.stylesheet_link(app_root + "dist/vis.min.css")}
+ ${h.stylesheet_link(app_root + "style.css")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
+
+stabil_physics = \
+''' iterations: {},
+ updateInterval: {}
+'''
+
+stabil_bar = \
+''' var iterations = {};
+ var updateInterval = {};
+'''
+
+# for testing
+# filename = open('bigger_pMC.json', "r")
+# graph = create_HTML_graph(filename)
+
+graph = create_HTML_graph()
+%>
+
+${graph}