diff --git a/hysop/core/graph/computational_graph.py b/hysop/core/graph/computational_graph.py
index d95fde0e6469a4d99b0741112341498f5832e81e..eda63ad2307c6fbdf046a2dce6bbaba6e3b909b7 100644
--- a/hysop/core/graph/computational_graph.py
+++ b/hysop/core/graph/computational_graph.py
@@ -702,16 +702,51 @@ class ComputationalGraph(ComputationalGraphNode):
             print self.variable_report()
             print self.operator_report()
 
-    @debug
-    @graph_built
-    def display(self, visu_rank=0, vertex_font_size=10, edge_font_size=16):
+    def display(self, visu_rank=0, show_buttons=False):
         """
         Display the reduced computational graph.
         """
         from hysop import main_rank
         if (visu_rank is None) or (main_rank != visu_rank):
             return
-        raise NotImplementedError('This feature has not been implemented yet.')
+        
+        net = self.to_pyvis() 
+        
+        import tempfile
+        with tempfile.NamedTemporaryFile(suffix='.html') as f:
+            net.show(f.name)
+    
+    def to_file(self, path, io_rank=0, show_buttons=False):
+        """
+        Generate an interactive computational graph in an html file.
+        """
+        from hysop import main_rank
+        if (io_rank is None) or (main_rank != io_rank):
+            return
+        
+        net = self.to_pyvis() 
+        net.write_html(path)
+
+    @graph_built
+    def to_pyvis(self):
+        """
+        Convert the graph to a pyvis network for vizualization.
+        """
+        try:
+            import pyvis
+        except ImportError:
+            msg='\nFATAL ERROR: Graph vizualization requires the pyvis module.\n'
+            print(msg)
+            raise
+
+        graph = self.reduced_graph
+        network = pyvis.network.Network()
+        for node in graph:
+            node_id = int(node)
+            network.add_node(node_id, label=node.label, title=node.title, 
+                    color=node.color)
+        return network
+
 
     @debug
     @graph_built
diff --git a/hysop/core/graph/graph.py b/hysop/core/graph/graph.py
index 6c2f37affefc5f464c96fc76eff1acbb188d8ee8..1d8dadb31cd5cb806fe390bc612e3a7719423621 100644
--- a/hysop/core/graph/graph.py
+++ b/hysop/core/graph/graph.py
@@ -1,5 +1,6 @@
 import inspect, networkx
 from hysop import dprint
+from hysop.constants import MemoryOrdering
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.decorators import not_implemented, debug, wraps, profile
 
@@ -25,9 +26,23 @@ def new_edge(graph, u, v, *args, **kwds):
     # /!\ We have to use networkx 2.2 which has a different interface for attributes
     graph.add_edge(u, v, object=EdgeAttributes(*args, **kwds))
     return (u,v)
+
+def generate_vertex_colors():
+    import matplotlib
+    from matplotlib import cm
+    c0 = cm.get_cmap('tab20c').colors
+    c1 = cm.get_cmap('tab20b').colors
+    colors = []
+    for i in range(4):
+        colors += c0[i::4] + c1[i::4]
+    colors = tuple(map(matplotlib.colors.to_hex, colors))
+    return colors
         
 class VertexAttributes(object):
     """Simple class to hold vertex data."""
+
+    colors = generate_vertex_colors()
+
     def __init__(self, graph, operator=None):
         if not hasattr(graph, '_hysop_node_counter'):
             graph._hysop_node_counter = 0
@@ -61,6 +76,7 @@ class VertexAttributes(object):
         self.output_states = output_states
         return self
     
+    # hashing for networkx
     def hash(self):
         return self.node_id
     def __eq__(self, other):
@@ -68,6 +84,87 @@ class VertexAttributes(object):
     def __int__(self):
         return self.node_id
 
