← Back to team overview

yellow team mailing list archive

[Merge] lp:~bcsaller/juju-gui/topology-panzoom into lp:juju-gui

 

Benjamin Saller has proposed merging lp:~bcsaller/juju-gui/topology-panzoom into lp:juju-gui.

Requested reviews:
  Juju GUI Hackers (juju-gui)

For more details, see:
https://code.launchpad.net/~bcsaller/juju-gui/topology-panzoom/+merge/140671

Panzoom Module

This branch breaks out the first module
from Mega into a more module unit. While this
module is small a number of changes occur to make
this happen. The framework underwent some improvemnts,
changes around interaction with App and view replacement
occured, event bindings had to be updated. A pattern for 
cross module event firing was established.

In future modules, the topo/component fires the events and
modules are bubble targets.

https://codereview.appspot.com/6971045/

-- 
https://code.launchpad.net/~bcsaller/juju-gui/topology-panzoom/+merge/140671
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~bcsaller/juju-gui/topology-panzoom into lp:juju-gui.
=== modified file 'app/app.js'
--- app/app.js	2012-12-14 20:25:16 +0000
+++ app/app.js	2012-12-19 13:49:21 +0000
@@ -537,37 +537,22 @@
      * @method show_environment
      */
     show_environment: function(req, res, next) {
-      var view = this.getViewInfo('environment'),
-          instance = view.instance,
-          self = this;
-      if (!instance) {
-        console.log('new env view');
-        this.showView('environment',
-            { getModelURL: Y.bind(this.getModelURL, this),
-              /** A simple closure so changes to the value are available.*/
-              getServiceEndpoints: function() {return self.serviceEndpoints;},
-              loadService: this.loadService,
-              db: this.db,
-              env: this.env},
-            {render: true});
-      } else {
-        /* The current impl makes extensive use of
-         * event handlers which are not being properly rebound
-         * when the view is attached.  There is a workable pattern
-         * to enable this but we have to land the basics of this branch
-         * first.
-         */
-        this.showView('environment',
-            { getModelURL: Y.bind(this.getModelURL, this),
-              /** A simple closure so changes to the value are available.*/
-              getServiceEndpoints: function() {return self.serviceEndpoints;},
-              loadService: this.loadService,
-              db: this.db,
-              env: this.env},
-            { update: false,
-              render: true,
-              callback: function(view) {view.postRender();}});
-      }
+      var self = this,
+          view = this.getViewInfo('environment'),
+          options = {
+            getModelURL: Y.bind(this.getModelURL, this),
+            /** A simple closure so changes to the value are available.*/
+            getServiceEndpoints: function() {
+              return self.serviceEndpoints;},
+            loadService: this.loadService,
+            db: this.db,
+            env: this.env};
+
+      this.showView('environment', options, {
+        callback: function() {
+          this.views.environment.instance.postRender();
+        },
+        render: true});
     },
 
     /**

=== modified file 'app/assets/javascripts/d3-components.js'
--- app/assets/javascripts/d3-components.js	2012-12-14 15:25:55 +0000
+++ app/assets/javascripts/d3-components.js	2012-12-19 13:49:21 +0000
@@ -34,6 +34,7 @@
     initializer: function() {
       this.events = Y.mix(this.events, this._defaultEvents,
                           false, undefined, 0, true);
+
     },
 
     componentBound: function() {},
@@ -116,6 +117,9 @@
       this.events[module.name] = modEvents;
       this.bind(module.name);
       module.componentBound();
+
+      // Add Module as an event target of Component
+      this.addTarget(module);
       return this;
     },
 
@@ -129,10 +133,51 @@
     removeModule: function(moduleName) {
       this.unbind(moduleName);
       delete this.events[moduleName];
+      this.removeTarget(this.modules[moduleName]);
       delete this.modules[moduleName];
       return this;
     },
 
+    // Return a resolved handler object in the form
+    // {phase: str, callback: function}
+    _normalizeHandler: function(handler, module, selector) {
+      var result = {};
+
+      if (L.isString(handler)) {
+        result.callback = module[handler];
+        result.phase = 'on';
+      }
+
+      if (L.isObject(handler)) {
+        result.phase = handler.phase || 'on';
+        result.callback = handler.callback;
+      }
+
+      if (L.isString(result.callback)) {
+        result.callback = module[result.callback];
+      }
+
+      if (!result.callback) {
+        console.error('No Event handler for', selector, module.name);
+        return;
+      }
+      if (!L.isFunction(result.callback)) {
+        console.error('Unable to resolve a proper callback for',
+                      selector, handler, module.name, result);
+        return;
+      }
+      // Set up binding context for callback.
+      result.context = module;
+      if (handler.context) {
+        if (handler.context === 'component') {
+          result.context = this;
+        } else if (handler.context === 'window') {
+          result.context = Y.one('window');
+        }
+      }
+      return result;
+    },
+
     /**
      * Internal implementation of binding both Module.events.scene and
      * Module.events.yui.
@@ -163,49 +208,12 @@
             Y.delegate(name, d3Adaptor, container, selector, context));
       }
 
-      // Return a resolved handler object in the form
-      // {phase: str, callback: function}
-      function _normalizeHandler(handler, module, selector) {
-        var result = {};
-
-        if (L.isString(handler)) {
-          result.callback = module[handler];
-          result.phase = 'on';
-        }
-
-        if (L.isObject(handler)) {
-          result.phase = handler.phase || 'on';
-          result.callback = handler.callback;
-        }
-
-        if (L.isString(result.callback)) {
-          result.callback = module[result.callback];
-        }
-
-        if (!result.callback) {
-          console.error('No Event handler for', selector, modName);
-          return;
-        }
-        if (!L.isFunction(result.callback)) {
-          console.error('Unable to resolve a proper callback for',
-                        selector, handler, modName, result);
-          return;
-        }
-        // Set up binding context for callback.
-        result.context = module;
-        if (handler.context &&
-            handler.context === 'component') {
-          result.context = self;
-        }
-        return result;
-      }
-
       this.unbind(modName);
 
       // Bind 'scene' events
       Y.each(modEvents.scene, function(handlers, selector, sceneEvents) {
         Y.each(handlers, function(handler, trigger) {
-          handler = _normalizeHandler(handler, module, selector);
+          handler = self._normalizeHandler(handler, module, selector);
           if (L.isValue(handler)) {
             _bindEvent(trigger, handler.callback,
                        container, selector, handler.context);
@@ -222,7 +230,7 @@
         Y.each(['after', 'before', 'on'], function(eventPhase) {
           var resolvedHandler = {};
           Y.each(modEvents.yui, function(handler, name) {
-            handler = _normalizeHandler(handler, module, name);
+            handler = self._normalizeHandler(handler, module, name);
             if (!handler || handler.phase !== eventPhase) {
               return;
             }
@@ -235,8 +243,17 @@
               // this signature: Y.on(event, callback, target, context).
               // For this reason, it is not possible here to just pass the
               // context as third argument.
-              var callback = Y.bind(handler.callback, handler.context);
-              subscriptions.push(Y[eventPhase](name, callback));
+              var target = self,
+                  callback = Y.bind(handler.callback, handler.context);
+              if (Y.Array.indexOf(['windowresize'], name) !== -1) {
+                target = Y;
+              } else {
+                // (re)Register the event to bubble.
+                self.publish(name, {emitFacade: true});
+              }
+              subscriptions.push(
+                  target[eventPhase](
+                  name, callback, handler.context));
             });
           }
         });
@@ -278,30 +295,60 @@
     _bindD3Events: function(modName) {
       // Walk each selector for a given module 'name', doing a
       // d3 selection and an 'on' binding.
-      var modEvents = this.events[modName],
+      var self = this,
+          modEvents = this.events[modName],
           owns = Y.Object.owns,
           module;
+
       if (!modEvents || !modEvents.d3) {
         return;
       }
       modEvents = modEvents.d3;
       module = this.modules[modName];
 
-      function _normalizeHandler(handler, module) {
-        if (handler && !L.isFunction(handler)) {
-          handler = module[handler];
-        }
-        return handler;
-      }
-
       Y.each(modEvents, function(handlers, selector) {
         Y.each(handlers, function(handler, trigger) {
-          handler = _normalizeHandler(handler, module);
-          d3.selectAll(selector).on(trigger, handler);
+          var adapter;
+          handler = self._normalizeHandler(handler, module);
+          // Create an adaptor
+          adapter = function() {
+            var selection = d3.select(this),
+                d = selection.data()[0];
+            // This is a minor violation (extension)
+            // of the interface, but suits us well.
+            return handler.callback.call(this, d, handler.context);
+          };
+          d3.selectAll(selector).on(trigger, adapter);
         });
       });
     },
 
+    bindAllD3Events: function() {
+      var self = this;
+      Y.each(this.modules, function(mod, name) {
+        self._bindD3Events(name);
+      });
+    },
+
+    /**
+     * Register a manual event subscription on
+     * behalf of a module.
+     *
+     * @method recordSubscription
+     * @param {Module} module to record relative to.
+     * @param {Object} YUI event subscription.
+     * @chainable
+     **/
+    recordSubscription: function(module, subscription) {
+      if (!(module.name in this.events)) {
+        throw 'Unable able to recordSubscription, module not added.';
+      }
+      if (!subscription) {
+        throw 'Invalid/undefined subscription object cannot be recorded.';
+      }
+      this.events[module.name].subscriptions.push(subscription);
+    },
+
     /**
       Internal Detail. Called by unbind automatically.
      * D3 events follow a 'slot' like system. Setting the
@@ -360,8 +407,9 @@
      *
      * Called the first time render is invoked. See {render}.
      **/
