diff --git a/python/graph_collapse_helpers.py b/python/graph_collapse_helpers.py
index 4cb2396c6926c208c04b8231b959b25738d2bfa3..c28da4402b7284a0948f78bb2db3ba65a7a36b1d 100644
--- a/python/graph_collapse_helpers.py
+++ b/python/graph_collapse_helpers.py
@@ -91,17 +91,17 @@ def at_and_above_in_uncollapsed(node):
     return node_set
 
 # get the set of removable_edges and addable_edge_pairs
-def collapsed_edges(graph_item):
+def collapsed_edges(graph_scene):
     print("collapsed_edges.a")
 
     # make sure there are edges to work with
-    if not graph_item.edges:
+    if not graph_scene.edges:
         return (list(), list())
 
     # identify the existing collapsed source_node, dest_node edge pairs
     existing_edge_pairs = set()
     existing_edges = dict()
-    for edge in graph_item.edges:
+    for edge in graph_scene.edges:
         if edge.relation == "COLLAPSED_FOLLOWS":
             existing_edge_pairs.add((edge.source_node, edge.dest_node))
             existing_edges[(edge.source_node, edge.dest_node)] = edge
@@ -110,7 +110,7 @@ def collapsed_edges(graph_item):
 
     # calculate the set of source_node, dest_node edge pairs
     edge_pairs = set()