+    # pyvis attributes for display
+    @property
+    def label(self):
+        return self.operator.pretty_name
+    @property
+    def title(self):
+        return self.node_info().replace('\n','<br>')
+    
+    @property
+    def color(self):
+        cq = self.command_queue
+        if (cq is None):
+            return None
+        assert isinstance(cq, int) and cq >= 0
+        colors = self.colors
+        ncolors = len(colors)
+        return colors[cq%ncolors]
+
+    def node_info(self):
+        op = self.operator
+        istates = self.input_states
+        ostates = self.output_states
+
+        ifields = op.input_fields
+        ofields = op.output_fields
+        iparams = op.input_params
+        oparams = op.output_params
+
+        memorder2str = {
+                MemoryOrdering.C_CONTIGUOUS: 'C',
+                MemoryOrdering.F_CONTIGUOUS: 'F',
+        }
+
+        def ifinfo(field, topo):
+            info = (field.name, topo.id)
+            if istates:
+                assert field in istates
+                istate = istates[field]
+                assert (istate is not None)
+                info+=(memorder2str[istate.memory_order],)
+                info+=(str(istate.tstate),)
+            return ', '.join(map(str,info))
+        def ofinfo(field, topo):
+            info = (field.name, topo.id)
+            if ostates:
+                assert field in ostates
+                ostate = ostates[field]
+                assert (ostate is not None)
+                info+=(memorder2str[ostate.memory_order],)
+                info+=(str(ostate.tstate),)
+            return ', '.join(map(str,info))
+        def ipinfo(param):
+            return param.name
+        def opinfo(param):
+            return param.name
+                
+        prefix='&nbsp;&nbsp<b>'
+        suffix='</b>&nbsp;&nbsp'
+        sep = '\n'+'&nbsp'*14
+
+        ss = '<h2>Operator {}</h2>{}{}{}{}{}\n{}'.format(op.name,
+                '{p}Rank:{s}{}\n\n'.format(self.op_ordering, p=prefix, s=suffix)
+                    if self.op_ordering else '',
+                '{p}Pin:{s}{}\n'.format(sep.join(ipinfo(param) 
+                    for param in iparams.values()), p=prefix, s=suffix+'&nbsp&nbsp')
+                    if iparams else '',
+                '{p}Fin:{s}{}\n'.format(sep.join([ifinfo(f,topo) 
+                    for (f,topo) in ifields.iteritems()]), p=prefix, s=suffix+'&nbsp&nbsp')
+                    if ifields else '',
+                '{p}Pout:{s}{}\n'.format(sep.join([opinfo(param) 
+                    for param in oparams.values()]), p=prefix, s=suffix) 
+                    if oparams else '',
+                '{p}Fout:{s}{}\n'.format(sep.join([ofinfo(f,topo) 
+                    for (f,topo) in ofields.iteritems()]), p=prefix, s=suffix) 
+                    if ofields else '',
+                '{p}Type:{s} {}'.format(
+                    sep.join(map(lambda x: x.__name__, type(op).__mro__[:-2])),
+                    p=prefix, s=suffix))
+        return ss
+
+
 class EdgeAttributes(object):
     """Simple class to hold edge data."""
     def __init__(self, variable=None, topology=None):
@@ -242,53 +339,3 @@ def op_apply(f):
                 return
         return ret
     return apply
