yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #02050
[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"> </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