-    for edge in graph_item.edges:
+    for edge in graph_scene.edges:
         if edge.relation == "FOLLOWS" and (edge.source_node.collapse
                                            or edge.dest_node.collapse):
             print("will add COLLAPSED_FOLLOWS to bridge: %s    to    %s" % (
diff --git a/python/graph_constants.py b/python/graph_constants.py
deleted file mode 100644
index 72c44fdb3a307b1ff63cdcdc5ef8247745f050af..0000000000000000000000000000000000000000
--- a/python/graph_constants.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from PyQt5.QtCore import QRectF
-
-# global constants and defaults
-GRAPHICS_RECT = QRectF(-50, -50, 25000, 25000)
-
diff --git a/python/graph_item.py b/python/graph_item.py
index b7ee8eab1faa1defd19ff12fb4910d59af972ba7..839efd0fcf1ebc8ff887af07e5f70bec4d5a2180 100644
--- a/python/graph_item.py
+++ b/python/graph_item.py
@@ -1,5 +1,4 @@
 from PyQt5.QtCore import QRect
-from graph_constants import GRAPHICS_RECT
 from graph_collapse_helpers import collapsed_edges
 from edge import Edge
 from edge_point_placer import EdgePointPlacer
@@ -12,7 +11,7 @@ class GraphItem():
       * nodes (list<Node>)
       * edges (list<Edge>)
 
-    NOTE: Optimization: call initialize_items and appearance just-in-time.
+    NOTE: Optimization: call initialize_items and set_appearance just-in-time.
     Set appearance when graph should be painted differently.
     """
 
@@ -64,23 +63,7 @@ class GraphItem():
         # perform any just-in-time initialization
         self.initialize_items()
         self.set_appearance()
-
-        # find the corners of this graph
-        min_x = GRAPHICS_RECT.x() + GRAPHICS_RECT.width()
-        max_x = GRAPHICS_RECT.x()
-        min_y = GRAPHICS_RECT.y() + GRAPHICS_RECT.height()
-        max_y = GRAPHICS_RECT.y()
-        for node in self.nodes:
-            if node.x() - node.w/2 < min_x:
-                min_x = node.x() - node.w/2
-            if node.x() + node.w/2 > max_x:
-                max_x = node.x() + node.w/2
-            if node.y() - node.h/2 < min_y:
-                min_y = node.y() - node.h/2
-            if node.y() + node.h/2 > max_y:
-                max_y = node.y() + node.h/2
-
-        return QRect(min_x, min_y, max_x-min_x, max_y-min_y)
+        return self.itemsBoundingRect()
 
     # uncollapse all nodes of node type
     def uncollapse(self, node_type):
diff --git a/python/graph_main_widget.py b/python/graph_main_widget.py
index 3297bda567b76f9edffe8de4fdd06212bd44d21b..0a9160a4ef16d13831264d3a800bb9d9457cb3d8 100644
--- a/python/graph_main_widget.py
+++ b/python/graph_main_widget.py
@@ -26,14 +26,12 @@ from edge_grip import EdgeGrip
 
 # GraphicsView
 class GraphMainView(QGraphicsView):
-    """GraphMainWidget provides the main QGraphicsView.  It manages signals
-    and wraps these:
-      * GraphMainView
-      * GraphMainScene
+    """GraphMainWidget provides the main QGraphicsView.
+    It manages signals.
 
     GraphMainWidget also issues signal:
-      * signal_graph_item_view_changed = pyqtSignal(GraphItem,
-                                                    name='graphItemViewChanged')
+      * signal_graph_item_view_changed = pyqtSignal(dict,
+                                                    name='graphViewChanged')
     """
 
     def __init__(self):
diff --git a/python/graph_scene.py b/python/graph_scene.py
new file mode 100644
index 0000000000000000000000000000000000000000..70743b4d3968fda50965f37e394d5640dca44db0
--- /dev/null
+++ b/python/graph_scene.py
@@ -0,0 +1,214 @@
+import math
+from PyQt5.QtCore import QRectF, Qt
+from PyQt5.QtCore import pyqtSignal
+from PyQt5.QtCore import pyqtSlot
+from PyQt5.QtCore import QObject
+from PyQt5.QtGui import QBrush, QColor, QLinearGradient, QPainter, QPen
+from PyQt5.QtGui import QTransform
+from PyQt5.QtWidgets import QGraphicsView
+from PyQt5.QtWidgets import QGraphicsScene
+from settings_manager import settings
+from graph_collapse_helpers import collapse_below, uncollapse_below
+#from node import Node # for Node detection
+from edge_grip import EdgeGrip
+
+# QGraphicsScene for a trace graph
+class GraphScene(QGraphicsScene):
+
+    def __init__(self, trace):
+        super(GraphScene, self).__init__()
+        self.index = trace["index"]
+        self.mark = trace["mark"]
+        self.probability = trace["probability"]
+        self.nodes = trace["nodes"]
+        self.edges = trace["edges"]
+        self.graph_views = trace["trace_views"] # zz future
+
+        # edge highlight state, grips show when edges are highlighted
+        self._highlighted_edge = None
+        self._source_edge_grip = None
+        self._dest_edge_grip = None
+
+        self.initialize_items()
+        self.set_appearance()
+
+    # set_scene, useful in hide/collapse nodes
+    def set_scene(self):
+        self.clear_scene()
+
+        # optimization: just-in-time for display
+        self.initialize_items()
+        self.set_appearance()
+
+        # add node items
+        for node in self.nodes:
+            self.addItem(node)
+
+        # add edge items
+        for edge in self.edges:
+            self.addItem(edge)
+
+        # set initial size
+        self.setSceneRect(self.itemsBoundingRect())
+
+    # clear_scene, useful in hide/collapse nodes
+    def clear_scene(self):
+        # turn off grip selection and grips
+        if self._highlighted_edge:
+            self.unhighlight_edge()
+
+        # we must call removeItem so QGraphicsScene does not delete these
+        for item in self.items():
+            item.prepareGeometryChange()
+            self.removeItem(item)
+
+#        # hack to remove cached residue from edges
+#        self.clear()
+
+    # perform orderly just-in-time initialization of graph item components
+    def initialize_items(self):
+        if not self._items_initialized:
+            # initialize items
+            for node in self.nodes:
+                node.initialize_node(self)
+            placer = EdgePointPlacer()
+            for edge in self.edges:
+                edge.initialize_edge(placer)
+            self._items_initialized = True
+
+    # reset appearance when graph color or shape changes
+    def set_appearance(self):
+        if not self.appearance_set:
+            for node in self.nodes:
+                node.set_appearance()
+            for edge in self.edges:
+                edge.set_appearance()
+            self.appearance_set = True
+
+    def change_h_spacing(self, old_spacing, old_indent,
+                               new_spacing, new_indent):
+        for node in self.nodes:
+            x = (node.x() - old_indent) / old_spacing * new_spacing + new_indent
+            node.setPos(x, node.y())
+
+    def change_v_spacing(self, old_spacing, old_indent,
+                               new_spacing, new_indent):
+        for node in self.nodes:
+            y = (node.y() - old_indent) / old_spacing * new_spacing + new_indent
+            node.setPos(node.x(), y)
+
+    def bounding_rect(self):
+        # perform any just-in-time initialization
+        self.set_appearance()
+        return self.itemsBoundingRect()
+
+    # highlight edge and create its two bezier edge grips
+    def highlight_edge(self, edge):
+        if self._highlighted_edge:
+            raise Exceiption("Bad")
+        edge.set_highlighted(True)
+        self._source_edge_grip = EdgeGrip(edge, "source")
+        self.addItem(self._source_edge_grip)
+        self._dest_edge_grip = EdgeGrip(edge, "dest")
+        self.addItem(self._dest_edge_grip)
+        self._highlighted_edge = edge
+
+    # unhighlight edge and remove its two bezier edge grips
+    def unhighlight_edge(self):
+        if not self._highlighted_edge:
+            raise Exceiption("Bad")
+        self._highlighted_edge.set_highlighted(False)
+        # be careful with scene internal indexes else Qt crashes when deleting
+        self._source_edge_grip.prepareGeometryChange()
+        self.removeItem(self._source_edge_grip)
+        self._source_edge_grip = None
+        self._dest_edge_grip.prepareGeometryChange()
+        self.removeItem(self._dest_edge_grip)
+        self._dest_edge_grip = None
+        self._highlighted_edge = None
+
+    # uncollapse all nodes of node type
+    def uncollapse(self, node_type):
+        for node in self.nodes:
+            if node.node_type == node_type:
+                node.do_uncollapse()
+        self.appearance_set = False
+        self.set_appearance()
+        self.set_collapsed_edges()
+
+    # diagnostics
+    def _print_totals(self):
+        if not self.edges:
+            print("Totals: None, empty graph.")
+            return
+
+        # total items in scene
+        scene_item_total = len(self.items())
+
+        # nodes + edges in this scene
+        graph_item_total = len(self.nodes) + len(self.edges)
+
+        # edge_list_total
+        edge_list_total = 0
+        for node in self.nodes:
+            edge_list_total += len(node.edge_list)
+
+        print("Totals: scene: %d, graph: %d, graph nodes: %d, "
+              "graph edges: %d, edge list total: %d" % (
+                       scene_item_total, graph_item_total,
+                       len(self.nodes), len(self.edges), edge_list_total))
+
+    # redo the list of collapsed edges in edges[] based on what is collapsed
+    def set_collapsed_edges(self):
+        print("set_collapsed_edges.a")
+        self._print_totals()
+        removable_edges, addable_edge_pairs = collapsed_edges(self)
+
+        # a reference to node_lookup
+        node_lookup = self.edges[0].node_lookup
+
+        # create the addable edges
+        addable_edges = list()
+        for source_node, dest_node in addable_edge_pairs:
+            addable_edge = Edge(source_node.node_id, "COLLAPSED_FOLLOWS",
+                                dest_node.node_id, "", node_lookup)
+            addable_edges.append(addable_edge)
+
+        # remove any discontinued FOLLOWS edges
+        for removable_edge in removable_edges:
+            removable_edge.source_node.edge_list.remove(removable_edge)
+            removable_edge.dest_node.edge_list.remove(removable_edge)
+            self.edges.remove(removable_edge)
+
+        # add any new FOLLOWS edges
+        placer = EdgePointPlacer()
+        for addable_edge in addable_edges:
+            self.edges.append(addable_edge)
+            addable_edge.initialize_edge(placer)
+            addable_edge.set_appearance()
+
+        # change the COLLAPSED_FOLLOWS edges
+        self.clear_scene()
+        self.set_scene(self)
+
+        # reset appearance
+        self.appearance_set = False
+        self.set_appearance()
+
+        print("set_collapsed_edges.b")
+        self._print_totals()
+
+    # remove grips on mouse down unless on grip
+    def mousePressEvent(self, event):
+        if self._highlighted_edge:
+            if not self._source_edge_grip.isUnderMouse() \
+                         and not self._dest_edge_grip.isUnderMouse():
+                self.unhighlight_edge()
+
+        super(GraphScene, self).mousePressEvent(event)
+
+    # allow QGraphicsScene size to shrink back
+    def mouseMoveEvent(self, _event):
+        self.setSceneRect(self.itemsBoundingRect())
+        super(GraphScene, self).mouseMoveEvent(_event)
+
diff --git a/python/graphs_manager.py b/python/graphs_manager.py
index 63ca2eb6cbed16212f190b47fb835e366cb4e8da..8e6aa0637c43edefa28cb30a4d4f500786394330 100644
--- a/python/graphs_manager.py
+++ b/python/graphs_manager.py
@@ -3,15 +3,15 @@ from PyQt5.QtCore import pyqtSignal # for signal/slot support
 from PyQt5.QtCore import pyqtSlot # for signal/slot support
 
 class GraphsManager(QObject):
-    """Provides graphs (list<GraphItem>) and signals when the graph list
+    """Provides graphs (dict<dict>) and signals when the graph dict
     is loaded or cleared.
 
-    Register with signal_graphs_loaded to provide current graph list.
+    Register with signal_graphs_loaded to provide current graph dict.
 
     Data structures:
       * schema_name (str)
       * scope (int)
-      * graphs (list<GraphItem>)
+      * graphs (dict<dict>)
 
     Signals:
       * signal_graphs_loaded() - graphs were loaded or cleared
@@ -48,17 +48,18 @@ class GraphsManager(QObject):
     def appearance_changed(self, old_settings, new_settings):
         # make all graphs need appearance_set
         for graph in self.graphs:
-            graph.appearance_set = False
+            if "graph_item" in graph:
+                graph.appearance_set = False
 
-        # maybe change spacing
-        if old_settings["graph_h_spacing"] != new_settings["graph_h_spacing"]:
-            for graph in self.graphs:
+                # maybe change spacing
+                if old_settings["graph_h_spacing"] \
+                                != new_settings["graph_h_spacing"]:
                 graph.change_h_spacing(old_settings["graph_h_spacing"],
                                        old_settings["node_width"]/2,
                                        new_settings["graph_h_spacing"],
                                        new_settings["node_width"]/2)
-        if old_settings["graph_v_spacing"] != new_settings["graph_v_spacing"]:
-            for graph in self.graphs:
+                if old_settings["graph_v_spacing"]
+                                != new_settings["graph_v_spacing"]:
                 graph.change_v_spacing(old_settings["graph_v_spacing"],
                                        old_settings["node_height"]/2,
                                        new_settings["graph_v_spacing"],
@@ -69,10 +70,8 @@ class GraphsManager(QObject):
 
     # return graph associated with graph_index else None
     def find_graph(self, graph_index):
-        for graph in self.graphs:
-            if graph.index == graph_index:
-                # return subscript
-                return graph
+        if graph_index in self.graphs:
+            return self.graphs[graph_index]
         # not found
         return None
 
diff --git a/python/gui_manager.py b/python/gui_manager.py
index 63396055f141675aaf8226f684d972941b9ff3b1..4254bf4277a3d90477305c33f49166c799bf1247 100644
--- a/python/gui_manager.py
+++ b/python/gui_manager.py
@@ -631,8 +631,8 @@ class GUIManager(QObject):
 
         else:
             # accept request
-            status, graphs = mp_code_io_manager.read_generated_json(
-                                                       generated_json_text)
+            status, global_view_specification, graphs = \
+                    mp_code_io_manager.read_generated_json(generated_json_text)
 
             if status:
                 # log failure
diff --git a/python/mp_code_io_manager.py b/python/mp_code_io_manager.py
index 7b3f20b846e20828929f6972772dd30ea9d401dc..5c691189e34febbbf437f8ecd845bb5f7eaf464b 100644
--- a/python/mp_code_io_manager.py
+++ b/python/mp_code_io_manager.py
@@ -31,8 +31,9 @@ read_generated_json(generated_json):
 import_gry_file(gry_filename):
     import JGF Gryphon file.  Return (status, metadata fields and graphs)
 
-NOTE: RIGAL functions read/write in same directory, so we change to the
-scratch RIGAL work directory to work, then move back when done.
+export_gry_file(gry_filename, mp_code_text, scope,
+                   selected_index, scale, x_slider, y_slider, graphs):
+    export JGF Gryphon file.  Return status
 """
 
 # read MP Code file.  Return (status, mp_code_text)
@@ -69,7 +70,8 @@ def save_mp_code_file(mp_code_text, mp_code_filename):
 # NOTE: per request, removes underscores from node label
 def read_generated_json(generated_json_text):
 
-    graphs = list()
+    graphs = dict()
+    global_view_specification = dict() # zz not populated yet
     recommended_node_width = settings["node_width"]
     recommended_node_height = settings["node_height"]
     graph_h_spacing = settings["graph_h_spacing"]
@@ -87,10 +89,11 @@ def read_generated_json(generated_json_text):
         if "GLOBAL" in generated_json:
             print("Implementation TBD for GLOBAL:%s"%generated_json["GLOBAL"])
 
-        i = 1
+        trace_index = 1
         for trace in generated_json["traces"]:
             nodes = list()
             edges = list()
+            trace_views = dict()
 
             # build graph from trace
             # item 0: graph mark, "U" or "M"
@@ -101,25 +104,25 @@ def read_generated_json(generated_json_text):
 
             # item 2: event list of nodes
             node_lookup = dict() # helper for connecting edges
-            json_nodes = trace[2]
+            json_nodes = trace[2] # event list
             y_extra_say = 0
-            for json_node in json_nodes:
+            for json_node in json_nodes: # event in event list
 
-                # prepare label without underscores
-                label = json_node[0].replace('_'," ")
+                # prepare event name without underscores
+                event_name = json_node[0].replace('_'," ")
 
-                # get x and y, not available in older JSON
+                # compute x and y
                 x=json_node[3] * graph_h_spacing + recommended_node_width/2
                 y=json_node[4] * graph_v_spacing + recommended_node_height/2
                 if json_node[1] == "T": # SAY
                     # spread out SAY boxes more
-                    y_extra_say += 22 * (len(label)//20)
+                    y_extra_say += 22 * (len(event_name)//20)
                     y += y_extra_say
 #                    y=json_node[4]*1.8*graph_v_spacing+recommended_node_height
 
                 # node_id, type, name, x, y, hide, collapse, collapse_below
                 node = Node("%s"%json_node[2], json_node[1],
-                              label, x, y, False, False, False)
+                              event_name, x, y, False, False, False)
                 nodes.append(node)
                 node_lookup[node.node_id] = node
 
@@ -139,30 +142,39 @@ def read_generated_json(generated_json_text):
                                   "%s"%json_edge[0], "",
                                   node_lookup))
 
-            # items 5...n-1: the user-defined named relations
-            for user_defined_json_edges in trace[5:]:
-                if isinstance(user_defined_json_edges, list):
-                    label = user_defined_json_edges[0]
-                    for json_edge in user_defined_json_edges[1:]:
-                        # source_id, relation, target_id, label
+            # items 5...: list of user-defined named relations and dict of views
+            for type_specific_element in trace[5:]:
+                if isinstance(type_specific_element, list):
+                    relation_name = type_specific_element[0]
+                    for json_edge in type_specific_element[1:]:
+                        # source_id, relation, target_id, relation_name
                         edges.append(Edge("%s"%json_edge[0],
                                           "USER_DEFINED",
-                                          "%s"%json_edge[1], label,
+                                          "%s"%json_edge[1], relation_name,
                                           node_lookup))
-                elif isinstance(user_defined_json_edges, dict):
+                elif isinstance(type_specific_element, dict):
                     # VIEWS
-                    for key, value in user_defined_json_edges.items():
+                    for key, value in type_specific_element.items():
                         if value:
+                            views[key] = value
                             print("Implementation TBD for %s:%s"%(key, value))
 
-            # build graph item from this
-            graph_item = GraphItem(i, graph_mark, trace_probability,
-                                                             nodes, edges)
-            graphs.append(graph_item)
-
-            i += 1
-
-        return ("", graphs)
+#            # build graph item from this
+#            graph_item = GraphItem(trace_index, graph_mark, trace_probability,
+#                                                             nodes, edges)
+            graph = {"index":trace_index,
+                     "mark":graph_mark,
+                     "probability":trace_probability,
+                     "nodes":nodes,
+                     "edges":edges,
+                     "trace_views":trace_views,
+                     "graph_item":None # calculated just-in-time
+                    }
+
+            graphs[trace_index] = graph
+            trace_index += 1
+
+        return ("", global_view_specification, graphs)
 
     except Exception as e:
         print("Error reading generated JSON text: %s" % (repr(e)))