-
-def _op_info(op, istates=None, ostates=None, jmp=False):
-    ifields = op.input_fields
-    ofields = op.output_fields
-    iparams = op.input_params
-    oparams = op.output_params
-
-    memorder2str = {
-            MemoryOrdering.C_CONTIGUOUS: 'C',
-            MemoryOrdering.F_CONTIGUOUS: 'F',
-    }
-
-    def ifinfo(field, topo):
-        info = (field.name, topo.id)
-        if istates:
-            assert field in istates
-            istate = istates[field]
-            assert (istate is not None)
-            info+=(memorder2str[istate.memory_order],)
-            info+=(str(istate.tstate),)
-        return info
-    def ofinfo(field, topo):
-        info = (field.name, topo.id)
-        if ostates:
-            assert field in ostates
-            ostate = ostates[field]
-            assert (ostate is not None)
-            info+=(memorder2str[ostate.memory_order],)
-            info+=(str(ostate.tstate),)
-        return info
-    def ipinfo(param):
-        return param.name
-    def opinfo(param):
-        return param.name
-
-    ss = 'Operator {} => \n {}{}{}{}\n  {}'.format(op.name,
-            'Pin:{}\n  '.format([ ipinfo(param) for param in iparams.values() ])
-                if iparams else '',
-            'Fin:{}\n  '.format([ ifinfo(f,topo) for (f,topo) in ifields.iteritems() ])
-                if ifields else '',
-            'Pout:{}\n  '.format([ opinfo(param) for param in oparams.values() ])
-                if oparams else '',
-            'Fout:{}\n  '.format([ ofinfo(f,topo) for (f,topo) in ofields.iteritems() ])
-                if ofields else '',
-            op.__class__)
-    if jmp:
-        return ss
-    else:
-        return ss.replace('\n','    ')
-
diff --git a/hysop/core/graph/tests/test_graph.py b/hysop/core/graph/tests/test_graph.py
index a359a69b1368d63ab28d1530216f5905673f8d10..b91380728945a9a189ae39e84686a07fbf21ad5a 100644
--- a/hysop/core/graph/tests/test_graph.py
+++ b/hysop/core/graph/tests/test_graph.py
@@ -101,4 +101,4 @@ class TestGraph(object):
 
 if __name__ == '__main__':
     test = TestGraph()
-    test.test_graph_build(display=False)
+    test.test_graph_build(display=True)
diff --git a/hysop/numerics/splitting/test/test_strang.py b/hysop/numerics/splitting/test/test_strang.py
index ac6048a007526bf67185159abb49987678dbf953..b6c543fb082f7523ca16d127dc3dc88b3f9c51cf 100644
--- a/hysop/numerics/splitting/test/test_strang.py
+++ b/hysop/numerics/splitting/test/test_strang.py
@@ -81,8 +81,6 @@ class TestStrang(object):
         problem.insert(splitting)
         problem.insert(poisson)
         problem.build()
-
-        problem.display()
         problem.finalize()
 
     def test_strang_2d(self, n=33):
diff --git a/hysop/operator/tests/test_fd_derivative.py b/hysop/operator/tests/test_fd_derivative.py
index 12f7f188da354a1cc86787d5cad43389c8590872..036e75cc0e7039479d40fcdbf8d03faa17acfc0a 100644
--- a/hysop/operator/tests/test_fd_derivative.py
+++ b/hysop/operator/tests/test_fd_derivative.py
@@ -187,7 +187,6 @@ class TestFiniteDifferencesDerivative(object):
         for impl in implementations:
             for op in iter_impl(impl):
                 op.build(outputs_are_inputs=True)
-                #op.display()
 
                 dF     = op.get_input_discrete_field(F)
                 dgradF = op.get_output_discrete_field(gradF)
diff --git a/hysop/operator/tests/test_spectral_derivative.py b/hysop/operator/tests/test_spectral_derivative.py
index fbed428d10b327d316a237160509ffe1ea5c7003..a02ae7c9aa9b56a80578fed5e7f8ca5898afabcc 100644
--- a/hysop/operator/tests/test_spectral_derivative.py
+++ b/hysop/operator/tests/test_spectral_derivative.py
@@ -223,7 +223,6 @@ class TestSpectralDerivative(object):
             for impl in implementations:
                 for op in iter_impl(impl):
                     op.build(outputs_are_inputs=False)
-                    # op.display()
 
                     Fd = op.get_input_discrete_field(F)
                     dFd = op.get_output_discrete_field(dF)
diff --git a/hysop_examples/examples/particles_above_salt/particles_above_salt_bc.py b/hysop_examples/examples/particles_above_salt/particles_above_salt_bc.py
index fecea38c9b450792b57ea28391187d576a196b8f..cc3c345fc7d89c42a58e69d61dcf75059c0f50f5 100644
--- a/hysop_examples/examples/particles_above_salt/particles_above_salt_bc.py
+++ b/hysop_examples/examples/particles_above_salt/particles_above_salt_bc.py
@@ -296,7 +296,7 @@ def compute(args):
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
     if args.display_graph:
