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

Proposed by Benjamin Saller
Status: Merged
Merged at revision: 292
Proposed branch: lp:~bcsaller/juju-gui/topology-panzoom
Merge into: lp:juju-gui/experimental
Diff against target: 1720 lines (+618/-503)
11 files modified
app/app.js (+16/-31)
app/assets/javascripts/d3-components.js (+116/-56)
app/templates/overview.handlebars (+1/-1)
app/views/environment.js (+24/-16)
app/views/topology/mega.js (+120/-287)
app/views/topology/panzoom.js (+155/-6)
app/views/topology/topology.js (+89/-15)
test/test_d3_components.js (+6/-4)
test/test_environment_view.js (+10/-7)
test/test_topology.js (+3/-2)
undocumented (+78/-78)
To merge this branch: bzr merge lp:~bcsaller/juju-gui/topology-panzoom
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+140671@code.launchpad.net

Description of the change

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/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Reviewers: mp+140671_code.launchpad.net,

Message:
Please take a look.

Description:
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://code.launchpad.net/~bcsaller/juju-gui/topology-panzoom/+merge/140671

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/6971045/

Affected files:
   [revision details]
   app/app.js
   app/assets/javascripts/d3-components.js
   app/templates/overview.handlebars
   app/views/environment.js
   app/views/topology/mega.js
   app/views/topology/panzoom.js
   app/views/topology/topology.js
   test/test_d3_components.js
   test/test_environment_view.js
   test/test_topology.js
   undocumented

299. By Benjamin Saller

wip

300. By Benjamin Saller

merge trunk

301. By Benjamin Saller

restore bind origin change

Revision history for this message
Benjamin Saller (bcsaller) wrote :
302. By Benjamin Saller

remove only

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Revision history for this message
Gary Poster (gary) wrote :

Thanks for helping me understand this. As I expected, I'm +1 on the
code. I would like a bug/card to fix the prod tests on start. I'd like
to see if we can fix the viewport resize issue one way or another before
we land.

Thank you

Gary