-    renderOnce: function() {},
-
+    renderOnce: function() {
+      this.all('renderOnce');
+    },
     /**
      * Render each module bound to the canvas. The first call to
      * render() will automatically call renderOnce (a noop by default)
@@ -419,7 +467,7 @@
     detachContainer: function() {
       var container = this.get('container');
       if (container.inDoc()) {
-        container.remove();
+        container.one('.topology').remove();
       }
       return container;
     },
@@ -432,10 +480,17 @@
      * @chainable
      */
     update: function() {
-      Y.each(Y.Object.values(this.modules), function(mod) {
-        mod.update();
+      this.all('update');
+      return this;
+    },
+
+    all: function(methodName) {
+      Y.each(this.modules, function(mod, name) {
+        if (methodName in mod) {
+          console.log('Component', methodName, 'on', name);
+          mod[methodName]();
+        }
       });
-      return this;
     }
   }, {
     ATTRS: {

=== modified file 'app/templates/overview.handlebars'
--- app/templates/overview.handlebars	2012-12-11 04:11:39 +0000
+++ app/templates/overview.handlebars	2012-12-19 13:49:21 +0000
@@ -1,5 +1,5 @@
 <div class="topology">
-    <div class="crosshatch-background">
+    <div class="topology-canvas crosshatch-background">
         <div class="environment-menu" id="service-menu">
             <div class="triangle">&nbsp;</div>
             <ul>

=== modified file 'app/views/environment.js'
--- app/views/environment.js	2012-12-11 03:58:03 +0000
+++ app/views/environment.js	2012-12-19 13:49:21 +0000
@@ -23,7 +23,9 @@
       {
         initializer: function() {
           console.log('View: Initialized: Env');
-          this.publish('navigateTo', {preventable: false});
+          this.publish('navigateTo', {
+            broadcast: true,
+            preventable: false});
         },
 
         render: function() {
@@ -48,18 +50,24 @@
               container: container});
             // Bind all the behaviors we need as modules.
             topo.addModule(views.MegaModule);
+            topo.addModule(views.PanZoomModule);
 
             topo.addTarget(this);
             this.topo = topo;
           }
+
           topo.render();
           return this;
         },
-        // XXX: This method is a pass through,
-        // it will be removed when we move to
-        // incremental rendering.
+
         postRender: function() {
-          this.topo.modules.MegaModule.postRender();
+          this.topo.attachContainer();
+          this.topo.fire('rendered');
+          // Bind d3 events (manually)
+          // this needs to be postRender and
+          // the jiggle in phases has broken
+          // the existing (from change to showView)
+          this.topo.bindAllD3Events();
         }
       }, {
         ATTRS: {}
@@ -68,15 +76,15 @@
   views.environment = EnvironmentView;
 }, '0.1.0', {
   requires: ['juju-templates',
-    'juju-view-utils',
-    'juju-models',
-    'd3',
-    'd3-components',
-    'base-build',
-    'handlebars-base',
-    'node',
-    'svg-layouts',
-    'event-resize',
-    'slider',
-    'view']
+             'juju-view-utils',
+             'juju-models',
+             'd3',
+             'd3-components',
+             'base-build',
+             'handlebars-base',
+             'node',
+             'svg-layouts',
+             'event-resize',
+             'slider',
+             'view']
 });

=== modified file 'app/views/topology/mega.js'
--- app/views/topology/mega.js	2012-12-11 03:58:03 +0000
+++ app/views/topology/mega.js	2012-12-19 13:49:21 +0000
@@ -53,12 +53,10 @@
             .attr('class', 'unit-count hide-count');
           }}
         },
-
         '.rel-label': {
           click: 'relationClick',
           mousemove: 'mousemove'
         },
