diff --git a/python/views/activity_diagram.py b/python/views/activity_diagram.py
index 806cb3853a412e58c516e47f91a336b5db89cd0f..7bded9996d36e919368e9bd832b88d85ac158153 100644
--- a/python/views/activity_diagram.py
+++ b/python/views/activity_diagram.py
@@ -128,7 +128,9 @@ class ActivityDiagram(QGraphicsItem):
 
         # rectangle for mouse sense
         self.bounding_path = QPainterPath()
-        self.bounding_path.addRect(self.painter_path.boundingRect())
+        bounding_rect = self.painter_path.boundingRect()
+        bounding_rect.setTop(0)
+        self.bounding_path.addRect(bounding_rect)
 
     def json_data(self):
         self._json_data["x"] = self.x()
diff --git a/python/views/graph.py b/python/views/graph.py
new file mode 100644
index 0000000000000000000000000000000000000000..8db3c7ea7adca2a31f4b849f91e6084f35363203
--- /dev/null
+++ b/python/views/graph.py
@@ -0,0 +1,159 @@
+import random
+from PyQt5.QtCore import QPoint, QRectF, Qt
+from PyQt5.QtGui import QPainterPath
+from PyQt5.QtGui import QFontMetrics
+from PyQt5.QtGui import QFont
+from PyQt5.QtWidgets import QGraphicsItem
+from next_graphics_index import next_graphics_index
+
+# see also typical settings_manager values
+TITLE_Y = 15
+TITLE_SPACING = 25
+GRAPH_W = 400
+GRAPH_H = 400
+NODE_W = 127
+NODE_H = 17
+H_SPACING = 165
+V_SPACING = 55
+
+def _xy(json_node):
+    # node center point
+    x = json_node[2]
+    y = json_node[3] + TITLE_SPACING
+    return x, y
+
+def _xy_in(json_node):
+    x, y = _xy(json_node)
+    return x, y - NODE_H/2
+
+def _xy_out(json_node):
+    x, y = _xy(json_node)
+    return x, y + NODE_H/2
+
+def _add_node_path(json_node, path):
+    x, y = _xy(json_node)
+    w = NODE_W
+    h = NODE_H
+    path.addRect(x - w/2, y - h/2, w, h)
+
+def _add_edge_path(from_json_node, to_json_node, path):
+    from_x, from_y = _xy_out(from_json_node)
+    to_x, to_y = _xy_in(to_json_node)
+    path.moveTo(from_x, from_y)
+    path.lineTo(to_x, to_y)
+    center_x = (from_x + to_x)/2
+    center_y = (from_y + to_y)/2
+    return center_x, center_y
+
+
+def _random_xy():
+    x = random.randrange(int(NODE_W/2), int(GRAPH_W - NODE_W / 2))
+    y = random.randrange(int(NODE_H/2) + TITLE_SPACING,
+                         int(GRAPH_H - NODE_H / 2))
+    return x, y
+
+def _maybe_assign_xy(nodes):
+    for node in nodes:
+        if len(node) == 2:
+            x, y = _random_xy()
+        node.append(x)
+        node.append(y)
+
+class Graph(QGraphicsItem):
+    Type = next_graphics_index()
+
+    def __init__(self, json_data, default_x, default_y):
+        super(Graph, self).__init__()
+
+        # make graph deterministic
+        random.seed(a=1)
+
+        # graphicsItem mode
+        self.setFlag(QGraphicsItem.ItemIsMovable)
+        self.setFlag(QGraphicsItem.ItemIsSelectable)
+        self.setZValue(2)
+
+        # json_data
+        self._json_data = json_data # keep for export
+
+        # x,y
+        if json_data["x"] == 0 and json_data["y"] == 0:
+            self.setPos(QPoint(default_x + NODE_W / 2, default_y))
+        else:
+            self.setPos(QPoint(json_data["x"], json_data["y"]))
+
+        # graph data
+        data = json_data["data"]
+
+        # title, nodes, edges
+        self.title = data[0]
+        self.nodes = data[1]
+        edges = data[2]
+        _data3 = data[3] # ?
+
+        # maybe assign coordinates to nodes
+        _maybe_assign_xy(self.nodes)
+
+        # indexed nodes for edges
+        indexed_nodes = dict()
+
+        # painter path
+        self.painter_path = QPainterPath()
+
+        # nodes
+        for json_node in self.nodes:
+
+            _add_node_path(json_node, self.painter_path)
+            indexed_nodes[json_node[0]] = json_node
+
+        # edges and edge labels
+        self.edge_labels = list()
+        for json_edge in edges:
+            center_x, center_y = _add_edge_path(indexed_nodes[json_edge[0]],
+                                                indexed_nodes[json_edge[1]],
+                                                self.painter_path)
+            self.edge_labels.append((center_x, center_y, json_edge[2]))
+
+        # rectangle for mouse sense
+        self.bounding_path = QPainterPath()
+        self.bounding_path.addRect(QRectF(0,0,GRAPH_W, GRAPH_H))
+
+    def json_data(self):
+        self._json_data["x"] = self.x()
+        self._json_data["y"] = self.y()
+        return self._json_data
+        
+    def type(self):
+        return Graph.Type
+
+    # draw inside this rectangle
+    def boundingRect(self):
+        return self.bounding_path.boundingRect().adjusted(-1, -1, 1, 1)
+
+    # mouse hovers when inside this rectangle
+    def shape(self):
+        return self.bounding_path
+
+    def paint(self, painter, _option, _widget):
+
+        w = NODE_W
+        h = NODE_H
+
+        # title
+        painter.drawText(QRectF(0, TITLE_Y, GRAPH_W, h), Qt.AlignCenter,
+                         self.title)
+
+        # painter_path of nodes and edges
+        painter.drawPath(self.painter_path)
+
+        # node text
+        for node in self.nodes:
+            x, y = _xy(node)
+            painter.drawText(QRectF(x - w/2, y - h/2, w, h),
+                             Qt.AlignCenter, node[1])
+
+        # edge text
+        for x, y, label in self.edge_labels:
+            painter.drawText(QRectF(x-w/2, y-w/2, w, h),
+                             Qt.AlignCenter, node[1])
+
diff --git a/python/views/make_views.py b/python/views/make_views.py
index 2f0b780713f795b91ab078f6fb815187583cf651..6023fb14426f54283c27cd8d0ce2c6e0142d4c6f 100644
--- a/python/views/make_views.py
+++ b/python/views/make_views.py
@@ -1,9 +1,16 @@
+"""To add a new view:
+   * Write the view in its own file, see other views for examples.
+   * Import the view in this file.
+   * Add the view name to VIEW_TYPES.
+   * Instantiate the view in function make_view.
+"""
 #from views.pyplot_report import pyplot_report
 from views.report import Report
 from views.table import Table
 from views.activity_diagram import ActivityDiagram
+from views.graph import Graph
 
-VIEW_TYPES = {"REPORT", "TABLE", "AD"}
+VIEW_TYPES = {"REPORT", "TABLE", "AD", "GRAPH"}
 def make_json_views(generated_views):
     """Convert trace-generator view data structures into structures
        with x,y positioning."""
@@ -33,6 +40,8 @@ def make_view(json_view, default_x, default_y):
         view = Table(json_view, default_x, default_y)
     elif json_view["type"] == "AD":
         view = ActivityDiagram(json_view, default_x, default_y)
+    elif json_view["type"] == "GRAPH":
+        view = Graph(json_view, default_x, default_y)
 
     else:
         print("Unrecognized view type: %s"%view_type, json_view["type"])