blob: ca8d6d30b7e2e940785a90351d8aaab37ee60ac4 [file] [log] [blame]
Paul Greyson740bdaf2013-03-18 16:10:48 -07001/*global d3*/
2
Paul Greyson6d9ed862013-03-23 17:37:15 -07003d3.selection.prototype.moveToFront = function() {
4 return this.each(function(){
5 this.parentNode.appendChild(this);
6 });
7};
Paul Greysond1a22d92013-03-19 12:15:19 -07008
9var colors = [
Paul Greyson3e142162013-03-19 13:56:17 -070010 'color1',
11 'color2',
12 'color3',
13 'color4',
14 'color5',
15 'color6',
16 'color7',
17 'color8',
18 'color9',
19 'color10',
20 'color11',
21 'color12',
Paul Greysond1a22d92013-03-19 12:15:19 -070022]
Paul Greyson01a5dff2013-03-19 15:50:14 -070023colors.reverse();
Paul Greysond1a22d92013-03-19 12:15:19 -070024
25var controllerColorMap = {};
26
27
28
Paul Greyson740bdaf2013-03-18 16:10:48 -070029function createTopologyView() {
Paul Greysonb367de22013-03-23 11:09:11 -070030 var svg = d3.select('#svg-container').append('svg:svg');
31
32 svg.append("svg:defs").append("svg:marker")
33 .attr("id", "arrow")
34 .attr("viewBox", "0 -5 10 10")
35 .attr("refX", -1)
36 .attr("markerWidth", 5)
37 .attr("markerHeight", 5)
38 .attr("orient", "auto")
39 .append("svg:path")
Paul Greyson45303ac2013-03-23 16:44:01 -070040 .attr("d", "M0,-3L10,0L0,3");
Paul Greysonb367de22013-03-23 11:09:11 -070041
42 return svg.append('svg:svg').attr('id', 'viewBox').attr('viewBox', '0 0 1000 1000').attr('preserveAspectRatio', 'none').
Paul Greyson952ccb62013-03-18 22:22:08 -070043 attr('id', 'viewbox').append('svg:g').attr('transform', 'translate(500 500)');
Paul Greyson740bdaf2013-03-18 16:10:48 -070044}
45
Paul Greysond1a22d92013-03-19 12:15:19 -070046function updateHeader(model) {
Paul Greysonb48943b2013-03-19 13:27:57 -070047 d3.select('#lastUpdate').text(new Date());
Paul Greyson952ccb62013-03-18 22:22:08 -070048 d3.select('#activeSwitches').text(model.edgeSwitches.length + model.aggregationSwitches.length + model.coreSwitches.length);
49 d3.select('#activeFlows').text(model.flows.length);
50}
51
52function toRadians (angle) {
53 return angle * (Math.PI / 180);
Paul Greyson740bdaf2013-03-18 16:10:48 -070054}
55
Paul Greysonc17278a2013-03-23 10:17:12 -070056function createRingsFromModel(model) {
Paul Greyson740bdaf2013-03-18 16:10:48 -070057 var rings = [{
58 radius: 3,
Paul Greysonde7fad52013-03-19 12:47:32 -070059 width: 6,
Paul Greyson952ccb62013-03-18 22:22:08 -070060 switches: model.edgeSwitches,
Paul Greysonde7fad52013-03-19 12:47:32 -070061 className: 'edge',
62 angles: []
Paul Greyson740bdaf2013-03-18 16:10:48 -070063 }, {
Paul Greysond1a22d92013-03-19 12:15:19 -070064 radius: 2.25,
Paul Greysonde7fad52013-03-19 12:47:32 -070065 width: 12,
Paul Greyson952ccb62013-03-18 22:22:08 -070066 switches: model.aggregationSwitches,
Paul Greysonde7fad52013-03-19 12:47:32 -070067 className: 'aggregation',
68 angles: []
Paul Greyson740bdaf2013-03-18 16:10:48 -070069 }, {
Paul Greysond1a22d92013-03-19 12:15:19 -070070 radius: .75,
Paul Greysonde7fad52013-03-19 12:47:32 -070071 width: 18,
Paul Greyson952ccb62013-03-18 22:22:08 -070072 switches: model.coreSwitches,
Paul Greysonde7fad52013-03-19 12:47:32 -070073 className: 'core',
74 angles: []
Paul Greyson740bdaf2013-03-18 16:10:48 -070075 }];
76
Paul Greysonde7fad52013-03-19 12:47:32 -070077
78 var aggRanges = {};
79
80 // arrange edge switches at equal increments
81 var k = 360 / rings[0].switches.length;
82 rings[0].switches.forEach(function (s, i) {
83 var angle = k * i;
84
85 rings[0].angles[i] = angle;
86
87 // record the angle for the agg switch layout
88 var dpid = s.dpid.split(':');
Paul Greyson832d2202013-03-21 13:27:56 -070089 dpid[7] = '01'; // the last component of the agg switch is always '01'
Paul Greysonde7fad52013-03-19 12:47:32 -070090 var aggdpid = dpid.join(':');
91 var aggRange = aggRanges[aggdpid];
92 if (!aggRange) {
93 aggRange = aggRanges[aggdpid] = {};
94 aggRange.min = aggRange.max = angle;
95 } else {
96 aggRange.max = angle;
97 }
Paul Greysonde7fad52013-03-19 12:47:32 -070098 });
99
100 // arrange aggregation switches to "fan out" to edge switches
101 k = 360 / rings[1].switches.length;
102 rings[1].switches.forEach(function (s, i) {
103// rings[1].angles[i] = k * i;
104 var range = aggRanges[s.dpid];
105
Paul Greyson832d2202013-03-21 13:27:56 -0700106 rings[1].angles[i] = (range.min + range.max)/2;
Paul Greysonde7fad52013-03-19 12:47:32 -0700107 });
108
Paul Greyson3f890b62013-03-22 17:39:36 -0700109 // find the association between core switches and aggregation switches
110 var aggregationSwitchMap = {};
111 model.aggregationSwitches.forEach(function (s, i) {
Paul Greysonc17278a2013-03-23 10:17:12 -0700112 aggregationSwitchMap[s.dpid] = i;
Paul Greyson3f890b62013-03-22 17:39:36 -0700113 });
114
Paul Greyson3f890b62013-03-22 17:39:36 -0700115 // put core switches next to linked aggregation switches
Paul Greysonde7fad52013-03-19 12:47:32 -0700116 k = 360 / rings[2].switches.length;
117 rings[2].switches.forEach(function (s, i) {
Paul Greyson3f890b62013-03-22 17:39:36 -0700118// rings[2].angles[i] = k * i;
Paul Greysonc17278a2013-03-23 10:17:12 -0700119 var associatedAggregationSwitches = model.configuration.association[s.dpid];
120 // TODO: go between if there are multiple
121 var index = aggregationSwitchMap[associatedAggregationSwitches[0]];
122
123 rings[2].angles[i] = rings[1].angles[index];
Paul Greysonde7fad52013-03-19 12:47:32 -0700124 });
125
Paul Greyson644d92a2013-03-23 18:00:40 -0700126 // TODO: construct this form initially rather than converting. it works better because
127 // it allows binding by dpid
128 var testRings = [];
129 rings.forEach(function (ring) {
130 var testRing = [];
131 ring.switches.forEach(function (s, i) {
132 var testSwitch = {
133 dpid: s.dpid,
134 state: s.state,
135 radius: ring.radius,
136 width: ring.width,
137 className: ring.className,
138 angle: ring.angles[i],
139 controller: s.controller
140 }
141 testRing.push(testSwitch);
142 });
Paul Greyson6d9ed862013-03-23 17:37:15 -0700143
144
Paul Greyson644d92a2013-03-23 18:00:40 -0700145 testRings.push(testRing);
146 });
Paul Greyson6d9ed862013-03-23 17:37:15 -0700147
148
Paul Greyson644d92a2013-03-23 18:00:40 -0700149// return rings;
150 return testRings;
Paul Greysonc17278a2013-03-23 10:17:12 -0700151}
152
153function updateTopology(svg, model) {
154
155 // DRAW THE SWITCHES
156 var rings = svg.selectAll('.ring').data(createRingsFromModel(model));
157
Paul Greyson740bdaf2013-03-18 16:10:48 -0700158 function ringEnter(data, i) {
Paul Greyson644d92a2013-03-23 18:00:40 -0700159 if (!data.length) {
Paul Greyson740bdaf2013-03-18 16:10:48 -0700160 return;
161 }
162
Paul Greysonc17278a2013-03-23 10:17:12 -0700163 // create the nodes
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700164 var nodes = d3.select(this).selectAll("g")
Paul Greyson644d92a2013-03-23 18:00:40 -0700165 .data(data, function (data) {
166 return data.dpid;
167 })
Paul Greyson740bdaf2013-03-18 16:10:48 -0700168 .enter().append("svg:g")
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700169 .classed('nolabel', true)
Paul Greyson644d92a2013-03-23 18:00:40 -0700170 .attr("id", function (data, i) {
171 return data.dpid;
Paul Greyson23b0cd32013-03-18 23:45:48 -0700172 })
Paul Greyson644d92a2013-03-23 18:00:40 -0700173 .attr("transform", function(data, i) {
174 return "rotate(" + data.angle+ ")translate(" + data.radius * 150 + ")rotate(" + (-data.angle) + ")";
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700175 });
176
Paul Greysonc17278a2013-03-23 10:17:12 -0700177 // add the cirles representing the switches
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700178 nodes.append("svg:circle")
Paul Greyson644d92a2013-03-23 18:00:40 -0700179 .attr("transform", function(data, i) {
Paul Greysond1a22d92013-03-19 12:15:19 -0700180 var m = document.querySelector('#viewbox').getTransformToElement().inverse();
Paul Greysonf8f43172013-03-18 23:00:30 -0700181 if (data.scale) {
182 m = m.scale(data.scale);
183 }
184 return "matrix( " + m.a + " " + m.b + " " + m.c + " " + m.d + " " + m.e + " " + m.f + " )";
Paul Greyson952ccb62013-03-18 22:22:08 -0700185 })
Paul Greyson644d92a2013-03-23 18:00:40 -0700186 .attr("x", function (data) {
187 return -data.width / 2;
188 })
189 .attr("y", function (data) {
190 return -data.width / 2;
191 })
192 .attr("r", function (data) {
193 return data.width;
194 })
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700195
Paul Greysonc17278a2013-03-23 10:17:12 -0700196 // setup the mouseover behaviors
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700197 function showLabel(data, index) {
Paul Greyson644d92a2013-03-23 18:00:40 -0700198 d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', false);
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700199 }
200
201 function hideLabel(data, index) {
Paul Greyson644d92a2013-03-23 18:00:40 -0700202 d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', true);
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700203 }
204
205 nodes.on('mouseover', showLabel);
206 nodes.on('mouseout', hideLabel);
Paul Greyson740bdaf2013-03-18 16:10:48 -0700207 }
208
Paul Greysonc17278a2013-03-23 10:17:12 -0700209 // append switches
210 rings.enter().append("svg:g")
Paul Greyson740bdaf2013-03-18 16:10:48 -0700211 .attr("class", "ring")
212 .each(ringEnter);
Paul Greysonf8f43172013-03-18 23:00:30 -0700213
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700214
Paul Greysonc17278a2013-03-23 10:17:12 -0700215 function ringUpdate(data, i) {
Paul Greyson644d92a2013-03-23 18:00:40 -0700216 nodes = d3.select(this).selectAll("g")
217 .data(data, function (data) {
218 return data.dpid;
219 })
220 nodes.select('circle').attr('class', function (data, i) {
221 if (data.state == 'ACTIVE') {
222 return data.className + ' ' + controllerColorMap[data.controller];
Paul Greysonc17278a2013-03-23 10:17:12 -0700223 } else {
224 return data.className + ' ' + 'colorInactive';
225 }
226 })
227 }
228
229 // update switches
230 rings.each(ringUpdate);
231
Paul Greyson968d1b42013-03-23 16:58:41 -0700232
233 // Now setup the labels
234 // This is done separately because SVG draws in node order and we want the labels
235 // always on top
236 var labelRings = svg.selectAll('.labelRing').data(createRingsFromModel(model));
237
Paul Greyson9066ab02013-03-23 18:15:41 -0700238 function labelRingEnter(data) {
Paul Greyson644d92a2013-03-23 18:00:40 -0700239 if (!data.length) {
Paul Greyson968d1b42013-03-23 16:58:41 -0700240 return;
241 }
242
243 // create the nodes
244 var nodes = d3.select(this).selectAll("g")
Paul Greyson644d92a2013-03-23 18:00:40 -0700245 .data(data, function (data) {
246 return data.dpid;
247 })
Paul Greyson968d1b42013-03-23 16:58:41 -0700248 .enter().append("svg:g")
249 .classed('nolabel', true)
Paul Greyson9066ab02013-03-23 18:15:41 -0700250 .attr("id", function (data) {
Paul Greyson644d92a2013-03-23 18:00:40 -0700251 return data.dpid + '-label';
Paul Greyson968d1b42013-03-23 16:58:41 -0700252 })
Paul Greyson644d92a2013-03-23 18:00:40 -0700253 .attr("transform", function(data, i) {
254 return "rotate(" + data.angle+ ")translate(" + data.radius * 150 + ")rotate(" + (-data.angle) + ")";
Paul Greyson968d1b42013-03-23 16:58:41 -0700255 });
256
257 // add the text nodes which show on mouse over
258 nodes.append("svg:text")
Paul Greyson9066ab02013-03-23 18:15:41 -0700259 .text(function (data) {return data.dpid})
260 .attr("x", function (data) {
261 if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
262 if (data.className == 'edge') {
Paul Greyson1eb2dd12013-03-23 18:22:00 -0700263 return - data.width*3 - 4;
Paul Greyson9066ab02013-03-23 18:15:41 -0700264 } else {
Paul Greyson1eb2dd12013-03-23 18:22:00 -0700265 return - data.width - 4;
Paul Greyson9066ab02013-03-23 18:15:41 -0700266 }
267 } else {
268 if (data.className == 'edge') {
269 return data.width*3 + 4;
270 } else {
271 return data.width + 4;
272 }
273 }
274 })
275 .attr("y", function (data) {
276 var y;
277 if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
278 if (data.className == 'edge') {
279 y = data.width*3/2 + 4;
280 } else {
281 y = data.width/2 + 4;
282 }
283 } else {
284 if (data.className == 'edge') {
285 y = data.width*3/2 + 4;
286 } else {
287 y = data.width/2 + 4;
288 }
289 }
290 return y - 6;
291 })
292 .attr("text-anchor", function (data) {
293 if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
294 return "end";
295 } else {
296 return "start";
297 }
298 })
299 .attr("transform", function(data) {
Paul Greyson968d1b42013-03-23 16:58:41 -0700300 var m = document.querySelector('#viewbox').getTransformToElement().inverse();
301 if (data.scale) {
302 m = m.scale(data.scale);
303 }
304 return "matrix( " + m.a + " " + m.b + " " + m.c + " " + m.d + " " + m.e + " " + m.f + " )";
305 })
306 }
307
308 labelRings.enter().append("svg:g")
309 .attr("class", "textRing")
310 .each(labelRingEnter);
311
312
Paul Greysonc17278a2013-03-23 10:17:12 -0700313 // switches should not change during operation of the ui so no
314 // rings.exit()
315
316
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700317 // do mouseover zoom on edge nodes
Paul Greyson6d9ed862013-03-23 17:37:15 -0700318 function zoom(data, index) {
Paul Greyson644d92a2013-03-23 18:00:40 -0700319 var g = d3.select(document.getElementById(data.dpid)).select('circle');
320 g.transition().duration(100).attr("r", data.width*3);
Paul Greyson6d9ed862013-03-23 17:37:15 -0700321 // TODO: this doesn't work because the data binding is by index
Paul Greyson644d92a2013-03-23 18:00:40 -0700322 d3.select(this.parentNode).moveToFront();
Paul Greyson6d9ed862013-03-23 17:37:15 -0700323 }
Paul Greysonf8f43172013-03-18 23:00:30 -0700324
Paul Greyson6d9ed862013-03-23 17:37:15 -0700325 svg.selectAll('.edge').on('mouseover', zoom);
326 svg.selectAll('.edge').on('mousedown', zoom);
Paul Greysond1a22d92013-03-19 12:15:19 -0700327
Paul Greyson6d9ed862013-03-23 17:37:15 -0700328 function unzoom(data, index) {
Paul Greyson644d92a2013-03-23 18:00:40 -0700329 var g = d3.select(document.getElementById(data.dpid)).select('circle');
330 g.transition().duration(100).attr("r", data.width);
Paul Greyson6d9ed862013-03-23 17:37:15 -0700331 }
332 svg.selectAll('.edge').on('mouseout', unzoom);
Paul Greysonf9edc1a2013-03-19 13:22:06 -0700333
334
Paul Greysond1a22d92013-03-19 12:15:19 -0700335 // DRAW THE LINKS
336 var line = d3.svg.line()
337 .x(function(d) {
338 return d.x;
339 })
340 .y(function(d) {
341 return d.y;
Paul Greysonb367de22013-03-23 11:09:11 -0700342 });
343// .interpolate("basis");
Paul Greysond1a22d92013-03-19 12:15:19 -0700344
Paul Greysonc17278a2013-03-23 10:17:12 -0700345 // key on link dpids since these will come/go during demo
Paul Greysonb367de22013-03-23 11:09:11 -0700346 var links = d3.select('svg').selectAll('.link').data(model.links, function (d) {
Paul Greysonc17278a2013-03-23 10:17:12 -0700347 return d['src-switch']+'->'+d['dst-switch'];
348 });
349
350 // add new links
Paul Greysonb367de22013-03-23 11:09:11 -0700351 links.enter().append("svg:path")
352 .attr("class", "link")
353 .attr("d", function (d) {
Paul Greysonc17278a2013-03-23 10:17:12 -0700354
Paul Greysond1a22d92013-03-19 12:15:19 -0700355 var src = d3.select(document.getElementById(d['src-switch']));
356 var dst = d3.select(document.getElementById(d['dst-switch']));
357
358 var srcPt = document.querySelector('svg').createSVGPoint();
359 srcPt.x = src.attr('x');
Paul Greysonb367de22013-03-23 11:09:11 -0700360 srcPt.y = src.attr('y');
361 srcPt = srcPt.matrixTransform(src[0][0].getCTM());
Paul Greysond1a22d92013-03-19 12:15:19 -0700362
363 var dstPt = document.querySelector('svg').createSVGPoint();
364 dstPt.x = dst.attr('x');
Paul Greysonb367de22013-03-23 11:09:11 -0700365 dstPt.y = dst.attr('y'); // tmp: make up and down links distinguishable
366 dstPt = dstPt.matrixTransform(dst[0][0].getCTM());
Paul Greysond1a22d92013-03-19 12:15:19 -0700367
Paul Greysonb367de22013-03-23 11:09:11 -0700368 var midPt = document.querySelector('svg').createSVGPoint();
369 midPt.x = (srcPt.x + dstPt.x)/2;
370 midPt.y = (srcPt.y + dstPt.y)/2;
371
372 return line([srcPt, midPt, dstPt]);
373 })
374 .attr("marker-mid", function(d) { return "url(#arrow)"; });
Paul Greysonc17278a2013-03-23 10:17:12 -0700375
376 // remove old links
377 links.exit().remove();
Paul Greyson644d92a2013-03-23 18:00:40 -0700378
Paul Greysond1a22d92013-03-19 12:15:19 -0700379}
380
381function updateControllers(model) {
382 var controllers = d3.select('#controllerList').selectAll('.controller').data(model.controllers);
Paul Greyson3e142162013-03-19 13:56:17 -0700383 controllers.enter().append('div')
Paul Greysone262a292013-03-23 10:35:23 -0700384 .each(function (c) {
385 controllerColorMap[c] = colors.pop();
386 d3.select(document.body).classed(controllerColorMap[c] + '-selected', true);
387 })
388 .text(function (d) {
389 return d;
390 });
Paul Greysonbcd3c772013-03-21 13:16:44 -0700391
Paul Greysone262a292013-03-23 10:35:23 -0700392 controllers.attr('class', function (d) {
Paul Greysoneed36352013-03-23 11:19:11 -0700393 var color = 'colorInactive';
Paul Greysonbcd3c772013-03-21 13:16:44 -0700394 if (model.activeControllers.indexOf(d) != -1) {
395 color = controllerColorMap[d];
Paul Greysond1a22d92013-03-19 12:15:19 -0700396 }
Paul Greysonbcd3c772013-03-21 13:16:44 -0700397 var className = 'controller ' + color;
398 return className;
Paul Greysond1a22d92013-03-19 12:15:19 -0700399 });
Paul Greysond1a22d92013-03-19 12:15:19 -0700400
Paul Greysone262a292013-03-23 10:35:23 -0700401 // this should never be needed
402 // controllers.exit().remove();
Paul Greysond1a22d92013-03-19 12:15:19 -0700403
Paul Greyson3e142162013-03-19 13:56:17 -0700404 controllers.on('click', function (c, index) {
Paul Greysonc3e21a02013-03-21 13:56:05 -0700405 var allSelected = true;
406 for (var key in controllerColorMap) {
407 if (!d3.select(document.body).classed(controllerColorMap[key] + '-selected')) {
408 allSelected = false;
409 break;
410 }
411 }
412 if (allSelected) {
413 for (var key in controllerColorMap) {
414 d3.select(document.body).classed(controllerColorMap[key] + '-selected', key == c)
415 }
416 } else {
417 for (var key in controllerColorMap) {
418 d3.select(document.body).classed(controllerColorMap[key] + '-selected', true)
419 }
420 }
421
422 // var selected = d3.select(document.body).classed(controllerColorMap[c] + '-selected');
423 // d3.select(document.body).classed(controllerColorMap[c] + '-selected', !selected);
Paul Greysond1a22d92013-03-19 12:15:19 -0700424 });
Paul Greyson740bdaf2013-03-18 16:10:48 -0700425}
426
Paul Greysonb48943b2013-03-19 13:27:57 -0700427var oldModel;
Paul Greyson740bdaf2013-03-18 16:10:48 -0700428function sync(svg) {
Paul Greysonbcd3c772013-03-21 13:16:44 -0700429 var d = Date.now();
Paul Greysonb48943b2013-03-19 13:27:57 -0700430 updateModel(function (newModel) {
Paul Greysonbcd3c772013-03-21 13:16:44 -0700431 console.log('Update time: ' + (Date.now() - d)/1000 + 's');
Paul Greyson740bdaf2013-03-18 16:10:48 -0700432
Paul Greyson6d9ed862013-03-23 17:37:15 -0700433 if (true || !oldModel || JSON.stringify(oldModel) != JSON.stringify(newModel)) {
Paul Greysonb48943b2013-03-19 13:27:57 -0700434 updateControllers(newModel);
435 updateTopology(svg, newModel);
436 } else {
437 console.log('no change');
438 }
439 updateHeader(newModel);
440
441 oldModel = newModel;
Paul Greyson740bdaf2013-03-18 16:10:48 -0700442
443 // do it again in 1s
444 setTimeout(function () {
Paul Greysona36a9232013-03-22 22:41:27 -0700445 sync(svg)
Paul Greysond1a22d92013-03-19 12:15:19 -0700446 }, 1000);
Paul Greyson6f86d1e2013-03-18 14:40:39 -0700447 });
448}
Paul Greyson740bdaf2013-03-18 16:10:48 -0700449
Paul Greyson38d8bde2013-03-22 22:07:35 -0700450svg = createTopologyView();
451// workaround for Chrome v25 bug
452// if executed immediately, the view box transform logic doesn't work properly
453// fixed in Chrome v27
454setTimeout(function () {
455 // workaround for another Chrome v25 bug
456 // viewbox transform stuff doesn't work in combination with browser zoom
Paul Greysonc17278a2013-03-23 10:17:12 -0700457 // also works in Chrome v27
Paul Greyson38d8bde2013-03-22 22:07:35 -0700458 d3.select('#svg-container').style('zoom', window.document.body.clientWidth/window.document.width);
459 sync(svg);
460}, 100);