-
         '.topology .crosshatch-background rect:first-child': {
           /**
            * If the user clicks on the background we cancel any active add
@@ -81,9 +79,6 @@
             self.backgroundClicked();
           }}
         },
-
-        '#zoom-out-btn': {click: 'zoom_out'},
-        '#zoom-in-btn': {click: 'zoom_in'},
         '.graph-list-picker .picker-button': {
           click: 'showGraphListPicker'
         },
@@ -132,9 +127,9 @@
       },
       d3: {
         '.service': {
-          'mousedown.addrel': {callback: function(d, self) {
+          'mousedown.addrel': {callback: function(d, context) {
             var evt = d3.event;
-            self.longClickTimer = Y.later(750, this, function(d, e) {
+            context.longClickTimer = Y.later(750, this, function(d, e) {
               // Provide some leeway for accidental dragging.
               if ((Math.abs(d.x - d.oldX) + Math.abs(d.y - d.oldY)) /
                   2 > 5) {
@@ -146,25 +141,27 @@
               d3.event = e;
 
               // Start the process of adding a relation
-              self.addRelationDragStart(d, self);
+              context.addRelationDragStart(d, context);
             }, [d, evt], false);
           }},
-          'mouseup.addrel': {callback: function(d, self) {
+          'mouseup.addrel': {callback: function(d, context) {
             // Cancel the long-click timer if it exists.
-            if (self.longClickTimer) {
-              self.longClickTimer.cancel();
+            if (context.longClickTimer) {
+              context.longClickTimer.cancel();
             }
           }}
         }
       },
       yui: {
-        windowresize: 'setSizesFromViewport'
+        windowresize: {
+          callback: 'setSizesFromViewport',
+          context: 'module'},
+        rendered: 'renderedHandler'
       }
     },
 
     initializer: function(options) {
       MegaModule.superclass.constructor.apply(this, arguments);
-      this.publish('navigateTo', {preventable: false});
 
       // Build a service.id -> BoundingBox map for services.
       this.service_boxes = {};
@@ -173,83 +170,12 @@
       this.set('currentServiceClickAction', 'toggleControlPanel');
     },
 
-    render: function() {
-      MegaModule.superclass.render.apply(this, arguments);
-      var container = this.get('container');
-      container.setHTML(Templates.overview());
-      this.svg = container.one('.topology');
-
-      this.renderOnce();
-
-      return this;
-    },
-    /*
-     * Construct a persistent scene that is managed in update.
-     */
-    renderOnce: function() {
-      var self = this,
-          container = this.get('container'),
-          height = 600,
-          width = 640,
-          fill = d3.scale.category20();
-
-      this.service_scale = d3.scale.log().range([150, 200]);
-      this.service_scale_width = d3.scale.log().range([164, 200]),
-      this.service_scale_height = d3.scale.log().range([64, 100]);
-      this.xscale = d3.scale.linear()
-      .domain([-width / 2, width / 2])
-      .range([0, width]),
-      this.yscale = d3.scale.linear()
-      .domain([-height / 2, height / 2])
-      .range([height, 0]);
-
-      // Create a pan/zoom behavior manager.
-      var zoom = d3.behavior.zoom()
-      .x(this.xscale)
-      .y(this.yscale)
-      .scaleExtent([0.25, 2.0])
-      .on('zoom', function() {
-            // Keep the slider up to date with the scale on other sorts
-            // of zoom interactions
-            var s = self.slider;
-            s.set('value', Math.floor(d3.event.scale * 100));
-            self.rescale(vis, d3.event);
-          });
-      self.zoom = zoom;
-
-      // Set up the visualization with a pack layout.
-      var vis = d3.select(container.getDOMNode())
-      .select('.crosshatch-background')
-      .append('svg:svg')
-      .attr('pointer-events', 'all')
-      .attr('width', width)
-      .attr('height', height)
-      .append('svg:g')
-      .call(zoom)
-          // Disable zoom on double click.
-      .on('dblclick.zoom', null)
-      .append('g');
-
-      vis.append('svg:rect')
-      .attr('class', 'graph')
-      .attr('fill', 'rgba(255,255,255,0)');
-
-      this.vis = vis;
-      this.tree = d3.layout.pack()
-      .size([width, height])
-      .value(function(d) {
-            return Math.max(d.unit_count, 1);
-          })
-      .padding(300);
-
-      this.updateCanvas();
-    },
-
     serviceClick: function(d, context) {
       // Ignore if we clicked outside the actual service node.
-      var container = context.get('container'),
-              mouse_coords = d3.mouse(container.one('svg').getDOMNode());
-      if (!d.containsPoint(mouse_coords, context.zoom)) {
+      var topo = context.get('component'),
+          container = context.get('container'),
+          mouse_coords = d3.mouse(container.one('svg').getDOMNode());
+      if (!d.containsPoint(mouse_coords, topo.zoom)) {
         return;
       }
       // Get the current click action
@@ -321,8 +247,9 @@
      */
     updateData: function() {
       //model data
-      var vis = this.vis,
-          db = this.get('component').get('db'),
+      var topo = this.get('component'),
+          vis = topo.vis,
+          db = topo.get('db'),
           relations = db.relations.toArray(),
           services = db.services.map(views.toBoundingBox);
 
@@ -352,17 +279,33 @@
       // Nodes are mapped by modelId tuples.
       this.node = vis.selectAll('.service')
                        .data(services, function(d) {
-                return d.modelId();});
+                         return d.modelId();});
     },
 
     /*
      * Attempt to reuse as much of the existing graph and view models
      * as possible to re-render the graph.
      */
-    updateCanvas: function() {
+    update: function() {
       var self = this,
-              tree = this.tree,
-              vis = this.vis;
+          topo = this.get('component'),
+          width = topo.get('width'),
+          height = topo.get('height');
+
+      if (!this.service_scale) {
+        this.service_scale = d3.scale.log().range([150, 200]);
+        this.service_scale_width = d3.scale.log().range([164, 200]),
+        this.service_scale_height = d3.scale.log().range([64, 100]);
+      }
+
+      if (!this.tree) {
+        this.tree = d3.layout.pack()
+                      .size([width, height])
+                      .value(function(d) {
+                          return Math.max(d.unit_count, 1);
+                        })
+                      .padding(300);
+      }
 
       //Process any changed data.
       this.updateData();
@@ -396,7 +339,6 @@
                   // Clear any state while dragging.
                   self.get('container').all('.environment-menu.active')
                     .removeClass('active');
-                  self.service_click_actions.toggleControlPanel(null, self);
                   self.cancelRelationBuild();
 
                   // Update relation lines for just this service.
@@ -432,7 +374,6 @@
                 .attr('y2', t[1]);
           rel_group.select('.rel-label')
                 .attr('transform', function(d) {
-                // XXX: This has to happen on update, not enter
                 return 'translate(' +
                     [Math.max(s[0], t[0]) -
                          Math.abs((s[0] - t[0]) / 2),
@@ -459,32 +400,21 @@
 
       // enter
       node
-            .enter().append('g')
-            .attr('class', function(d) {
+        .enter().append('g')
+        .attr('class', function(d) {
             return (d.subordinate ? 'subordinate ' : '') + 'service';
           })
-            .call(drag)
-            .on('mousedown.addrel', function(d) {
-                self.d3Events['.service']['mousedown.addrel']
-                .call(this, d, self, d3.event);
-              })
-            .on('mouseup.addrel', function(d) {
-                self.d3Events['.service']['mouseup.addrel']
-                .call(this, d, self, d3.event);
-              })
-            .attr('transform', function(d) {
-                return d.translateStr();});
+        .call(drag)
+        .attr('transform', function(d) {
+            return d.translateStr();
+          });
 
       // Update
       this.drawService(node);
 
       // Exit
       node.exit()
-            .call(function(d) {
-            // TODO: update the service_boxes
-            // removing the bound data
-          })
-            .remove();
+          .remove();
 
       function updateLinks() {
         // Enter.
@@ -509,10 +439,11 @@
     drawRelationGroup: function() {
       // Add a labelgroup.
       var self = this,
-              g = self.vis.selectAll('g.rel-group')
-                  .data(self.rel_pairs, function(r) {
-                    return r.modelIds();
-                  });
+          vis = this.get('component').vis,
+          g = vis.selectAll('g.rel-group')
+                 .data(self.rel_pairs, function(r) {
+                   return r.modelIds();
+                 });
 
       var enter = g.enter();
 
@@ -534,7 +465,7 @@
                     'relation';
               });
 
-      g.selectAll('rel-label').remove();
+      g.selectAll('.rel-label').remove();
       g.selectAll('text').remove();
       g.selectAll('rect').remove();
       var label = g.append('g')
@@ -851,31 +782,6 @@
             p.scope === 'container';
       });
     },
-    renderSlider: function() {
-      var self = this,
-              value = 100,
-              currentScale = this.get('scale');
-      // Build a slider to control zoom level
-      if (currentScale) {
-        value = currentScale * 100;
-      }
-      var slider = new Y.Slider({
-        min: 25,
-        max: 200,
-        value: value
-      });
-      slider.render('#slider-parent');
-      slider.after('valueChange', function(evt) {
-        // Don't fire a zoom if there's a zoom event already in progress;
-        // that will run rescale for us.
-        if (d3.event && d3.event.scale && d3.event.translate) {
-          return;
-        }
-        self._fire_zoom((evt.newVal - evt.prevVal) / 100);
-      });
-      self.slider = slider;
-    },
-
     /*
          * Utility method to get a service object from the DB
          * given a BoundingBox.
@@ -919,9 +825,11 @@
          * in app.showView(), and in testing, it needs to be called manually,
          * if the test relies on any of this data.
          */
-    postRender: function() {
+    renderedHandler: function() {
       var container = this.get('container');
 
+      this.update();
+
       // Set the sizes from the viewport.
       this.setSizesFromViewport();
 
@@ -932,29 +840,6 @@
               .setAttribute('x', -width / 2);
       });
 
-      // Preserve zoom when the scene is updated.
-      var changed = false,
-              currentScale = this.get('scale'),
-              currentTranslate = this.get('translate');
-      if (currentTranslate && currentTranslate !== this.zoom.translate()) {
-        this.zoom.translate(currentTranslate);
-        changed = true;
-      }
-      if (currentScale && currentScale !== this.zoom.scale()) {
-        this.zoom.scale(currentScale);
-        changed = true;
-      }
-      if (changed) {
-        this._fire_zoom(0);
-      }
-
-      // Render the slider after the view is attached.
-      // Although there is a .syncUI() method on sliders, it does not
-      // seem to play well with the app framework: the slider will render
-      // the first time, but on navigation away and back, will not
-      // re-render within the view.
-      this.renderSlider();
-
       // Chainable method.
       return this;
     },
@@ -975,14 +860,16 @@
 
     addRelationDragStart: function(d, context) {
       // Create a pending drag-line.
-      var dragline = this.vis.append('line')
-              .attr('class', 'relation pending-relation dragline dragging'),
-              self = this;
+      var vis = this.get('component').vis,
+          dragline = vis.append('line')
+                        .attr('class',
+                              'relation pending-relation dragline dragging'),
+          self = this;
 
       // Start the line between the cursor and the nearest connector
       // point on the service.
-      var mouse = d3.mouse(Y.one('svg').getDOMNode());
-      self.cursorBox = views.BoundingBox();
+      var mouse = d3.mouse(Y.one('.topology svg').getDOMNode());
+      self.cursorBox = new views.BoundingBox();
       self.cursorBox.pos = {x: mouse[0], y: mouse[1], w: 0, h: 0};
       var point = self.cursorBox.getConnectorPair(d);
       dragline.attr('x1', point[0][0])
@@ -1091,6 +978,7 @@
     },
 
     cancelRelationBuild: function() {
+      var vis = this.get('component').vis;
       if (this.dragline) {
         // Get rid of our drag line
         this.dragline.remove();
@@ -1099,7 +987,7 @@
       this.clickAddRelation = null;
       this.set('currentServiceClickAction', 'toggleControlPanel');
       this.buildingRelation = false;
-      this.show(this.vis.selectAll('.service'))
+      this.show(vis.selectAll('.service'))
                   .classed('selectable-service', false);
     },
 
@@ -1128,10 +1016,12 @@
      */
     startRelation: function(service) {
       // Set flags on the view that indicate we are building a relation.
+      var vis = this.get('component').vis;
+
       this.buildingRelation = true;
       this.clickAddRelation = true;
 
-      this.show(this.vis.selectAll('.service'));
+      this.show(vis.selectAll('.service'));
 
       var db = this.get('component').get('db'),
           getServiceEndpoints = this.get('component')
@@ -1157,7 +1047,7 @@
       // Rather than two loops this marks
       // all services as selectable and then
       // removes the invalid ones.
-      this.fade(this.vis.selectAll('.service')
+      this.fade(vis.selectAll('.service')
               .classed('selectable-service', true)
               .filter(function(d) {
                 return (d.id in invalidRelationTargets &&
@@ -1171,73 +1061,6 @@
       this.set('currentServiceClickAction', 'ambiguousAddRelationCheck');
     },
 
-
-    /*
-         * Zoom in event handler.
-         */
-    zoom_out: function(data, context) {
-      var slider = context.slider,
-              val = slider.get('value');
-      slider.set('value', val - 25);
-    },
-
-    /*
-         * Zoom out event handler.
-         */
-    zoom_in: function(data, context) {
-      var slider = context.slider,
-              val = slider.get('value');
-      slider.set('value', val + 25);
-    },
-
-    /*
-         * Wraper around the actual rescale method for zoom buttons.
-         */
-    _fire_zoom: function(delta) {
-      var vis = this.vis,
-              zoom = this.zoom,
-              evt = {};
-
-      // Build a temporary event that rescale can use of a similar
-      // construction to d3.event.
-      evt.translate = zoom.translate();
-      evt.scale = zoom.scale() + delta;
-
-      // Update the scale in our zoom behavior manager to maintain state.
-      zoom.scale(evt.scale);
-
-      // Update the translate so that we scale from the center
-      // instead of the origin.
-      var rect = vis.select('rect');
-      evt.translate[0] -= parseInt(rect.attr('width'), 10) / 2 * delta;
-      evt.translate[1] -= parseInt(rect.attr('height'), 10) / 2 * delta;
-      zoom.translate(evt.translate);
-
-      this.rescale(vis, evt);
-    },
-
-    /*
-         * Rescale the visualization on a zoom/pan event.
-         */
-    rescale: function(vis, evt) {
-      // Make sure we don't scale outside of our bounds.
-      // This check is needed because we're messing with d3's zoom
-      // behavior outside of mouse events (e.g.: with the slider),
-      // and can't trust that zoomExtent will play well.
-      var new_scale = Math.floor(evt.scale * 100);
-      if (new_scale < 25 || new_scale > 200) {
-        evt.scale = this.get('scale');
-      }
-      // Store the current value of scale so that it can be restored later.
-      this.set('scale', evt.scale);
-      // Store the current value of translate as well, by copying the event
-      // array in order to avoid reference sharing.
-      this.set('translate', evt.translate.slice(0));
-      vis.attr('transform', 'translate(' + evt.translate + ')' +
-              ' scale(' + evt.scale + ')');
-      this.updateServiceMenuLocation();
-    },
-
     /*
          * Event handler to show the graph-list picker
          */
@@ -1293,14 +1116,16 @@
       // affect the page size, such as the charm panel, to get out of the
       // way before we compute sizes.  Note the
       // "afterPageSizeRecalculation" event at the end of this function.
-      Y.fire('beforePageSizeRecalculation');
       // start with some reasonable defaults
-      var vis = this.vis,
-              container = this.get('container'),
-              xscale = this.xscale,
-              yscale = this.yscale,
-              svg = container.one('svg'),
-              canvas = container.one('.crosshatch-background');
+      var topo = this.get('component'),
+          container = this.get('container'),
+          vis = topo.vis,
+          xscale = topo.xScale,
+          yscale = topo.yScale,
+          svg = container.one('svg'),
+          canvas = container.one('.topology-canvas');
+
+      topo.fire('beforePageSizeRecalculation');
       // Get the canvas out of the way so we can calculate the size
       // correctly (the canvas contains the svg).  We want it to be the
       // smallest size we accept--no smaller or bigger--or else the
@@ -1321,25 +1146,26 @@
             .setStyle('width', dimensions.width);
 
       // Reset the scale parameters
-      this.xscale.domain([-dimensions.width / 2, dimensions.width / 2])
+      topo.xScale.domain([-dimensions.width / 2, dimensions.width / 2])
             .range([0, dimensions.width]);
-      this.yscale.domain([-dimensions.height / 2, dimensions.height / 2])
+      topo.yScale.domain([-dimensions.height / 2, dimensions.height / 2])
             .range([dimensions.height, 0]);
 
-      this.width = dimensions.width;
-      this.height = dimensions.height;
-      Y.fire('afterPageSizeRecalculation');
+      topo.set('size', [dimensions.width, dimensions.height]);
+      topo.fire('afterPageSizeRecalculation');
     },
 
     /*
          * Update the location of the active service panel
          */
     updateServiceMenuLocation: function() {
-      var container = this.get('container'),
-              cp = container.one('.environment-menu.active'),
-              service = this.get('active_service'),
-              tr = this.zoom.translate(),
-              z = this.zoom.scale();
+      var topo = this.get('component'),
+          container = this.get('container'),
+          cp = container.one('.environment-menu.active'),
+          service = this.get('active_service'),
+          tr = topo.get('translate'),
+          z = topo.get('scale');
+
       if (service && cp) {
         var cp_width = cp.getClientRect().width,
                 menu_left = service.x * z + service.w * z / 2 <
@@ -1375,9 +1201,10 @@
       }
 
       // Do not fire unless we're within the service box.
-      var container = context.get('container'),
+      var topo = context.get('component'),
+          container = context.get('container'),
           mouse_coords = d3.mouse(container.one('svg').getDOMNode());
-      if (!d.containsPoint(mouse_coords, context.zoom)) {
+      if (!d.containsPoint(mouse_coords, topo.zoom)) {
         return;
       }
 
@@ -1413,9 +1240,10 @@
       }
 
       // Do not fire if we're within the service box.
-      var container = self.get('container'),
+      var topo = this.get('component'),
+          container = self.get('container'),
           mouse_coords = d3.mouse(container.one('svg').getDOMNode());
-      if (d.containsPoint(mouse_coords, self.zoom)) {
+      if (d.containsPoint(mouse_coords, topo.zoom)) {
         return;
       }
       var rect = Y.one(this).one('.service-border');
@@ -1486,8 +1314,9 @@
            * View a service
            */
       show_service: function(m, context) {
-        context.get('component')
-        .fire('navigateTo', {url: '/service/' + m.get('id') + '/'});
+        var topo = context.get('component');
+        topo.detachContainer();
+        topo.fire('navigateTo', {url: '/service/' + m.get('id') + '/'});
       },
 
       /*
@@ -1567,11 +1396,12 @@
            * create the relation if not.
            */
       ambiguousAddRelationCheck: function(m, view, context) {
-        var endpoints = view
-                  .get('addRelationStart_possibleEndpoints')[m.id],
-                container = view.get('container');
+        var endpoints = view.get(
+            'addRelationStart_possibleEndpoints')[m.id],
+            container = view.get('container'),
+            topo = view.get('component');
 
-        if (endpoints.length === 1) {
+        if (endpoints && endpoints.length === 1) {
           // Create a relation with the only available endpoint.
           var ep = endpoints[0],
                   endpoints_item = [
@@ -1629,8 +1459,8 @@
         });
 
         // Display the menu at the service endpoint.
-        var tr = view.zoom.translate(),
-                z = view.zoom.scale();
+        var tr = topo.zoom.translate(),
+                z = topo.zoom.scale();
         menu.setStyle('top', m.y * z + tr[1]);
         menu.setStyle('left', m.x * z + m.w * z + tr[0]);
         menu.addClass('active');
@@ -1640,25 +1470,25 @@
       },
 
       /*
-           * Fired when clicking the second service is clicked in the
-           * add relation flow.
-           *
-           * :param endpoints: array of two endpoints, each in the form
-           *   ['service name', {
-           *     name: 'endpoint type',
-           *     role: 'client or server'
-           *   }]
-           */
+       * Fired when clicking the second service is clicked in the
+       * add relation flow.
+       *
+       * :param endpoints: array of two endpoints, each in the form
+       *   ['service name', {
+       *     name: 'endpoint type',
+       *     role: 'client or server'
+       *   }]
+       */
       addRelationEnd: function(endpoints, view, context) {
         // Redisplay all services
         view.cancelRelationBuild();
 
         // Get the vis, and links, build the new relation.
-        var vis = view.vis,
-                env = view.get('component').get('env'),
-                db = view.get('component').get('db'),
-                source = view.get('addRelationStart_service'),
-                relation_id = 'pending:' + endpoints[0][0] + endpoints[1][0];
+        var vis = view.get('component').vis,
+            env = view.get('component').get('env'),
+            db = view.get('component').get('db'),
+            source = view.get('addRelationStart_service'),
+            relation_id = 'pending:' + endpoints[0][0] + endpoints[1][0];
 
         if (endpoints[0][0] === endpoints[1][0]) {
           view.set('currentServiceClickAction', 'toggleControlPanel');
@@ -1676,7 +1506,9 @@
 
         // Firing the update event on the db will properly redraw the
         // graph and reattach events.
-        db.fire('update');
+        //db.fire('update');
+        view.get('component').bindAllD3Events();
+        view.update();
 
         // Fire event to add relation in juju.
         // This needs to specify interface in the future.

=== modified file 'app/views/topology/panzoom.js'
--- app/views/topology/panzoom.js	2012-12-11 03:58:03 +0000
+++ app/views/topology/panzoom.js	2012-12-19 13:49:21 +0000
@@ -6,25 +6,174 @@
       d3ns = Y.namespace('d3');
 
   /**
+   * Handle PanZoom within the a Topology.
+   *
+   * Emitted events:
+   *
+   *  rescaled: post-zoom event, after the scene has been rescaled,
+   *            queried object positions should be accurate.
+   *
    * @module topology-panzoom
    * @class PanZoomModule
    * @namespace views
    **/
   var PanZoomModule = Y.Base.create('PanZoomModule', d3ns.Module, [], {
+
+    events: {
+      scene: {
+        '#zoom-out-btn': {click: 'zoom_out'},
+        '#zoom-in-btn': {click: 'zoom_in'}
+      },
+      yui: {
+        zoom: {callback: 'zoomHandler'},
+        rendered: {callback: 'renderedHandler'}
+      }
+    },
+
     initializer: function(options) {
       PanZoomModule.superclass.constructor.apply(this, arguments);
-    },
-
-    render: function() {
-      PanZoomModule.superclass.render.apply(this, arguments);
-      return this;
+      this._translate = [0, 0];
+      this._scale = 1.0;
+    },
+
+    // Handler for 'zoom' event.
+    zoomHandler: function(evt) {
+      var s = this.slider,
+          vis = this.get('component').vis;
+
+      s.set('value', Math.floor(evt.scale * 100));
+      this.rescale(vis, evt);
+    },
+
+    renderSlider: function() {
+      var self = this,
+          topo = this.get('component'),
+          contianer = topo.get('container'),
+          value = 100,
+          currentScale = topo.get('scale');
+
+      if (self.slider) {
+        return;
+      }
+      // Build a slider to control zoom level
+      if (currentScale) {
+        value = currentScale * 100;
+      }
+      var slider = new Y.Slider({
+        min: 25,
+        max: 200,
+        value: value
+      });
+      slider.render('#slider-parent');
+      topo.recordSubscription(this,
+                              slider.after('valueChange', function(evt) {
+                                // Don't fire a zoom if there's a zoom event
+                                // already in progress; that will run rescale
+                                // for us.
+                                if (d3.event && d3.event.scale &&
+                                    d3.event.translate) {
+                                  return;
+                                }
+                                self._fire_zoom((
+                                    evt.newVal - evt.prevVal) / 100);
+                              }));
+      self.slider = slider;
     },
 
     update: function() {
       PanZoomModule.superclass.update.apply(this, arguments);
       return this;
+    },
+
+    /*
+     * Zoom out event handler.
+     */
+    zoom_out: function(data, context) {
+      var slider = context.slider,
+              val = slider.get('value');
+      slider.set('value', val - 25);
+    },
+
+    /*
+     * Zoom in event handler.
+     */
+    zoom_in: function(data, context) {
+      var slider = context.slider,
+              val = slider.get('value');
+      slider.set('value', val + 25);
+    },
+
+    /*
+     * Wraper around the actual rescale method for zoom buttons.
+     */
+    _fire_zoom: function(delta) {
+      var topo = this.get('component'),
+          vis = topo.vis,
+          zoom = topo.zoom,
+          evt = {};
+
+      // Build a temporary event that rescale can use of a similar
+      // construction to d3.event.
+      evt.translate = zoom.translate();
+      evt.scale = zoom.scale() + delta;
+
+      // Update the scale in our zoom behavior manager to maintain state.
+      zoom.scale(evt.scale);
+
+      // Update the translate so that we scale from the center
+      // instead of the origin.
+      var rect = vis.select('rect');
+      evt.translate[0] -= parseInt(rect.attr('width'), 10) / 2 * delta;
+      evt.translate[1] -= parseInt(rect.attr('height'), 10) / 2 * delta;
+      zoom.translate(evt.translate);
+
+      this.rescale(vis, evt);
+    },
+
+    /*
+     * Rescale the visualization on a zoom/pan event.
+     */
+    rescale: function(vis, evt) {
+      // Make sure we don't scale outside of our bounds.
+      // This check is needed because we're messing with d3's zoom
+      // behavior outside of mouse events (e.g.: with the slider),
+      // and can't trust that zoomExtent will play well.
+      var new_scale = Math.floor(evt.scale * 100),
+          topo = this.get('component');
+
+      if (new_scale < 25 || new_scale > 200) {
+        evt.scale = this.get('scale');
+      }
+      // Store the current value of scale so that it can be restored later.
+      this._scale = evt.scale;
+      // Store the current value of translate as well, by copying the event
+      // array in order to avoid reference sharing.
+      this._translate = Y.mix(evt.translate);
+      vis.attr('transform', 'translate(' + evt.translate + ')' +
+              ' scale(' + evt.scale + ')');
+      topo.fire('rescaled');
+    },
+
+    renderedHandler: function(evt) {
+      // Preserve zoom when the scene is updated.
+      var topo = this.get('component'),
+          changed = false,
+          currentScale = this._scale,
+          currentTranslate = this._translate;
+
+      this.renderSlider();
+      if (currentTranslate && currentTranslate !== topo.get('translate')) {
+        topo.zoom.translate(currentTranslate);
+        changed = true;
+      }
+      if (currentScale && currentScale !== topo.zoom.scale()) {
+        topo.zoom.scale(currentScale);
+        changed = true;
+      }
+      if (changed) {
+        this._fire_zoom(0);
+      }
     }
-
   }, {
     ATTRS: {}
 

=== modified file 'app/views/topology/topology.js'
--- app/views/topology/topology.js	2012-12-05 05:23:37 +0000
+++ app/views/topology/topology.js	2012-12-19 13:49:21 +0000
@@ -14,6 +14,11 @@
    * configuration belong here. If the only shared requirement on shared state
    * is watch/event like behavior fire an event and place the logic in a module.
    *
+   * Emmitted Events:
+   *
+   *  zoom: When the zoom level of the canvas changes a 'zoom'
+   *        event is fired. Analogous to d3's zoom event.
+   *
    * @class Topology
    * @namespace juju.views
    **/
@@ -23,6 +28,33 @@
       this.options = Y.mix(options || {});
     },
 
+    /**
+     * Called by render, conditionally attach container to the DOM if
+     * it isn't already. The framework calls this before module
+     * rendering so that d3 Events will have attached DOM elements. If
+     * your application doesn't need this behavior feel free to override.
+     *
+     * In this case we currently rely on app.showView to do all the
+     * container management, this only works on a preserved view.
+     *
+     * @method attachContainer
+     * @chainable
+     **/
+    attachContainer: function() {
+      return this;
+    },
+
+    /**
+     * Remove container from DOM returning container. This
+     * is explicitly not chainable.
+     *
+     * @method detachContainer
+     **/
+    detachContainer: function() {
+      return;
+    },
+
+
     renderOnce: function() {
       var self = this,
           vis,
@@ -31,30 +63,72 @@
           container = this.get('container'),
           templateName = this.options.template || 'overview';
 
-      if (this.svg) {
+      if (this._templateRendered) {
         return;
       }
-      container.setHTML(views.Templates[templateName]());
+      //container.setHTML(views.Templates[templateName]());
       // Take the first element.
-      this.svg = container.one(':first-child');
+      this._templateRendered = true;
+
+      // Create a pan/zoom behavior manager.
+      this.xScale = d3.scale.linear()
+                      .domain([-width / 2, width / 2])
+                      .range([0, width]);
+      this.yScale = d3.scale.linear()
+                      .domain([-height / 2, height / 2])
+                      .range([height, 0]);
+
+      // Include very basic behavior, fire
+      // yui event for anything more complex.
+      this.zoom = d3.behavior.zoom()
+                    .x(this.xScale)
+                    .y(this.yScale)
+                    .scaleExtent([0.25, 2.0])
+                    .on('zoom', function(evt) {
+                        // This will add the d3 properties to the
+                        // eventFacade
+                        self.fire('zoom', d3.event);
+                     });
 
       // Set up the visualization with a pack layout.
       vis = d3.select(container.getDOMNode())
-      .selectAll('.topology-canvas')
-      .append('svg:svg')
-      .attr('pointer-events', 'all')
-      .attr('width', width)
-      .attr('height', height)
-      .append('svg:g')
-      .append('g');
+              .selectAll('.topology-canvas')
+              .append('svg:svg')
+              .attr('pointer-events', 'all')
+              .attr('width', width)
+              .attr('height', height)
+              .append('svg:g')
+              .call(this.zoom)
+              .append('g');
 
       vis.append('svg:rect')
-      .attr('class', 'graph')
-      .attr('fill', 'rgba(255,255,255,0)');
+         .attr('class', 'graph')
+         .attr('fill', 'rgba(255,255,255,0)');
 
       this.vis = vis;
 
+      // Build out scale and zoom.
+      // These are defaults, a Module
+      // can implement policy around them.
+      this.sizeChangeHandler();
+      this.on('sizeChanged', this.sizeChangeHandler);
+
+      Topology.superclass.renderOnce.apply(this, arguments);
       return this;
+    },
+
+    sizeChangeHandler: function() {
+      var self = this,
+          width = this.get('width'),
+          height = this.get('height');
+
+      // Update the pan/zoom behavior manager.
+      this.xScale.domain([-width / 2, width / 2])
+        .range([0, width]);
+      this.yScale.domain([-height / 2, height / 2])
+        .range([height, 0]);
+      this.zoom.x(this.xScale)
+        .y(this.yScale);
     }
 
   }, {
@@ -82,9 +156,9 @@
       /**
        * @property {Array} transform
        **/
-      transform: {
-        getter: function() {return this.get('zoom').transform();},
-        setter: function(v) {this.get('zoom').transform(v);}
+      translate: {
+        getter: function() {return this.zoom.translate();},
+        setter: function(v) {this.zoom.translate(v);}
       },
 
       width: {

=== modified file 'test/test_d3_components.js'
--- test/test_d3_components.js	2012-12-14 15:25:55 +0000
+++ test/test_d3_components.js	2012-12-19 13:49:21 +0000
@@ -73,7 +73,7 @@
     comp.addModule(TestModule);
 
     // Test that default bindings work by simulating
-    Y.fire('cancel');
+    comp.fire('cancel');
     state.cancelled.should.equal(true);
 
     // XXX: While on the plane I determined that things like
@@ -84,12 +84,12 @@
     state.cancelled = false;
     comp.removeModule('TestModule');
 
-    Y.fire('cancel');
+    comp.fire('cancel');
     state.cancelled.should.equal(false);
 
     // Adding the module back again doesn't create any issues.
     comp.addModule(TestModule);
-    Y.fire('cancel');
+    comp.fire('cancel');
     state.cancelled.should.equal(true);
 
     // Simulated events on DOM handlers better work.
@@ -137,7 +137,9 @@
     modA.windowResizeHandler = function(evt) {
       resized = true;
     };
-    modA.events.yui.windowresize = 'windowResizeHandler';
+    modA.events.yui.windowresize = {
+      callback: 'windowResizeHandler',
+      context: 'window'};
     comp.addModule(modA);
     var subscription = Y.after('windowresize', function(evt) {
       subscription.detach();

=== modified file 'test/test_environment_view.js'
--- test/test_environment_view.js	2012-12-14 15:25:55 +0000
+++ test/test_environment_view.js	2012-12-19 13:49:21 +0000
@@ -119,14 +119,17 @@
     });
 
     it('must handle the window resize event', function(done) {
-      var view = new views.environment({container: container, db: db});
+      var view = new views.environment({container: container, db: db}),
+          topo,
+          beforeResizeEventFired = false;
       view.render();
-      var beforeResizeEventFired = false;
-      Y.once('beforePageSizeRecalculation', function() {
+      topo = view.topo;
+
+      topo.once('beforePageSizeRecalculation', function() {
         // This event must be fired by views.MegaModule.setSizesFromViewport.
         beforeResizeEventFired = true;
       });
-      Y.once('afterPageSizeRecalculation', function() {
+      topo.once('afterPageSizeRecalculation', function() {
         // This event must be fired by views.MegaModule.setSizesFromViewport.
         assert.isTrue(beforeResizeEventFired);
         done();
@@ -257,7 +260,7 @@
       view.postRender();
       var zoom_in = container.one('#zoom-in-btn'),
           zoom_out = container.one('#zoom-out-btn'),
-          module = view.topo.modules.MegaModule,
+          module = view.topo.modules.PanZoomModule,
           slider = module.slider,
           svg = container.one('svg g g');
 

=== modified file 'test/test_topology.js'
--- test/test_topology.js	2012-12-11 15:35:25 +0000
+++ test/test_topology.js	2012-12-19 13:49:21 +0000
@@ -72,7 +72,7 @@
     topo.render();
 
     // Verify that we have built the default scene.
-    Y.Lang.isValue(topo.svg).should.equal(true);
+    Y.Lang.isValue(topo.vis).should.equal(true);
   });
 
   function createStandardTopo() {
@@ -80,6 +80,7 @@
     topo = new views.Topology();
     topo.setAttrs({container: container, db: db});
     topo.addModule(views.MegaModule);
+    topo.addModule(views.PanZoomModule);
     return topo;
   }
 
@@ -88,7 +89,7 @@
        topo = createStandardTopo();
        topo.render();
        // Verify that we have built the default scene.
-       Y.Lang.isValue(topo.svg).should.equal(true);
+       Y.Lang.isValue(topo.vis).should.equal(true);
      });
 
 });

=== modified file 'undocumented'
--- undocumented	2012-12-11 04:11:39 +0000
+++ undocumented	2012-12-19 13:49:21 +0000
@@ -1,5 +1,5 @@
 app/app.js:95 "callback"
-app/app.js:569 "callback"
+app/app.js:552 "callback"
 app/store/env.js:64 "on_close"
 app/store/env.js:69 "on_message"
 app/store/env.js:181 "status"
@@ -71,30 +71,30 @@
 app/views/utils.js:647 "get"
 app/views/utils.js:276 "removeSVGClass"
 app/views/environment.js:24 "initializer"
-app/views/environment.js:29 "render"
-app/views/environment.js:61 "postRender"
-app/views/charm-panel.js:948 "createInstance"
+app/views/environment.js:63 "postRender"
+app/views/environment.js:31 "render"
+app/views/charm-panel.js:1168 "calculatePanelPosition"
 app/views/charm-panel.js:476 "initializer"
 app/views/charm-panel.js:250 "render"
-app/views/charm-panel.js:1140 "calculatePanelPosition"
-app/views/charm-panel.js:723 "showDescription"
+app/views/charm-panel.js:901 "onCharmDeployClicked"
+app/views/charm-panel.js:976 "createInstance"
+app/views/charm-panel.js:764 "hideDescription"
+app/views/charm-panel.js:1022 "setPanel"
 app/views/charm-panel.js:332 "showConfiguration"
-app/views/charm-panel.js:936 "setupOverlay"
+app/views/charm-panel.js:1151 "updatePanelPosition"
+app/views/charm-panel.js:1213 "killInstance"
 app/views/charm-panel.js:655 "render"
-app/views/charm-panel.js:873 "onCharmDeployClicked"
-app/views/charm-panel.js:736 "hideDescription"
+app/views/charm-panel.js:1198 "setDefaultSeries"
 app/views/charm-panel.js:195 "mouseleave"
 app/views/charm-panel.js:417 "_showErrors"
-app/views/charm-panel.js:1179 "getInstance"
+app/views/charm-panel.js:651 "initializer"
 app/views/charm-panel.js:480 "render"
-app/views/charm-panel.js:994 "setPanel"
-app/views/charm-panel.js:651 "initializer"
+app/views/charm-panel.js:751 "showDescription"
+app/views/charm-panel.js:725 "_moveTooltip"
 app/views/charm-panel.js:209 "initializer"
-app/views/charm-panel.js:1123 "updatePanelPosition"
-app/views/charm-panel.js:700 "_moveTooltip"
+app/views/charm-panel.js:964 "setupOverlay"
 app/views/charm-panel.js:192 "mouseenter"
-app/views/charm-panel.js:1170 "setDefaultSeries"
-app/views/charm-panel.js:1185 "killInstance"
+app/views/charm-panel.js:1207 "getInstance"
 app/views/charm.js:32 "render"
 app/views/charm.js:96 "_deployCallback"
 app/views/charm.js:61 "on_charm_data"
@@ -154,60 +154,59 @@
 app/views/service.js:23 "resetUnits"
 app/views/service.js:230 "unexposeService"
 app/views/service.js:237 "_unexposeServiceCallback"
-app/views/topology/mega.js:1441 "subRelBlockMouseLeave"
-app/views/topology/mega.js:892 "show"
-app/views/topology/mega.js:509 "drawRelationGroup"
-app/views/topology/mega.js:1337 "updateServiceMenuLocation"
-app/views/topology/mega.js:1187 "zoom_in"
-app/views/topology/mega.js:1196 "_fire_zoom"
-app/views/topology/mega.js:189 "renderOnce"
-app/views/topology/mega.js:574 "drawRelation"
-app/views/topology/mega.js:1036 "removeRelation"
-app/views/topology/mega.js:1222 "rescale"
-app/views/topology/mega.js:265 "serviceDblClick"
-app/views/topology/mega.js:1469 "toggleControlPanel"
-app/views/topology/mega.js:1093 "cancelRelationBuild"
-app/views/topology/mega.js:1519 "destroyService"
-app/views/topology/mega.js:1073 "removeRelationConfirm"
-app/views/topology/mega.js:904 "fade"
-app/views/topology/mega.js:1254 "hideGraphListPicker"
-app/views/topology/mega.js:248 "serviceClick"
-app/views/topology/mega.js:590 "drawService"
-app/views/topology/mega.js:271 "relationClick"
-app/views/topology/mega.js:854 "renderSlider"
-app/views/topology/mega.js:165 "initializer"
-app/views/topology/mega.js:489 "updateLinks"
-app/views/topology/mega.js:1048 "_removeRelationCallback"
-app/views/topology/mega.js:1652 "addRelationEnd"
-app/views/topology/mega.js:1432 "subRelBlockMouseEnter"
-app/views/topology/mega.js:362 "updateCanvas"
-app/views/topology/mega.js:1409 "serviceMouseLeave"
-app/views/topology/mega.js:1244 "showGraphListPicker"
-app/views/topology/mega.js:322 "updateData"
-app/views/topology/mega.js:176 "render"
-app/views/topology/mega.js:97 "callback"
-app/views/topology/mega.js:976 "addRelationDragStart"
-app/views/topology/mega.js:883 "serviceForBox"
-app/views/topology/mega.js:1496 "destroyServiceConfirm"
-app/views/topology/mega.js:1178 "zoom_out"
-app/views/topology/mega.js:1691 "_addRelationCallback"
-app/views/topology/mega.js:999 "addRelationDrag"
-app/views/topology/mega.js:1558 "addRelationStart"
-app/views/topology/mega.js:965 "addRelation"
-app/views/topology/mega.js:823 "processRelations"
-app/views/topology/mega.js:1291 "setSizesFromViewport"
-app/views/topology/mega.js:1370 "serviceMouseEnter"
-app/views/topology/mega.js:812 "processRelation"
-app/views/topology/mega.js:1488 "show_service"
-app/views/topology/mega.js:922 "postRender"
-app/views/topology/mega.js:898 "hide"
-app/views/topology/mega.js:1017 "addRelationDragEnd"
-app/views/topology/mega.js:1569 "ambiguousAddRelationCheck"
-app/views/topology/mega.js:1528 "_destroyCallback"
-app/views/topology/mega.js:848 "subordinateRelationsForService"
-app/views/topology/panzoom.js:14 "initializer"
-app/views/topology/panzoom.js:18 "render"
-app/views/topology/panzoom.js:23 "update"
+app/views/topology/mega.js:810 "fade"
+app/views/topology/mega.js:173 "serviceClick"
+app/views/topology/mega.js:935 "_removeRelationCallback"
+app/views/topology/mega.js:1067 "showGraphListPicker"
+app/views/topology/mega.js:1523 "_addRelationCallback"
+app/views/topology/mega.js:419 "updateLinks"
+app/views/topology/mega.js:861 "addRelationDragStart"
+app/views/topology/mega.js:850 "addRelation"
+app/views/topology/mega.js:828 "renderedHandler"
+app/views/topology/mega.js:1077 "hideGraphListPicker"
+app/views/topology/mega.js:248 "updateData"
+app/views/topology/mega.js:789 "serviceForBox"
+app/views/topology/mega.js:1325 "destroyServiceConfirm"
+app/views/topology/mega.js:1260 "subRelBlockMouseEnter"
+app/views/topology/mega.js:505 "drawRelation"
+app/views/topology/mega.js:1357 "_destroyCallback"
+app/views/topology/mega.js:754 "processRelations"
+app/views/topology/mega.js:1297 "toggleControlPanel"
+app/views/topology/mega.js:1482 "addRelationEnd"
+app/views/topology/mega.js:743 "processRelation"
+app/views/topology/mega.js:1316 "show_service"
+app/views/topology/mega.js:191 "serviceDblClick"
+app/views/topology/mega.js:798 "show"
+app/views/topology/mega.js:439 "drawRelationGroup"
+app/views/topology/mega.js:904 "addRelationDragEnd"
+app/views/topology/mega.js:886 "addRelationDrag"
+app/views/topology/mega.js:289 "update"
+app/views/topology/mega.js:1269 "subRelBlockMouseLeave"
+app/views/topology/mega.js:1196 "serviceMouseEnter"
+app/views/topology/mega.js:960 "removeRelationConfirm"
+app/views/topology/mega.js:1387 "addRelationStart"
+app/views/topology/mega.js:1398 "ambiguousAddRelationCheck"
+app/views/topology/mega.js:923 "removeRelation"
+app/views/topology/mega.js:980 "cancelRelationBuild"
+app/views/topology/mega.js:92 "callback"
+app/views/topology/mega.js:521 "drawService"
+app/views/topology/mega.js:804 "hide"
+app/views/topology/mega.js:163 "initializer"
+app/views/topology/mega.js:197 "relationClick"
+app/views/topology/mega.js:1348 "destroyService"
+app/views/topology/mega.js:1161 "updateServiceMenuLocation"
+app/views/topology/mega.js:1114 "setSizesFromViewport"
+app/views/topology/mega.js:1236 "serviceMouseLeave"
+app/views/topology/mega.js:779 "subordinateRelationsForService"
+app/views/topology/panzoom.js:83 "update"
+app/views/topology/panzoom.js:40 "zoomHandler"
+app/views/topology/panzoom.js:100 "zoom_in"
+app/views/topology/panzoom.js:136 "rescale"
+app/views/topology/panzoom.js:109 "_fire_zoom"
+app/views/topology/panzoom.js:33 "initializer"
+app/views/topology/panzoom.js:157 "renderedHandler"
+app/views/topology/panzoom.js:91 "zoom_out"
+app/views/topology/panzoom.js:48 "renderSlider"
 app/views/topology/relation.js:18 "render"
 app/views/topology/relation.js:23 "update"
 app/views/topology/relation.js:14 "initializer"
@@ -216,14 +215,15 @@
 app/views/topology/service.js:26 "initializer"
 app/views/topology/service.js:50 "update"
 app/views/topology/service.js:39 "_scaleLayout"
-app/views/topology/topology.js:26 "renderOnce"
-app/views/topology/topology.js:87 "setter"
-app/views/topology/topology.js:95 "getter"
-app/views/topology/topology.js:91 "getter"
-app/views/topology/topology.js:80 "setter"
-app/views/topology/topology.js:86 "getter"
-app/views/topology/topology.js:21 "initializer"
-app/views/topology/topology.js:79 "getter"
+app/views/topology/topology.js:161 "setter"
+app/views/topology/topology.js:26 "initializer"
+app/views/topology/topology.js:154 "setter"
+app/views/topology/topology.js:165 "getter"
+app/views/topology/topology.js:120 "sizeChangeHandler"
+app/views/topology/topology.js:160 "getter"
+app/views/topology/topology.js:153 "getter"
+app/views/topology/topology.js:169 "getter"
+app/views/topology/topology.js:58 "renderOnce"
 app/views/topology/viewport.js:33 "initializer"
 app/views/topology/viewport.js:66 "update"
 app/views/topology/viewport.js:37 "render"


Follow ups