https://codereview.appspot.com/6971045/diff/17/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/6971045/diff/17/app/assets/javascripts/d3-components.js#newcode328
app/assets/javascripts/d3-components.js:328: bindAllD3Events: function()
{
Comment describing behavior and saying that the expected client of this
is the component might be nice.

https://codereview.appspot.com/6971045/diff/17/app/views/topology/panzoom.js
File app/views/topology/panzoom.js (right):

https://codereview.appspot.com/6971045/diff/17/app/views/topology/panzoom.js#newcode107
app/views/topology/panzoom.js:107: * Wraper around the actual rescale
method for zoom buttons.
Wrapper

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

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

quick items

https://codereview.appspot.com/6971045/diff/7002/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/6971045/diff/7002/app/assets/javascripts/d3-components.js#newcode255
app/assets/javascripts/d3-components.js:255: console.log('Bind', target,
eventPhase, name);
s/console.log // console.debug ..
s/'Bind'/'d3 components bind'

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

303. By Benjamin Saller

review changes, fix from makyo for window events

Revision history for this message
Benjamin Saller (bcsaller) wrote :

*** Submitted:

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.

R=gary.poster, hazmat
CC=
https://codereview.appspot.com/6971045

https://codereview.appspot.com/6971045/diff/17/app/views/environment.js
File app/views/environment.js (right):

https://codereview.appspot.com/6971045/diff/17/app/views/environment.js#newcode27
app/views/environment.js:27: broadcast: true,
Test again w/o broadcast.

https://codereview.appspot.com/6971045/diff/17/app/views/topology/mega.js
File app/views/topology/mega.js (right):

https://codereview.appspot.com/6971045/diff/17/app/views/topology/mega.js#newcode872
app/views/topology/mega.js:872: self.cursorBox = new
views.BoundingBox();
need new here?

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

Revision history for this message
Brad Crittenden (bac) wrote :

I make several suggestions but they are all straight-forward so no
re-look is necessary before landing.

Recently I *think* we agreed to the 'one var per variable' idea which
you didn't follow for the newly added code. Perhaps it was "When in
Rome" or you just forgot. I'm just mentioning as a point of discussion
not a suggestion to change as it would delay this important work from
landing.

https://codereview.appspot.com/6971045/diff/17/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/6971045/diff/17/app/assets/javascripts/d3-components.js#newcode155
app/assets/javascripts/d3-components.js:155:
These two cases are mutually exclusive, right, so 'else if' would be
clearer.

https://codereview.appspot.com/6971045/diff/17/app/assets/javascripts/d3-components.js#newcode248
app/assets/javascripts/d3-components.js:248: if
(Y.Array.indexOf(['windowresize'], name) !== -1) {
I don't understand why the indexOf is used instead of just a comparison.

https://codereview.appspot.com/6971045/diff/17/app/assets/javascripts/d3-components.js#newcode315
app/assets/javascripts/d3-components.js:315: // Create an adaptor
s/adaptor/adapter

https://codereview.appspot.com/6971045/diff/17/app/views/environment.js
File app/views/environment.js (right):

https://codereview.appspot.com/6971045/diff/17/app/views/environment.js#newcode66
app/views/environment.js:66: // Bind d3 events (manually)
This line is a complete thought and needs a period to be more readable.

https://codereview.appspot.com/6971045/diff/17/app/views/environment.js#newcode69
app/views/environment.js:69: // the existing (from change to showView)
I'm unclear what you're saying here.

https://codereview.appspot.com/6971045/diff/17/app/views/topology/panzoom.js
File app/views/topology/panzoom.js (right):

https://codereview.appspot.com/6971045/diff/17/app/views/topology/panzoom.js#newcode9
app/views/topology/panzoom.js:9: * Handle PanZoom within the a Topology.
typo: the a

Pick one? :)

https://codereview.appspot.com/6971045/diff/17/app/views/topology/panzoom.js#newcode51
app/views/topology/panzoom.js:51: contianer = topo.get('container'),
typo: container

Is that variable even used?

https://codereview.appspot.com/6971045/diff/17/app/views/topology/panzoom.js#newcode127
app/views/topology/panzoom.js:127: evt.translate[1] -=
parseInt(rect.attr('height'), 10) / 2 * delta;
I wish we'd made a wrapper early on for parseInt that defaulted to base
10. So annoying to see everywhere.

https://codereview.appspot.com/6971045/diff/17/app/views/topology/topology.js
File app/views/topology/topology.js (right):

https://codereview.appspot.com/6971045/diff/17/app/views/topology/topology.js#newcode17
app/views/topology/topology.js:17: * Emmitted Events:
Emitted

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/app.js'
2--- app/app.js 2012-12-14 20:25:16 +0000
3+++ app/app.js 2012-12-20 16:25:28 +0000
4@@ -537,37 +537,22 @@
5 * @method show_environment
6 */
7 show_environment: function(req, res, next) {
8- var view = this.getViewInfo('environment'),
9- instance = view.instance,
10- self = this;
11- if (!instance) {
12- console.log('new env view');
13- this.showView('environment',
14- { getModelURL: Y.bind(this.getModelURL, this),
15- /** A simple closure so changes to the value are available.*/
16- getServiceEndpoints: function() {return self.serviceEndpoints;},
17- loadService: this.loadService,
18- db: this.db,
19- env: this.env},
20- {render: true});
21- } else {
22- /* The current impl makes extensive use of
23- * event handlers which are not being properly rebound
24- * when the view is attached. There is a workable pattern
25- * to enable this but we have to land the basics of this branch
26- * first.
27- */
28- this.showView('environment',
29- { getModelURL: Y.bind(this.getModelURL, this),
30- /** A simple closure so changes to the value are available.*/
31- getServiceEndpoints: function() {return self.serviceEndpoints;},
32- loadService: this.loadService,
33- db: this.db,
34- env: this.env},
35- { update: false,
36- render: true,
37- callback: function(view) {view.postRender();}});
38- }
39+ var self = this,
40+ view = this.getViewInfo('environment'),
41+ options = {
42+ getModelURL: Y.bind(this.getModelURL, this),
43+ /** A simple closure so changes to the value are available.*/
44+ getServiceEndpoints: function() {
45+ return self.serviceEndpoints;},
46+ loadService: this.loadService,
47+ db: this.db,
48+ env: this.env};
49+
50+ this.showView('environment', options, {
51+ callback: function() {
52+ this.views.environment.instance.postRender();
53+ },
54+ render: true});
55 },
56
57 /**
58
59=== modified file 'app/assets/javascripts/d3-components.js'
60--- app/assets/javascripts/d3-components.js 2012-12-14 15:25:55 +0000
61+++ app/assets/javascripts/d3-components.js 2012-12-20 16:25:28 +0000
62@@ -34,6 +34,7 @@
63 initializer: function() {
64 this.events = Y.mix(this.events, this._defaultEvents,
65 false, undefined, 0, true);
66+
67 },
68
69 componentBound: function() {},
70@@ -116,6 +117,9 @@
71 this.events[module.name] = modEvents;
72 this.bind(module.name);
73 module.componentBound();
74+
75+ // Add Module as an event target of Component
76+ this.addTarget(module);
77 return this;
78 },
79
80@@ -129,10 +133,51 @@
81 removeModule: function(moduleName) {
82 this.unbind(moduleName);
83 delete this.events[moduleName];
84+ this.removeTarget(this.modules[moduleName]);
85 delete this.modules[moduleName];
86 return this;
87 },
88
89+ // Return a resolved handler object in the form
90+ // {phase: str, callback: function}
91+ _normalizeHandler: function(handler, module, selector) {
92+ var result = {};
93+
94+ if (L.isString(handler)) {
95+ result.callback = module[handler];
96+ result.phase = 'on';
97+ }
98+
99+ if (L.isObject(handler)) {
100+ result.phase = handler.phase || 'on';
101+ result.callback = handler.callback;
102+ }
103+
104+ if (L.isString(result.callback)) {
105+ result.callback = module[result.callback];
106+ }
107+
108+ if (!result.callback) {
109+ console.error('No Event handler for', selector, module.name);
110+ return;
111+ }
112+ if (!L.isFunction(result.callback)) {
113+ console.error('Unable to resolve a proper callback for',
114+ selector, handler, module.name, result);
115+ return;
116+ }
117+ // Set up binding context for callback.
118+ result.context = module;
119+ if (handler.context) {
120+ if (handler.context === 'component') {
121+ result.context = this;
122+ } else if (handler.context === 'window') {
123+ result.context = Y.one('window');
124+ }
125+ }
126+ return result;
127+ },
128+
129 /**
130 * Internal implementation of binding both Module.events.scene and
131 * Module.events.yui.
132@@ -163,49 +208,12 @@
133 Y.delegate(name, d3Adaptor, container, selector, context));
134 }
135
136- // Return a resolved handler object in the form
137- // {phase: str, callback: function}
138- function _normalizeHandler(handler, module, selector) {
139- var result = {};
140-
141- if (L.isString(handler)) {
142- result.callback = module[handler];
143- result.phase = 'on';
144- }
145-
146- if (L.isObject(handler)) {
147- result.phase = handler.phase || 'on';
148- result.callback = handler.callback;
149- }
150-
151- if (L.isString(result.callback)) {
152- result.callback = module[result.callback];
153- }
154-
155- if (!result.callback) {
156- console.error('No Event handler for', selector, modName);
157- return;
158- }
159- if (!L.isFunction(result.callback)) {
160- console.error('Unable to resolve a proper callback for',
161- selector, handler, modName, result);
162- return;
163- }
164- // Set up binding context for callback.
165- result.context = module;
166- if (handler.context &&
167- handler.context === 'component') {
168- result.context = self;
169- }
170- return result;
171- }
172-
173 this.unbind(modName);
174
175 // Bind 'scene' events
176 Y.each(modEvents.scene, function(handlers, selector, sceneEvents) {
177 Y.each(handlers, function(handler, trigger) {
178- handler = _normalizeHandler(handler, module, selector);
179+ handler = self._normalizeHandler(handler, module, selector);
180 if (L.isValue(handler)) {
181 _bindEvent(trigger, handler.callback,
182 container, selector, handler.context);
183@@ -222,7 +230,7 @@
184 Y.each(['after', 'before', 'on'], function(eventPhase) {
185 var resolvedHandler = {};
186 Y.each(modEvents.yui, function(handler, name) {
187- handler = _normalizeHandler(handler, module, name);
188+ handler = self._normalizeHandler(handler, module, name);
189 if (!handler || handler.phase !== eventPhase) {
190 return;
191 }
192@@ -235,8 +243,20 @@
193 // this signature: Y.on(event, callback, target, context).
194 // For this reason, it is not possible here to just pass the
195 // context as third argument.
196- var callback = Y.bind(handler.callback, handler.context);
197- subscriptions.push(Y[eventPhase](name, callback));
198+ var target = self,
199+ callback = Y.bind(handler.callback, handler.context);
200+ if (Y.Array.indexOf(['windowresize'], name) !== -1) {
201+ target = Y;
202+ handler.context = null;
203+ } else {
204+ // (re)Register the event to bubble.
205+ self.publish(name, {emitFacade: true});
206+ }
207+ console.debug('d3 component yui event binding', target.toString(),
208+ eventPhase, name);
209+ subscriptions.push(
210+ target[eventPhase](
211+ name, callback, handler.context));
212 });
213 }
214 });
215@@ -278,31 +298,63 @@
216 _bindD3Events: function(modName) {
217 // Walk each selector for a given module 'name', doing a
218 // d3 selection and an 'on' binding.
219- var modEvents = this.events[modName],
220+ var self = this,
221+ modEvents = this.events[modName],
222 owns = Y.Object.owns,
223 module;
224+
225 if (!modEvents || !modEvents.d3) {
226 return;
227 }
228 modEvents = modEvents.d3;
229 module = this.modules[modName];
230
231- function _normalizeHandler(handler, module) {
232- if (handler && !L.isFunction(handler)) {
233- handler = module[handler];
234- }
235- return handler;
236- }
237-
238 Y.each(modEvents, function(handlers, selector) {
239 Y.each(handlers, function(handler, trigger) {
240- handler = _normalizeHandler(handler, module);
241- d3.selectAll(selector).on(trigger, handler);
242+ var adapter;
243+ handler = self._normalizeHandler(handler, module);
244+ // Create an adaptor
245+ adapter = function() {
246+ var selection = d3.select(this),
247+ d = selection.data()[0];
248+ return handler.callback.call(this, d, handler.context);
249+ };
250+ d3.selectAll(selector).on(trigger, adapter);
251 });
252 });
253 },
254
255 /**
256+ * Allow d3 event rebinding after rendering.
257+ *
258+ **/
259+ bindAllD3Events: function() {
260+ var self = this;
261+ Y.each(this.modules, function(mod, name) {
262+ self._bindD3Events(name);
263+ });
264+ },
265+
266+ /**
267+ * Register a manual event subscription on
268+ * behalf of a module.
269+ *
270+ * @method recordSubscription
271+ * @param {Module} module to record relative to.
272+ * @param {Object} YUI event subscription.
273+ * @chainable
274+ **/
275+ recordSubscription: function(module, subscription) {
276+ if (!(module.name in this.events)) {
277+ throw 'Unable able to recordSubscription, module not added.';
278+ }
279+ if (!subscription) {
280+ throw 'Invalid/undefined subscription object cannot be recorded.';
281+ }
282+ this.events[module.name].subscriptions.push(subscription);
283+ },
284+
285+ /**
286 Internal Detail. Called by unbind automatically.
287 * D3 events follow a 'slot' like system. Setting the
288 * event to null unbinds existing handlers.
289@@ -360,8 +412,9 @@
290 *
291 * Called the first time render is invoked. See {render}.
292 **/
293- renderOnce: function() {},
294-
295+ renderOnce: function() {
296+ this.all('renderOnce');
297+ },
298 /**
299 * Render each module bound to the canvas. The first call to
300 * render() will automatically call renderOnce (a noop by default)
301@@ -432,10 +485,17 @@
302 * @chainable
303 */
304 update: function() {
305- Y.each(Y.Object.values(this.modules), function(mod) {
306- mod.update();
307+ this.all('update');
308+ return this;
309+ },
310+
311+ all: function(methodName) {
312+ Y.each(this.modules, function(mod, name) {
313+ if (methodName in mod) {
314+ console.log('Component', methodName, 'on', name);
315+ mod[methodName]();
316+ }
317 });
318- return this;
319 }
320 }, {
321 ATTRS: {
322
323=== modified file 'app/templates/overview.handlebars'
324--- app/templates/overview.handlebars 2012-12-11 04:11:39 +0000
325+++ app/templates/overview.handlebars 2012-12-20 16:25:28 +0000
326@@ -1,5 +1,5 @@
327 <div class="topology">
328- <div class="crosshatch-background">
329+ <div class="topology-canvas crosshatch-background">
330 <div class="environment-menu" id="service-menu">
331 <div class="triangle">&nbsp;</div>
332 <ul>
333
334=== modified file 'app/views/environment.js'
335--- app/views/environment.js 2012-12-11 03:58:03 +0000
336+++ app/views/environment.js 2012-12-20 16:25:28 +0000
337@@ -23,7 +23,9 @@
338 {
339 initializer: function() {
340 console.log('View: Initialized: Env');
341- this.publish('navigateTo', {preventable: false});
342+ this.publish('navigateTo', {
343+ broadcast: true,
344+ preventable: false});
345 },
346
347 render: function() {
348@@ -48,18 +50,24 @@
349 container: container});
350 // Bind all the behaviors we need as modules.
351 topo.addModule(views.MegaModule);
352+ topo.addModule(views.PanZoomModule);
353
354 topo.addTarget(this);
355 this.topo = topo;
356 }
357+
358 topo.render();
359 return this;
360 },
361- // XXX: This method is a pass through,
362- // it will be removed when we move to
363- // incremental rendering.
364+
365 postRender: function() {
366- this.topo.modules.MegaModule.postRender();
367+ this.topo.attachContainer();
368+ this.topo.fire('rendered');
369+ // Bind d3 events (manually)
370+ // this needs to be postRender and
371+ // the jiggle in phases has broken
372+ // the existing (from change to showView)
373+ this.topo.bindAllD3Events();
374 }
375 }, {
376 ATTRS: {}
377@@ -68,15 +76,15 @@
378 views.environment = EnvironmentView;
379 }, '0.1.0', {
380 requires: ['juju-templates',
381- 'juju-view-utils',
382- 'juju-models',
383- 'd3',
384- 'd3-components',
385- 'base-build',
386- 'handlebars-base',
387- 'node',
388- 'svg-layouts',
389- 'event-resize',
390- 'slider',
391- 'view']
392+ 'juju-view-utils',
393+ 'juju-models',
394+ 'd3',
395+ 'd3-components',
396+ 'base-build',
397+ 'handlebars-base',
398+ 'node',
399+ 'svg-layouts',
400+ 'event-resize',
401+ 'slider',
402+ 'view']
403 });
404
405=== modified file 'app/views/topology/mega.js'
406--- app/views/topology/mega.js 2012-12-11 03:58:03 +0000
407+++ app/views/topology/mega.js 2012-12-20 16:25:28 +0000
408@@ -53,12 +53,10 @@
409 .attr('class', 'unit-count hide-count');
410 }}
411 },
412-
413 '.rel-label': {
414 click: 'relationClick',
415 mousemove: 'mousemove'
416 },
417-
418 '.topology .crosshatch-background rect:first-child': {
419 /**
420 * If the user clicks on the background we cancel any active add
421@@ -81,9 +79,6 @@
422 self.backgroundClicked();
423 }}
424 },
425-
426- '#zoom-out-btn': {click: 'zoom_out'},
427- '#zoom-in-btn': {click: 'zoom_in'},
428 '.graph-list-picker .picker-button': {
429 click: 'showGraphListPicker'
430 },
431@@ -132,9 +127,9 @@
432 },
433 d3: {
434 '.service': {
435- 'mousedown.addrel': {callback: function(d, self) {
436+ 'mousedown.addrel': {callback: function(d, context) {
437 var evt = d3.event;
438- self.longClickTimer = Y.later(750, this, function(d, e) {
439+ context.longClickTimer = Y.later(750, this, function(d, e) {
440 // Provide some leeway for accidental dragging.
441 if ((Math.abs(d.x - d.oldX) + Math.abs(d.y - d.oldY)) /
442 2 > 5) {
443@@ -146,25 +141,27 @@
444 d3.event = e;
445
446 // Start the process of adding a relation
447- self.addRelationDragStart(d, self);
448+ context.addRelationDragStart(d, context);
449 }, [d, evt], false);
450 }},
451- 'mouseup.addrel': {callback: function(d, self) {
452+ 'mouseup.addrel': {callback: function(d, context) {
453 // Cancel the long-click timer if it exists.
454- if (self.longClickTimer) {
455- self.longClickTimer.cancel();
456+ if (context.longClickTimer) {
457+ context.longClickTimer.cancel();
458 }
459 }}
460 }
461 },
462 yui: {
463- windowresize: 'setSizesFromViewport'
464+ windowresize: {
465+ callback: 'setSizesFromViewport',
466+ context: 'module'},
467+ rendered: 'renderedHandler'
468 }
469 },
470
471 initializer: function(options) {
472 MegaModule.superclass.constructor.apply(this, arguments);
473- this.publish('navigateTo', {preventable: false});
474
475 // Build a service.id -> BoundingBox map for services.
476 this.service_boxes = {};
477@@ -173,83 +170,12 @@
478 this.set('currentServiceClickAction', 'toggleControlPanel');
479 },
480
481- render: function() {
482- MegaModule.superclass.render.apply(this, arguments);
483- var container = this.get('container');
484- container.setHTML(Templates.overview());
485- this.svg = container.one('.topology');
486-
487- this.renderOnce();
488-
489- return this;
490- },
491- /*
492- * Construct a persistent scene that is managed in update.
493- */
494- renderOnce: function() {
495- var self = this,
496- container = this.get('container'),
497- height = 600,
498- width = 640,
499- fill = d3.scale.category20();
500-
501- this.service_scale = d3.scale.log().range([150, 200]);
502- this.service_scale_width = d3.scale.log().range([164, 200]),
503- this.service_scale_height = d3.scale.log().range([64, 100]);
504- this.xscale = d3.scale.linear()
505- .domain([-width / 2, width / 2])
506- .range([0, width]),
507- this.yscale = d3.scale.linear()
508- .domain([-height / 2, height / 2])
509- .range([height, 0]);
510-
511- // Create a pan/zoom behavior manager.
512- var zoom = d3.behavior.zoom()
513- .x(this.xscale)
514- .y(this.yscale)
515- .scaleExtent([0.25, 2.0])
516- .on('zoom', function() {
517- // Keep the slider up to date with the scale on other sorts
518- // of zoom interactions
519- var s = self.slider;
520- s.set('value', Math.floor(d3.event.scale * 100));
521- self.rescale(vis, d3.event);
522- });
523- self.zoom = zoom;
524-
525- // Set up the visualization with a pack layout.
526- var vis = d3.select(container.getDOMNode())
527- .select('.crosshatch-background')
528- .append('svg:svg')
529- .attr('pointer-events', 'all')
530- .attr('width', width)
531- .attr('height', height)
532- .append('svg:g')
533- .call(zoom)
534- // Disable zoom on double click.
535- .on('dblclick.zoom', null)
536- .append('g');
537-
538- vis.append('svg:rect')
539- .attr('class', 'graph')
540- .attr('fill', 'rgba(255,255,255,0)');
541-
542- this.vis = vis;
543- this.tree = d3.layout.pack()
544- .size([width, height])
545- .value(function(d) {
546- return Math.max(d.unit_count, 1);
547- })
548- .padding(300);
549-
550- this.updateCanvas();
551- },
552-
553 serviceClick: function(d, context) {
554 // Ignore if we clicked outside the actual service node.
555- var container = context.get('container'),
556- mouse_coords = d3.mouse(container.one('svg').getDOMNode());
557- if (!d.containsPoint(mouse_coords, context.zoom)) {
558+ var topo = context.get('component'),
559+ container = context.get('container'),
560+ mouse_coords = d3.mouse(container.one('svg').getDOMNode());
561+ if (!d.containsPoint(mouse_coords, topo.zoom)) {
562 return;
563 }
564 // Get the current click action
565@@ -321,8 +247,9 @@
566 */
567 updateData: function() {
568 //model data
569- var vis = this.vis,
570- db = this.get('component').get('db'),
571+ var topo = this.get('component'),
572+ vis = topo.vis,
573+ db = topo.get('db'),
574 relations = db.relations.toArray(),
575 services = db.services.map(views.toBoundingBox);
576
577@@ -352,17 +279,33 @@
578 // Nodes are mapped by modelId tuples.
579 this.node = vis.selectAll('.service')
580 .data(services, function(d) {
581- return d.modelId();});
582+ return d.modelId();});
583 },
584
585 /*
586 * Attempt to reuse as much of the existing graph and view models
587 * as possible to re-render the graph.
588 */
589- updateCanvas: function() {
590+ update: function() {
591 var self = this,
592- tree = this.tree,
593- vis = this.vis;
594+ topo = this.get('component'),
595+ width = topo.get('width'),
596+ height = topo.get('height');
597+
598+ if (!this.service_scale) {
599+ this.service_scale = d3.scale.log().range([150, 200]);
600+ this.service_scale_width = d3.scale.log().range([164, 200]),
601+ this.service_scale_height = d3.scale.log().range([64, 100]);
602+ }
603+
604+ if (!this.tree) {
605+ this.tree = d3.layout.pack()
606+ .size([width, height])
607+ .value(function(d) {
608+ return Math.max(d.unit_count, 1);
609+ })
610+ .padding(300);
611+ }
612
613 //Process any changed data.
614 this.updateData();
615@@ -396,7 +339,6 @@
616 // Clear any state while dragging.
617 self.get('container').all('.environment-menu.active')
618 .removeClass('active');
619- self.service_click_actions.toggleControlPanel(null, self);
620 self.cancelRelationBuild();
621
622 // Update relation lines for just this service.
623@@ -432,7 +374,6 @@
624 .attr('y2', t[1]);
625 rel_group.select('.rel-label')
626 .attr('transform', function(d) {
627- // XXX: This has to happen on update, not enter
628 return 'translate(' +
629 [Math.max(s[0], t[0]) -
630 Math.abs((s[0] - t[0]) / 2),
631@@ -459,32 +400,21 @@
632
633 // enter
634 node
635- .enter().append('g')
636- .attr('class', function(d) {
637+ .enter().append('g')
638+ .attr('class', function(d) {
639 return (d.subordinate ? 'subordinate ' : '') + 'service';
640 })
641- .call(drag)
642- .on('mousedown.addrel', function(d) {
643- self.d3Events['.service']['mousedown.addrel']
644- .call(this, d, self, d3.event);
645- })
646- .on('mouseup.addrel', function(d) {
647- self.d3Events['.service']['mouseup.addrel']
648- .call(this, d, self, d3.event);
649- })
650- .attr('transform', function(d) {
651- return d.translateStr();});
652+ .call(drag)
653+ .attr('transform', function(d) {
654+ return d.translateStr();
655+ });
656
657 // Update
658 this.drawService(node);
659
660 // Exit
661 node.exit()
662- .call(function(d) {
663- // TODO: update the service_boxes
664- // removing the bound data
665- })
666- .remove();
667+ .remove();
668
669 function updateLinks() {
670 // Enter.
671@@ -509,10 +439,11 @@
672 drawRelationGroup: function() {
673 // Add a labelgroup.
674 var self = this,
675- g = self.vis.selectAll('g.rel-group')
676- .data(self.rel_pairs, function(r) {
677- return r.modelIds();
678- });
679+ vis = this.get('component').vis,
680+ g = vis.selectAll('g.rel-group')
681+ .data(self.rel_pairs, function(r) {
682+ return r.modelIds();
683+ });
684
685 var enter = g.enter();
686
687@@ -534,7 +465,7 @@
688 'relation';
689 });
690
691- g.selectAll('rel-label').remove();
692+ g.selectAll('.rel-label').remove();
693 g.selectAll('text').remove();
694 g.selectAll('rect').remove();
695 var label = g.append('g')
696@@ -851,31 +782,6 @@
697 p.scope === 'container';
698 });
699 },
700- renderSlider: function() {
701- var self = this,
702- value = 100,
703- currentScale = this.get('scale');
704- // Build a slider to control zoom level
705- if (currentScale) {
706- value = currentScale * 100;
707- }
708- var slider = new Y.Slider({
709- min: 25,
710- max: 200,
711- value: value
712- });
713- slider.render('#slider-parent');
714- slider.after('valueChange', function(evt) {
715- // Don't fire a zoom if there's a zoom event already in progress;
716- // that will run rescale for us.
717- if (d3.event && d3.event.scale && d3.event.translate) {
718- return;
719- }
720- self._fire_zoom((evt.newVal - evt.prevVal) / 100);
721- });
722- self.slider = slider;
723- },
724-
725 /*
726 * Utility method to get a service object from the DB
727 * given a BoundingBox.
728@@ -919,9 +825,11 @@
729 * in app.showView(), and in testing, it needs to be called manually,
730 * if the test relies on any of this data.
731 */
732- postRender: function() {
733+ renderedHandler: function() {
734 var container = this.get('container');
735
736+ this.update();
737+
738 // Set the sizes from the viewport.
739 this.setSizesFromViewport();
740
741@@ -932,29 +840,6 @@
742 .setAttribute('x', -width / 2);
743 });
744
745- // Preserve zoom when the scene is updated.
746- var changed = false,
747- currentScale = this.get('scale'),
748- currentTranslate = this.get('translate');
749- if (currentTranslate && currentTranslate !== this.zoom.translate()) {
750- this.zoom.translate(currentTranslate);
751- changed = true;
752- }
753- if (currentScale && currentScale !== this.zoom.scale()) {
754- this.zoom.scale(currentScale);
755- changed = true;
756- }
757- if (changed) {
758- this._fire_zoom(0);
759- }
760-
761- // Render the slider after the view is attached.
762- // Although there is a .syncUI() method on sliders, it does not
763- // seem to play well with the app framework: the slider will render
764- // the first time, but on navigation away and back, will not
765- // re-render within the view.
766- this.renderSlider();
767-
768 // Chainable method.
769 return this;
770 },
771@@ -975,14 +860,16 @@
772
773 addRelationDragStart: function(d, context) {
774 // Create a pending drag-line.
775- var dragline = this.vis.append('line')
776- .attr('class', 'relation pending-relation dragline dragging'),
777- self = this;
778+ var vis = this.get('component').vis,
779+ dragline = vis.append('line')
780+ .attr('class',
781+ 'relation pending-relation dragline dragging'),
782+ self = this;
783
784 // Start the line between the cursor and the nearest connector
785 // point on the service.
786- var mouse = d3.mouse(Y.one('svg').getDOMNode());
787- self.cursorBox = views.BoundingBox();
788+ var mouse = d3.mouse(Y.one('.topology svg').getDOMNode());
789+ self.cursorBox = new views.BoundingBox();
790 self.cursorBox.pos = {x: mouse[0], y: mouse[1], w: 0, h: 0};
791 var point = self.cursorBox.getConnectorPair(d);
792 dragline.attr('x1', point[0][0])
793@@ -1091,6 +978,7 @@
794 },
795
796 cancelRelationBuild: function() {
797+ var vis = this.get('component').vis;
798 if (this.dragline) {
799 // Get rid of our drag line
800 this.dragline.remove();
801@@ -1099,7 +987,7 @@
802 this.clickAddRelation = null;
803 this.set('currentServiceClickAction', 'toggleControlPanel');
804 this.buildingRelation = false;
805- this.show(this.vis.selectAll('.service'))
806+ this.show(vis.selectAll('.service'))
807 .classed('selectable-service', false);
808 },
809
810@@ -1128,10 +1016,12 @@
811 */
812 startRelation: function(service) {
813 // Set flags on the view that indicate we are building a relation.
814+ var vis = this.get('component').vis;
815+
816 this.buildingRelation = true;
817 this.clickAddRelation = true;
818
819- this.show(this.vis.selectAll('.service'));
820+ this.show(vis.selectAll('.service'));
821
822 var db = this.get('component').get('db'),
823 getServiceEndpoints = this.get('component')
824@@ -1157,7 +1047,7 @@
825 // Rather than two loops this marks
826 // all services as selectable and then
827 // removes the invalid ones.
828- this.fade(this.vis.selectAll('.service')
829+ this.fade(vis.selectAll('.service')
830 .classed('selectable-service', true)
831 .filter(function(d) {
832 return (d.id in invalidRelationTargets &&
833@@ -1171,73 +1061,6 @@
834 this.set('currentServiceClickAction', 'ambiguousAddRelationCheck');
835 },
836
837-
838- /*
839- * Zoom in event handler.
840- */
841- zoom_out: function(data, context) {
842- var slider = context.slider,
843- val = slider.get('value');
844- slider.set('value', val - 25);
845- },
846-
847- /*
848- * Zoom out event handler.
849- */
850- zoom_in: function(data, context) {
851- var slider = context.slider,
852- val = slider.get('value');
853- slider.set('value', val + 25);
854- },
855-
856- /*
857- * Wraper around the actual rescale method for zoom buttons.
858- */
859- _fire_zoom: function(delta) {
860- var vis = this.vis,
861- zoom = this.zoom,
862- evt = {};
863-
864- // Build a temporary event that rescale can use of a similar
865- // construction to d3.event.
866- evt.translate = zoom.translate();
867- evt.scale = zoom.scale() + delta;
868-
869- // Update the scale in our zoom behavior manager to maintain state.
870- zoom.scale(evt.scale);
871-
872- // Update the translate so that we scale from the center
873- // instead of the origin.
874- var rect = vis.select('rect');
875- evt.translate[0] -= parseInt(rect.attr('width'), 10) / 2 * delta;
876- evt.translate[1] -= parseInt(rect.attr('height'), 10) / 2 * delta;
877- zoom.translate(evt.translate);
878-
879- this.rescale(vis, evt);
880- },
881-
882- /*
883- * Rescale the visualization on a zoom/pan event.
884- */
885- rescale: function(vis, evt) {
886- // Make sure we don't scale outside of our bounds.
887- // This check is needed because we're messing with d3's zoom
888- // behavior outside of mouse events (e.g.: with the slider),
889- // and can't trust that zoomExtent will play well.
890- var new_scale = Math.floor(evt.scale * 100);
891- if (new_scale < 25 || new_scale > 200) {
892- evt.scale = this.get('scale');
893- }
894- // Store the current value of scale so that it can be restored later.
895- this.set('scale', evt.scale);
896- // Store the current value of translate as well, by copying the event
897- // array in order to avoid reference sharing.
898- this.set('translate', evt.translate.slice(0));
899- vis.attr('transform', 'translate(' + evt.translate + ')' +
900- ' scale(' + evt.scale + ')');
901- this.updateServiceMenuLocation();
902- },
903-
904 /*
905 * Event handler to show the graph-list picker
906 */
907@@ -1293,14 +1116,17 @@
908 // affect the page size, such as the charm panel, to get out of the
909 // way before we compute sizes. Note the
910 // "afterPageSizeRecalculation" event at the end of this function.
911- Y.fire('beforePageSizeRecalculation');
912 // start with some reasonable defaults
913- var vis = this.vis,
914- container = this.get('container'),
915- xscale = this.xscale,
916- yscale = this.yscale,
917- svg = container.one('svg'),
918- canvas = container.one('.crosshatch-background');
919+ console.log('setSizesFromViewPort', this, arguments);
920+ var topo = this.get('component'),
921+ container = this.get('container'),
922+ vis = topo.vis,
923+ xscale = topo.xScale,
924+ yscale = topo.yScale,
925+ svg = container.one('svg'),
926+ canvas = container.one('.topology-canvas');
927+
928+ topo.fire('beforePageSizeRecalculation');
929 // Get the canvas out of the way so we can calculate the size
930 // correctly (the canvas contains the svg). We want it to be the
931 // smallest size we accept--no smaller or bigger--or else the
932@@ -1321,25 +1147,26 @@
933 .setStyle('width', dimensions.width);
934
935 // Reset the scale parameters
936- this.xscale.domain([-dimensions.width / 2, dimensions.width / 2])
937+ topo.xScale.domain([-dimensions.width / 2, dimensions.width / 2])
938 .range([0, dimensions.width]);
939- this.yscale.domain([-dimensions.height / 2, dimensions.height / 2])
940+ topo.yScale.domain([-dimensions.height / 2, dimensions.height / 2])
941 .range([dimensions.height, 0]);
942
943- this.width = dimensions.width;
944- this.height = dimensions.height;
945- Y.fire('afterPageSizeRecalculation');
946+ topo.set('size', [dimensions.width, dimensions.height]);
947+ topo.fire('afterPageSizeRecalculation');
948 },
949
950 /*
951 * Update the location of the active service panel
952 */
953 updateServiceMenuLocation: function() {
954- var container = this.get('container'),
955- cp = container.one('.environment-menu.active'),
956- service = this.get('active_service'),
957- tr = this.zoom.translate(),
958- z = this.zoom.scale();
959+ var topo = this.get('component'),
960+ container = this.get('container'),
961+ cp = container.one('.environment-menu.active'),
962+ service = this.get('active_service'),
963+ tr = topo.get('translate'),
964+ z = topo.get('scale');
965+
966 if (service && cp) {
967 var cp_width = cp.getClientRect().width,
968 menu_left = service.x * z + service.w * z / 2 <
969@@ -1375,9 +1202,10 @@
970 }
971
972 // Do not fire unless we're within the service box.
973- var container = context.get('container'),
974+ var topo = context.get('component'),
975+ container = context.get('container'),
976 mouse_coords = d3.mouse(container.one('svg').getDOMNode());
977- if (!d.containsPoint(mouse_coords, context.zoom)) {
978+ if (!d.containsPoint(mouse_coords, topo.zoom)) {
979 return;
980 }
981
982@@ -1413,9 +1241,10 @@
983 }
984
985 // Do not fire if we're within the service box.
986- var container = self.get('container'),
987+ var topo = this.get('component'),
988+ container = self.get('container'),
989 mouse_coords = d3.mouse(container.one('svg').getDOMNode());
990- if (d.containsPoint(mouse_coords, self.zoom)) {
991+ if (d.containsPoint(mouse_coords, topo.zoom)) {
992 return;
993 }
994 var rect = Y.one(this).one('.service-border');
995@@ -1486,8 +1315,9 @@
996 * View a service
997 */
998 show_service: function(m, context) {
999- context.get('component')
1000- .fire('navigateTo', {url: '/service/' + m.get('id') + '/'});
1001+ var topo = context.get('component');
1002+ topo.detachContainer();
1003+ topo.fire('navigateTo', {url: '/service/' + m.get('id') + '/'});
1004 },
1005
1006 /*
1007@@ -1567,11 +1397,12 @@
1008 * create the relation if not.
1009 */
1010 ambiguousAddRelationCheck: function(m, view, context) {
1011- var endpoints = view
1012- .get('addRelationStart_possibleEndpoints')[m.id],
1013- container = view.get('container');
1014+ var endpoints = view.get(
1015+ 'addRelationStart_possibleEndpoints')[m.id],
1016+ container = view.get('container'),
1017+ topo = view.get('component');
1018
1019- if (endpoints.length === 1) {
1020+ if (endpoints && endpoints.length === 1) {
1021 // Create a relation with the only available endpoint.
1022 var ep = endpoints[0],
1023 endpoints_item = [
1024@@ -1629,8 +1460,8 @@
1025 });
1026
1027 // Display the menu at the service endpoint.
1028- var tr = view.zoom.translate(),
1029- z = view.zoom.scale();
1030+ var tr = topo.zoom.translate(),
1031+ z = topo.zoom.scale();
1032 menu.setStyle('top', m.y * z + tr[1]);
1033 menu.setStyle('left', m.x * z + m.w * z + tr[0]);
1034 menu.addClass('active');
1035@@ -1640,25 +1471,25 @@
1036 },
1037
1038 /*
1039- * Fired when clicking the second service is clicked in the
1040- * add relation flow.
1041- *
1042- * :param endpoints: array of two endpoints, each in the form
1043- * ['service name', {
1044- * name: 'endpoint type',
1045- * role: 'client or server'
1046- * }]
1047- */
1048+ * Fired when clicking the second service is clicked in the
1049+ * add relation flow.
1050+ *
1051+ * :param endpoints: array of two endpoints, each in the form
1052+ * ['service name', {
1053+ * name: 'endpoint type',
1054+ * role: 'client or server'
1055+ * }]
1056+ */
1057 addRelationEnd: function(endpoints, view, context) {
1058 // Redisplay all services
1059 view.cancelRelationBuild();
1060
1061 // Get the vis, and links, build the new relation.
1062- var vis = view.vis,
1063- env = view.get('component').get('env'),
1064- db = view.get('component').get('db'),
1065- source = view.get('addRelationStart_service'),
1066- relation_id = 'pending:' + endpoints[0][0] + endpoints[1][0];
1067+ var vis = view.get('component').vis,
1068+ env = view.get('component').get('env'),
1069+ db = view.get('component').get('db'),
1070+ source = view.get('addRelationStart_service'),
1071+ relation_id = 'pending:' + endpoints[0][0] + endpoints[1][0];
1072
1073 if (endpoints[0][0] === endpoints[1][0]) {
1074 view.set('currentServiceClickAction', 'toggleControlPanel');
1075@@ -1676,7 +1507,9 @@
1076
1077 // Firing the update event on the db will properly redraw the
1078 // graph and reattach events.
1079- db.fire('update');
1080+ //db.fire('update');
1081+ view.get('component').bindAllD3Events();
1082+ view.update();
1083
1084 // Fire event to add relation in juju.
1085 // This needs to specify interface in the future.
1086
1087=== modified file 'app/views/topology/panzoom.js'
1088--- app/views/topology/panzoom.js 2012-12-11 03:58:03 +0000
1089+++ app/views/topology/panzoom.js 2012-12-20 16:25:28 +0000
1090@@ -6,25 +6,174 @@
1091 d3ns = Y.namespace('d3');
1092
1093 /**
1094+ * Handle PanZoom within the a Topology.
1095+ *
1096+ * Emitted events:
1097+ *
1098+ * rescaled: post-zoom event, after the scene has been rescaled,
1099+ * queried object positions should be accurate.
1100+ *
1101 * @module topology-panzoom
1102 * @class PanZoomModule
1103 * @namespace views
1104 **/
1105 var PanZoomModule = Y.Base.create('PanZoomModule', d3ns.Module, [], {
1106+
1107+ events: {
1108+ scene: {
1109+ '#zoom-out-btn': {click: 'zoom_out'},
1110+ '#zoom-in-btn': {click: 'zoom_in'}
1111+ },
1112+ yui: {
1113+ zoom: {callback: 'zoomHandler'},
1114+ rendered: {callback: 'renderedHandler'}
1115+ }
1116+ },
1117+
1118 initializer: function(options) {
1119 PanZoomModule.superclass.constructor.apply(this, arguments);
1120- },
1121-
1122- render: function() {
1123- PanZoomModule.superclass.render.apply(this, arguments);
1124- return this;
1125+ this._translate = [0, 0];
1126+ this._scale = 1.0;
1127+ },
1128+
1129+ // Handler for 'zoom' event.
1130+ zoomHandler: function(evt) {
1131+ var s = this.slider,
1132+ vis = this.get('component').vis;
1133+
1134+ s.set('value', Math.floor(evt.scale * 100));
1135+ this.rescale(vis, evt);
1136+ },
1137+
1138+ renderSlider: function() {
1139+ var self = this,
1140+ topo = this.get('component'),
1141+ contianer = topo.get('container'),
1142+ value = 100,
1143+ currentScale = topo.get('scale');
1144+
1145+ if (self.slider) {
1146+ return;
1147+ }
1148+ // Build a slider to control zoom level
1149+ if (currentScale) {
1150+ value = currentScale * 100;
1151+ }
1152+ var slider = new Y.Slider({
1153+ min: 25,
1154+ max: 200,
1155+ value: value
1156+ });
1157+ slider.render('#slider-parent');
1158+ topo.recordSubscription(this,
1159+ slider.after('valueChange', function(evt) {
1160+ // Don't fire a zoom if there's a zoom event
1161+ // already in progress; that will run rescale
1162+ // for us.
1163+ if (d3.event && d3.event.scale &&
1164+ d3.event.translate) {
1165+ return;
1166+ }
1167+ self._fire_zoom((
1168+ evt.newVal - evt.prevVal) / 100);
1169+ }));
1170+ self.slider = slider;
1171 },
1172
1173 update: function() {
1174 PanZoomModule.superclass.update.apply(this, arguments);
1175 return this;
1176+ },
1177+
1178+ /*
1179+ * Zoom out event handler.
1180+ */
1181+ zoom_out: function(data, context) {
1182+ var slider = context.slider,
1183+ val = slider.get('value');
1184+ slider.set('value', val - 25);
1185+ },
1186+
1187+ /*
1188+ * Zoom in event handler.
1189+ */
1190+ zoom_in: function(data, context) {
1191+ var slider = context.slider,
1192+ val = slider.get('value');
1193+ slider.set('value', val + 25);
1194+ },
1195+
1196+ /*
1197+ * Wrapper around the actual rescale method for zoom buttons.
1198+ */
1199+ _fire_zoom: function(delta) {
1200+ var topo = this.get('component'),
1201+ vis = topo.vis,
1202+ zoom = topo.zoom,
1203+ evt = {};
1204+
1205+ // Build a temporary event that rescale can use of a similar
1206+ // construction to d3.event.
1207+ evt.translate = zoom.translate();
1208+ evt.scale = zoom.scale() + delta;
1209+
1210+ // Update the scale in our zoom behavior manager to maintain state.
1211+ zoom.scale(evt.scale);
1212+
1213+ // Update the translate so that we scale from the center
1214+ // instead of the origin.
1215+ var rect = vis.select('rect');
1216+ evt.translate[0] -= parseInt(rect.attr('width'), 10) / 2 * delta;
1217+ evt.translate[1] -= parseInt(rect.attr('height'), 10) / 2 * delta;
1218+ zoom.translate(evt.translate);
1219+
1220+ this.rescale(vis, evt);
1221+ },
1222+
1223+ /*
1224+ * Rescale the visualization on a zoom/pan event.
1225+ */
1226+ rescale: function(vis, evt) {
1227+ // Make sure we don't scale outside of our bounds.
1228+ // This check is needed because we're messing with d3's zoom
1229+ // behavior outside of mouse events (e.g.: with the slider),
1230+ // and can't trust that zoomExtent will play well.
1231+ var new_scale = Math.floor(evt.scale * 100),
1232+ topo = this.get('component');
1233+
1234+ if (new_scale < 25 || new_scale > 200) {
1235+ evt.scale = topo.get('scale');
1236+ }
1237+ // Store the current value of scale so that it can be restored later.
1238+ this._scale = evt.scale;
1239+ // Store the current value of translate as well, by copying the event
1240+ // array in order to avoid reference sharing.
1241+ this._translate = Y.mix(evt.translate);
1242+ vis.attr('transform', 'translate(' + evt.translate + ')' +
1243+ ' scale(' + evt.scale + ')');
1244+ topo.fire('rescaled');
1245+ },
1246+
1247+ renderedHandler: function(evt) {
1248+ // Preserve zoom when the scene is updated.
1249+ var topo = this.get('component'),
1250+ changed = false,
1251+ currentScale = this._scale,
1252+ currentTranslate = this._translate;
1253+
1254+ this.renderSlider();
1255+ if (currentTranslate && currentTranslate !== topo.get('translate')) {
1256+ topo.zoom.translate(currentTranslate);
1257+ changed = true;
1258+ }
1259+ if (currentScale && currentScale !== topo.zoom.scale()) {
1260+ topo.zoom.scale(currentScale);
1261+ changed = true;
1262+ }
1263+ if (changed) {
1264+ this._fire_zoom(0);
1265+ }
1266 }
1267-
1268 }, {
1269 ATTRS: {}
1270
1271
1272=== modified file 'app/views/topology/topology.js'
1273--- app/views/topology/topology.js 2012-12-05 05:23:37 +0000
1274+++ app/views/topology/topology.js 2012-12-20 16:25:28 +0000
1275@@ -14,6 +14,11 @@
1276 * configuration belong here. If the only shared requirement on shared state
1277 * is watch/event like behavior fire an event and place the logic in a module.
1278 *
1279+ * Emmitted Events:
1280+ *
1281+ * zoom: When the zoom level of the canvas changes a 'zoom'
1282+ * event is fired. Analogous to d3's zoom event.
1283+ *
1284 * @class Topology
1285 * @namespace juju.views
1286 **/
1287@@ -23,6 +28,33 @@
1288 this.options = Y.mix(options || {});
1289 },
1290
1291+ /**
1292+ * Called by render, conditionally attach container to the DOM if
1293+ * it isn't already. The framework calls this before module
1294+ * rendering so that d3 Events will have attached DOM elements. If
1295+ * your application doesn't need this behavior feel free to override.
1296+ *
1297+ * In this case we currently rely on app.showView to do all the
1298+ * container management, this only works on a preserved view.
1299+ *
1300+ * @method attachContainer
1301+ * @chainable
1302+ **/
1303+ attachContainer: function() {
1304+ return this;
1305+ },
1306+
1307+ /**
1308+ * Remove container from DOM returning container. This
1309+ * is explicitly not chainable.
1310+ *
1311+ * @method detachContainer
1312+ **/
1313+ detachContainer: function() {
1314+ return;
1315+ },
1316+
1317+
1318 renderOnce: function() {
1319 var self = this,
1320 vis,
1321@@ -31,30 +63,72 @@
1322 container = this.get('container'),
1323 templateName = this.options.template || 'overview';
1324
1325- if (this.svg) {
1326+ if (this._templateRendered) {
1327 return;
1328 }
1329- container.setHTML(views.Templates[templateName]());
1330+ //container.setHTML(views.Templates[templateName]());
1331 // Take the first element.
1332- this.svg = container.one(':first-child');
1333+ this._templateRendered = true;
1334+
1335+ // Create a pan/zoom behavior manager.
1336+ this.xScale = d3.scale.linear()
1337+ .domain([-width / 2, width / 2])
1338+ .range([0, width]);
1339+ this.yScale = d3.scale.linear()
1340+ .domain([-height / 2, height / 2])
1341+ .range([height, 0]);
1342+
1343+ // Include very basic behavior, fire
1344+ // yui event for anything more complex.
1345+ this.zoom = d3.behavior.zoom()
1346+ .x(this.xScale)
1347+ .y(this.yScale)
1348+ .scaleExtent([0.25, 2.0])
1349+ .on('zoom', function(evt) {
1350+ // This will add the d3 properties to the
1351+ // eventFacade
1352+ self.fire('zoom', d3.event);
1353+ });
1354
1355 // Set up the visualization with a pack layout.
1356 vis = d3.select(container.getDOMNode())
1357- .selectAll('.topology-canvas')
1358- .append('svg:svg')
1359- .attr('pointer-events', 'all')
1360- .attr('width', width)
1361- .attr('height', height)
1362- .append('svg:g')
1363- .append('g');
1364+ .selectAll('.topology-canvas')
1365+ .append('svg:svg')
1366+ .attr('pointer-events', 'all')
1367+ .attr('width', width)
1368+ .attr('height', height)
1369+ .append('svg:g')
1370+ .call(this.zoom)
1371+ .append('g');
1372
1373 vis.append('svg:rect')
1374- .attr('class', 'graph')
1375- .attr('fill', 'rgba(255,255,255,0)');
1376+ .attr('class', 'graph')
1377+ .attr('fill', 'rgba(255,255,255,0)');
1378
1379 this.vis = vis;
1380
1381+ // Build out scale and zoom.
1382+ // These are defaults, a (Viewport) Module
1383+ // can implement policy around them.
1384+ this.sizeChangeHandler();
1385+ this.on('sizeChanged', this.sizeChangeHandler);
1386+
1387+ Topology.superclass.renderOnce.apply(this, arguments);
1388 return this;
1389+ },
1390+
1391+ sizeChangeHandler: function() {
1392+ var self = this,
1393+ width = this.get('width'),
1394+ height = this.get('height');
1395+
1396+ // Update the pan/zoom behavior manager.
1397+ this.xScale.domain([-width / 2, width / 2])
1398+ .range([0, width]);
1399+ this.yScale.domain([-height / 2, height / 2])
1400+ .range([height, 0]);
1401+ this.zoom.x(this.xScale)
1402+ .y(this.yScale);
1403 }
1404
1405 }, {
1406@@ -82,9 +156,9 @@
1407 /**
1408 * @property {Array} transform
1409 **/
1410- transform: {
1411- getter: function() {return this.get('zoom').transform();},
1412- setter: function(v) {this.get('zoom').transform(v);}
1413+ translate: {
1414+ getter: function() {return this.zoom.translate();},
1415+ setter: function(v) {this.zoom.translate(v);}
1416 },
1417
1418 width: {
1419
1420=== modified file 'test/test_d3_components.js'
1421--- test/test_d3_components.js 2012-12-14 15:25:55 +0000
1422+++ test/test_d3_components.js 2012-12-20 16:25:28 +0000
1423@@ -73,7 +73,7 @@
1424 comp.addModule(TestModule);
1425
1426 // Test that default bindings work by simulating
1427- Y.fire('cancel');
1428+ comp.fire('cancel');
1429 state.cancelled.should.equal(true);
1430
1431 // XXX: While on the plane I determined that things like
1432@@ -84,12 +84,12 @@
1433 state.cancelled = false;
1434 comp.removeModule('TestModule');
1435
1436- Y.fire('cancel');
1437+ comp.fire('cancel');
1438 state.cancelled.should.equal(false);
1439
1440 // Adding the module back again doesn't create any issues.
1441 comp.addModule(TestModule);
1442- Y.fire('cancel');
1443+ comp.fire('cancel');
1444 state.cancelled.should.equal(true);
1445
1446 // Simulated events on DOM handlers better work.
1447@@ -137,7 +137,9 @@
1448 modA.windowResizeHandler = function(evt) {
1449 resized = true;
1450 };
1451- modA.events.yui.windowresize = 'windowResizeHandler';
1452+ modA.events.yui.windowresize = {
1453+ callback: 'windowResizeHandler',
1454+ context: 'window'};
1455 comp.addModule(modA);
1456 var subscription = Y.after('windowresize', function(evt) {
1457 subscription.detach();
1458
1459=== modified file 'test/test_environment_view.js'
1460--- test/test_environment_view.js 2012-12-14 15:25:55 +0000
1461+++ test/test_environment_view.js 2012-12-20 16:25:28 +0000
1462@@ -119,15 +119,18 @@
1463 });
1464
1465 it('must handle the window resize event', function(done) {
1466- var view = new views.environment({container: container, db: db});
1467+ var view = new views.environment({container: container, db: db}),
1468+ topo,
1469+ beforeResizeEventFired = false;
1470 view.render();
1471- var beforeResizeEventFired = false;
1472- Y.once('beforePageSizeRecalculation', function() {
1473- // This event must be fired by views.MegaModule.setSizesFromViewport.
1474+ topo = view.topo;
1475+
1476+ topo.once('beforePageSizeRecalculation', function() {
1477+ // This event must be fired.
1478 beforeResizeEventFired = true;
1479 });
1480- Y.once('afterPageSizeRecalculation', function() {
1481- // This event must be fired by views.MegaModule.setSizesFromViewport.
1482+ topo.once('afterPageSizeRecalculation', function() {
1483+ // This event must be fired.
1484 assert.isTrue(beforeResizeEventFired);
1485 done();
1486 });
1487@@ -257,7 +260,7 @@
1488 view.postRender();
1489 var zoom_in = container.one('#zoom-in-btn'),
1490 zoom_out = container.one('#zoom-out-btn'),
1491- module = view.topo.modules.MegaModule,
1492+ module = view.topo.modules.PanZoomModule,
1493 slider = module.slider,
1494 svg = container.one('svg g g');
1495
1496
1497=== modified file 'test/test_topology.js'
1498--- test/test_topology.js 2012-12-11 15:35:25 +0000
1499+++ test/test_topology.js 2012-12-20 16:25:28 +0000
1500@@ -72,7 +72,7 @@
1501 topo.render();
1502
1503 // Verify that we have built the default scene.
1504- Y.Lang.isValue(topo.svg).should.equal(true);
1505+ Y.Lang.isValue(topo.vis).should.equal(true);
1506 });
1507
1508 function createStandardTopo() {
1509@@ -80,6 +80,7 @@
1510 topo = new views.Topology();
1511 topo.setAttrs({container: container, db: db});
1512 topo.addModule(views.MegaModule);
1513+ topo.addModule(views.PanZoomModule);
1514 return topo;
1515 }
1516
1517@@ -88,7 +89,7 @@
1518 topo = createStandardTopo();
1519 topo.render();
1520 // Verify that we have built the default scene.
1521- Y.Lang.isValue(topo.svg).should.equal(true);
1522+ Y.Lang.isValue(topo.vis).should.equal(true);
1523 });
1524
1525 });
1526
1527=== modified file 'undocumented'
1528--- undocumented 2012-12-11 04:11:39 +0000
1529+++ undocumented 2012-12-20 16:25:28 +0000
1530@@ -1,5 +1,5 @@
1531 app/app.js:95 "callback"
1532-app/app.js:569 "callback"
1533+app/app.js:552 "callback"
1534 app/store/env.js:64 "on_close"
1535 app/store/env.js:69 "on_message"
1536 app/store/env.js:181 "status"
1537@@ -71,30 +71,30 @@
1538 app/views/utils.js:647 "get"
1539 app/views/utils.js:276 "removeSVGClass"
1540 app/views/environment.js:24 "initializer"
1541-app/views/environment.js:29 "render"
1542-app/views/environment.js:61 "postRender"
1543-app/views/charm-panel.js:948 "createInstance"
1544+app/views/environment.js:63 "postRender"
1545+app/views/environment.js:31 "render"
1546+app/views/charm-panel.js:1168 "calculatePanelPosition"
1547 app/views/charm-panel.js:476 "initializer"
1548 app/views/charm-panel.js:250 "render"
1549-app/views/charm-panel.js:1140 "calculatePanelPosition"
1550-app/views/charm-panel.js:723 "showDescription"
1551+app/views/charm-panel.js:901 "onCharmDeployClicked"
1552+app/views/charm-panel.js:976 "createInstance"
1553+app/views/charm-panel.js:764 "hideDescription"
1554+app/views/charm-panel.js:1022 "setPanel"
1555 app/views/charm-panel.js:332 "showConfiguration"
1556-app/views/charm-panel.js:936 "setupOverlay"
1557+app/views/charm-panel.js:1151 "updatePanelPosition"
1558+app/views/charm-panel.js:1213 "killInstance"
1559 app/views/charm-panel.js:655 "render"
1560-app/views/charm-panel.js:873 "onCharmDeployClicked"
1561-app/views/charm-panel.js:736 "hideDescription"
1562+app/views/charm-panel.js:1198 "setDefaultSeries"
1563 app/views/charm-panel.js:195 "mouseleave"
1564 app/views/charm-panel.js:417 "_showErrors"
1565-app/views/charm-panel.js:1179 "getInstance"
1566+app/views/charm-panel.js:651 "initializer"
1567 app/views/charm-panel.js:480 "render"
1568-app/views/charm-panel.js:994 "setPanel"
1569-app/views/charm-panel.js:651 "initializer"
1570+app/views/charm-panel.js:751 "showDescription"
1571+app/views/charm-panel.js:725 "_moveTooltip"
1572 app/views/charm-panel.js:209 "initializer"
1573-app/views/charm-panel.js:1123 "updatePanelPosition"
1574-app/views/charm-panel.js:700 "_moveTooltip"
1575+app/views/charm-panel.js:964 "setupOverlay"
1576 app/views/charm-panel.js:192 "mouseenter"
1577-app/views/charm-panel.js:1170 "setDefaultSeries"
1578-app/views/charm-panel.js:1185 "killInstance"
1579+app/views/charm-panel.js:1207 "getInstance"
1580 app/views/charm.js:32 "render"
1581 app/views/charm.js:96 "_deployCallback"
1582 app/views/charm.js:61 "on_charm_data"
1583@@ -154,60 +154,59 @@
1584 app/views/service.js:23 "resetUnits"
1585 app/views/service.js:230 "unexposeService"
1586 app/views/service.js:237 "_unexposeServiceCallback"
1587-app/views/topology/mega.js:1441 "subRelBlockMouseLeave"
1588-app/views/topology/mega.js:892 "show"
1589-app/views/topology/mega.js:509 "drawRelationGroup"
1590-app/views/topology/mega.js:1337 "updateServiceMenuLocation"
1591-app/views/topology/mega.js:1187 "zoom_in"
1592-app/views/topology/mega.js:1196 "_fire_zoom"
1593-app/views/topology/mega.js:189 "renderOnce"
1594-app/views/topology/mega.js:574 "drawRelation"
1595-app/views/topology/mega.js:1036 "removeRelation"
1596-app/views/topology/mega.js:1222 "rescale"
1597-app/views/topology/mega.js:265 "serviceDblClick"
1598-app/views/topology/mega.js:1469 "toggleControlPanel"
1599-app/views/topology/mega.js:1093 "cancelRelationBuild"
1600-app/views/topology/mega.js:1519 "destroyService"
1601-app/views/topology/mega.js:1073 "removeRelationConfirm"
1602-app/views/topology/mega.js:904 "fade"
1603-app/views/topology/mega.js:1254 "hideGraphListPicker"
1604-app/views/topology/mega.js:248 "serviceClick"
1605-app/views/topology/mega.js:590 "drawService"
1606-app/views/topology/mega.js:271 "relationClick"
1607-app/views/topology/mega.js:854 "renderSlider"
1608-app/views/topology/mega.js:165 "initializer"
1609-app/views/topology/mega.js:489 "updateLinks"
1610-app/views/topology/mega.js:1048 "_removeRelationCallback"
1611-app/views/topology/mega.js:1652 "addRelationEnd"
1612-app/views/topology/mega.js:1432 "subRelBlockMouseEnter"
1613-app/views/topology/mega.js:362 "updateCanvas"
1614-app/views/topology/mega.js:1409 "serviceMouseLeave"
1615-app/views/topology/mega.js:1244 "showGraphListPicker"
1616-app/views/topology/mega.js:322 "updateData"
1617-app/views/topology/mega.js:176 "render"
1618-app/views/topology/mega.js:97 "callback"
1619-app/views/topology/mega.js:976 "addRelationDragStart"
1620-app/views/topology/mega.js:883 "serviceForBox"
1621-app/views/topology/mega.js:1496 "destroyServiceConfirm"
1622-app/views/topology/mega.js:1178 "zoom_out"
1623-app/views/topology/mega.js:1691 "_addRelationCallback"
1624-app/views/topology/mega.js:999 "addRelationDrag"
1625-app/views/topology/mega.js:1558 "addRelationStart"
1626-app/views/topology/mega.js:965 "addRelation"
1627-app/views/topology/mega.js:823 "processRelations"
1628-app/views/topology/mega.js:1291 "setSizesFromViewport"
1629-app/views/topology/mega.js:1370 "serviceMouseEnter"
1630-app/views/topology/mega.js:812 "processRelation"
1631-app/views/topology/mega.js:1488 "show_service"
1632-app/views/topology/mega.js:922 "postRender"
1633-app/views/topology/mega.js:898 "hide"
1634-app/views/topology/mega.js:1017 "addRelationDragEnd"
1635-app/views/topology/mega.js:1569 "ambiguousAddRelationCheck"
1636-app/views/topology/mega.js:1528 "_destroyCallback"
1637-app/views/topology/mega.js:848 "subordinateRelationsForService"
1638-app/views/topology/panzoom.js:14 "initializer"
1639-app/views/topology/panzoom.js:18 "render"
1640-app/views/topology/panzoom.js:23 "update"
1641+app/views/topology/mega.js:810 "fade"
1642+app/views/topology/mega.js:173 "serviceClick"
1643+app/views/topology/mega.js:935 "_removeRelationCallback"
1644+app/views/topology/mega.js:1067 "showGraphListPicker"
1645+app/views/topology/mega.js:1523 "_addRelationCallback"
1646+app/views/topology/mega.js:419 "updateLinks"
1647+app/views/topology/mega.js:861 "addRelationDragStart"
1648+app/views/topology/mega.js:850 "addRelation"
1649+app/views/topology/mega.js:828 "renderedHandler"
1650+app/views/topology/mega.js:1077 "hideGraphListPicker"
1651+app/views/topology/mega.js:248 "updateData"
1652+app/views/topology/mega.js:789 "serviceForBox"
1653+app/views/topology/mega.js:1325 "destroyServiceConfirm"
1654+app/views/topology/mega.js:1260 "subRelBlockMouseEnter"
1655+app/views/topology/mega.js:505 "drawRelation"
1656+app/views/topology/mega.js:1357 "_destroyCallback"
1657+app/views/topology/mega.js:754 "processRelations"
1658+app/views/topology/mega.js:1297 "toggleControlPanel"
1659+app/views/topology/mega.js:1482 "addRelationEnd"
1660+app/views/topology/mega.js:743 "processRelation"
1661+app/views/topology/mega.js:1316 "show_service"
1662+app/views/topology/mega.js:191 "serviceDblClick"
1663+app/views/topology/mega.js:798 "show"
1664+app/views/topology/mega.js:439 "drawRelationGroup"
1665+app/views/topology/mega.js:904 "addRelationDragEnd"
1666+app/views/topology/mega.js:886 "addRelationDrag"
1667+app/views/topology/mega.js:289 "update"
1668+app/views/topology/mega.js:1269 "subRelBlockMouseLeave"
1669+app/views/topology/mega.js:1196 "serviceMouseEnter"
1670+app/views/topology/mega.js:960 "removeRelationConfirm"
1671+app/views/topology/mega.js:1387 "addRelationStart"
1672+app/views/topology/mega.js:1398 "ambiguousAddRelationCheck"
1673+app/views/topology/mega.js:923 "removeRelation"
1674+app/views/topology/mega.js:980 "cancelRelationBuild"
1675+app/views/topology/mega.js:92 "callback"
1676+app/views/topology/mega.js:521 "drawService"
1677+app/views/topology/mega.js:804 "hide"
1678+app/views/topology/mega.js:163 "initializer"
1679+app/views/topology/mega.js:197 "relationClick"
1680+app/views/topology/mega.js:1348 "destroyService"
1681+app/views/topology/mega.js:1161 "updateServiceMenuLocation"
1682+app/views/topology/mega.js:1114 "setSizesFromViewport"
1683+app/views/topology/mega.js:1236 "serviceMouseLeave"
1684+app/views/topology/mega.js:779 "subordinateRelationsForService"
1685+app/views/topology/panzoom.js:83 "update"
1686+app/views/topology/panzoom.js:40 "zoomHandler"
1687+app/views/topology/panzoom.js:100 "zoom_in"
1688+app/views/topology/panzoom.js:136 "rescale"
1689+app/views/topology/panzoom.js:109 "_fire_zoom"
1690+app/views/topology/panzoom.js:33 "initializer"
1691+app/views/topology/panzoom.js:157 "renderedHandler"
1692+app/views/topology/panzoom.js:91 "zoom_out"
1693+app/views/topology/panzoom.js:48 "renderSlider"
1694 app/views/topology/relation.js:18 "render"
1695 app/views/topology/relation.js:23 "update"
1696 app/views/topology/relation.js:14 "initializer"
1697@@ -216,14 +215,15 @@
1698 app/views/topology/service.js:26 "initializer"
1699 app/views/topology/service.js:50 "update"
1700 app/views/topology/service.js:39 "_scaleLayout"
1701-app/views/topology/topology.js:26 "renderOnce"
1702-app/views/topology/topology.js:87 "setter"
1703-app/views/topology/topology.js:95 "getter"
1704-app/views/topology/topology.js:91 "getter"
1705-app/views/topology/topology.js:80 "setter"
1706-app/views/topology/topology.js:86 "getter"
1707-app/views/topology/topology.js:21 "initializer"
1708-app/views/topology/topology.js:79 "getter"
1709+app/views/topology/topology.js:161 "setter"
1710+app/views/topology/topology.js:26 "initializer"
1711+app/views/topology/topology.js:154 "setter"
1712+app/views/topology/topology.js:165 "getter"
1713+app/views/topology/topology.js:120 "sizeChangeHandler"
1714+app/views/topology/topology.js:160 "getter"
1715+app/views/topology/topology.js:153 "getter"
1716+app/views/topology/topology.js:169 "getter"
1717+app/views/topology/topology.js:58 "renderOnce"
1718 app/views/topology/viewport.js:33 "initializer"
1719 app/views/topology/viewport.js:66 "update"
1720 app/views/topology/viewport.js:37 "render"

Subscribers

People subscribed via source and target branches