-        problem.display()
+        problem.display(args.visu_rank)
     
     # Create a simulation
     # (do not forget to specify the t and dt parameters here)
diff --git a/hysop_examples/examples/particles_above_salt/particles_above_salt_bc_3d.py b/hysop_examples/examples/particles_above_salt/particles_above_salt_bc_3d.py
index a54c4163326f7a1705b802d87fb62230132ae5e8..00b82718e6ae2ae7db18dc8ede62697cff4d4685 100644
--- a/hysop_examples/examples/particles_above_salt/particles_above_salt_bc_3d.py
+++ b/hysop_examples/examples/particles_above_salt/particles_above_salt_bc_3d.py
@@ -311,7 +311,7 @@ def compute(args):
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
     if args.display_graph:
-        problem.display()
+        problem.display(args.visu_rank)
     
     # Create a simulation
     # (do not forget to specify the t and dt parameters here)
diff --git a/hysop_examples/examples/particles_above_salt/particles_above_salt_periodic.py b/hysop_examples/examples/particles_above_salt/particles_above_salt_periodic.py
index fda6b5c66e064e2dc25d87ad7c3dacd19cd21fea..c4a635967025c25e804c91f3ea1a64016b8cee42 100644
--- a/hysop_examples/examples/particles_above_salt/particles_above_salt_periodic.py
+++ b/hysop_examples/examples/particles_above_salt/particles_above_salt_periodic.py
@@ -305,7 +305,7 @@ def compute(args):
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
     if args.display_graph:
-        problem.display()
+        problem.display(args.visu_rank)
     
     # Create a simulation
     # (do not forget to specify the t and dt parameters here)
diff --git a/hysop_examples/examples/particles_above_salt/particles_above_salt_symmetrized.py b/hysop_examples/examples/particles_above_salt/particles_above_salt_symmetrized.py
index 42587f275edaf28481346bf21ece04e375be3764..0fcfdcb60a68791b79d04732a87334d8c4c60ae0 100644
--- a/hysop_examples/examples/particles_above_salt/particles_above_salt_symmetrized.py
+++ b/hysop_examples/examples/particles_above_salt/particles_above_salt_symmetrized.py
@@ -293,7 +293,7 @@ def compute(args):
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
     if args.display_graph:
-        problem.display()
+        problem.display(args.visu_rank)
     
     # Create a simulation
     # (do not forget to specify the t and dt parameters here)
diff --git a/hysop_examples/examples/sediment_deposit/sediment_deposit.py b/hysop_examples/examples/sediment_deposit/sediment_deposit.py
index 1cd16b714349a9731696919a3452260500882279..8bae7c71c8b0bc59737349199c4068f57c91ba10 100644
--- a/hysop_examples/examples/sediment_deposit/sediment_deposit.py
+++ b/hysop_examples/examples/sediment_deposit/sediment_deposit.py
@@ -307,7 +307,7 @@ def compute(args):
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
     if args.display_graph:
-        problem.display()
+        problem.display(args.visu_rank)
     
     # Create a simulation
     # (do not forget to specify the t and dt parameters here)
diff --git a/hysop_examples/examples/sediment_deposit/sediment_deposit_levelset.py b/hysop_examples/examples/sediment_deposit/sediment_deposit_levelset.py
index 4b8622f08c045429f2a24844174d74cc67e671da..9adbca147b9e70f16e08ec2ae36d4194eb43ffcf 100644
--- a/hysop_examples/examples/sediment_deposit/sediment_deposit_levelset.py
+++ b/hysop_examples/examples/sediment_deposit/sediment_deposit_levelset.py
@@ -368,7 +368,7 @@ def compute(args):
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
     if args.display_graph:
-        problem.display()
+        problem.display(args.visu_rank)
     
     # Create a simulation
     # (do not forget to specify the t and dt parameters here)