f9e97af61da2aead4fac91071ac2fe5de291a87b
[WebKit-https.git] / Websites / perf.webkit.org / public / v2 / interactive-chart.js
1 App.InteractiveChartComponent = Ember.Component.extend({
2     chartData: null,
3     showXAxis: true,
4     showYAxis: true,
5     interactive: false,
6     enableSelection: true,
7     classNames: ['chart'],
8     init: function ()
9     {
10         this._super();
11         this._needsConstruction = true;
12         this._eventHandlers = [];
13         $(window).resize(this._updateDimensionsIfNeeded.bind(this));
14     },
15     chartDataDidChange: function ()
16     {
17         var chartData = this.get('chartData');
18         if (!chartData)
19             return;
20         this._needsConstruction = true;
21         this._constructGraphIfPossible(chartData);
22     }.observes('chartData').on('init'),
23     didInsertElement: function ()
24     {
25         var chartData = this.get('chartData');
26         if (chartData)
27             this._constructGraphIfPossible(chartData);
28     },
29     willClearRender: function ()
30     {
31         this._eventHandlers.forEach(function (item) {
32             $(item[0]).off(item[1], item[2]);
33         })
34     },
35     _attachEventListener: function(target, eventName, listener)
36     {
37         this._eventHandlers.push([target, eventName, listener]);
38         $(target).on(eventName, listener);
39     },
40     _constructGraphIfPossible: function (chartData)
41     {
42         if (!this._needsConstruction || !this.get('element'))
43             return;
44
45         var element = this.get('element');
46
47         this._x = d3.time.scale();
48         this._y = d3.scale.linear();
49
50         // FIXME: Tear down the old SVG element.
51         this._svgElement = d3.select(element).append("svg")
52                 .attr("width", "100%")
53                 .attr("height", "100%");
54
55         var svg = this._svg = this._svgElement.append("g");
56
57         var clipId = element.id + "-clip";
58         this._clipPath = svg.append("defs").append("clipPath")
59             .attr("id", clipId)
60             .append("rect");
61
62         if (this.get('showXAxis')) {
63             this._xAxis = d3.svg.axis().scale(this._x).orient("bottom").ticks(6);
64             this._xAxisLabels = svg.append("g")
65                 .attr("class", "x axis");
66         }
67
68         if (this.get('showYAxis')) {
69             this._yAxis = d3.svg.axis().scale(this._y).orient("left").ticks(6).tickFormat(chartData.formatter);
70             this._yAxisLabels = svg.append("g")
71                 .attr("class", "y axis");
72         }
73
74         this._clippedContainer = svg.append("g")
75             .attr("clip-path", "url(#" + clipId + ")");
76
77         var xScale = this._x;
78         var yScale = this._y;
79         this._timeLine = d3.svg.line()
80             .x(function(point) { return xScale(point.time); })
81             .y(function(point) { return yScale(point.value); });
82
83         this._confidenceArea = d3.svg.area()
84 //            .interpolate("cardinal")
85             .x(function(point) { return xScale(point.time); })
86             .y0(function(point) { return point.interval ? yScale(point.interval[0]) : null; })
87             .y1(function(point) { return point.interval ? yScale(point.interval[1]) : null; });
88
89         if (this._paths)
90             this._paths.forEach(function (path) { path.remove(); });
91         this._paths = [];
92         if (this._areas)
93             this._areas.forEach(function (area) { area.remove(); });
94         this._areas = [];
95         if (this._dots)
96             this._dots.forEach(function (dot) { dots.remove(); });
97         this._dots = [];
98         if (this._highlights)
99             this._highlights.remove();
100         this._highlights = null;
101
102         this._currentTimeSeries = chartData.current;
103         this._currentTimeSeriesData = this._currentTimeSeries.series();
104         this._baselineTimeSeries = chartData.baseline;
105         this._targetTimeSeries = chartData.target;
106
107         this._yAxisUnit = chartData.unit;
108
109         if (this._baselineTimeSeries) {
110             this._paths.push(this._clippedContainer
111                 .append("path")
112                 .datum(this._baselineTimeSeries.series())
113                 .attr("class", "baseline"));
114         }
115         if (this._targetTimeSeries) {
116             this._paths.push(this._clippedContainer
117                 .append("path")
118                 .datum(this._targetTimeSeries.series())
119                 .attr("class", "target"));
120         }
121
122         this._areas.push(this._clippedContainer
123             .append("path")
124             .datum(this._currentTimeSeriesData)
125             .attr("class", "area"));
126
127         this._paths.push(this._clippedContainer
128             .append("path")
129             .datum(this._currentTimeSeriesData)
130             .attr("class", "current"));
131
132         this._dots.push(this._clippedContainer
133             .selectAll(".dot")
134                 .data(this._currentTimeSeriesData)
135             .enter().append("circle")
136                 .attr("class", "dot")
137                 .attr("r", this.get('chartPointRadius') || 1));
138
139         if (this.get('interactive')) {
140             this._attachEventListener(element, "mousemove", this._mouseMoved.bind(this));
141             this._attachEventListener(element, "mouseleave", this._mouseLeft.bind(this));
142             this._attachEventListener(element, "mousedown", this._mouseDown.bind(this));
143             this._attachEventListener($(element).parents("[tabindex]"), "keydown", this._keyPressed.bind(this));
144
145             this._currentItemLine = this._clippedContainer
146                 .append("line")
147                 .attr("class", "current-item");
148
149             this._currentItemCircle = this._clippedContainer
150                 .append("circle")
151                 .attr("class", "dot current-item")
152                 .attr("r", 3);
153         }
154
155         this._brush = null;
156         if (this.get('enableSelection')) {
157             this._brush = d3.svg.brush()
158                 .x(this._x)
159                 .on("brush", this._brushChanged.bind(this));
160
161             this._brushRect = this._clippedContainer
162                 .append("g")
163                 .attr("class", "x brush");
164         }
165
166         this._updateDomain();
167         this._updateDimensionsIfNeeded();
168
169         // Work around the fact the brush isn't set if we updated it synchronously here.
170         setTimeout(this._selectionChanged.bind(this), 0);
171
172         setTimeout(this._selectedItemChanged.bind(this), 0);
173
174         this._needsConstruction = false;
175
176         this._highlightedItemsChanged();
177         this._rangesChanged();
178     },
179     _updateDomain: function ()
180     {
181         var xDomain = this.get('domain');
182         var intrinsicXDomain = this._computeXAxisDomain(this._currentTimeSeriesData);
183         if (!xDomain)
184             xDomain = intrinsicXDomain;
185         var currentDomain = this._x.domain();
186         if (currentDomain && App.domainsAreEqual(currentDomain, xDomain))
187             return currentDomain;
188
189         var yDomain = this._computeYAxisDomain(xDomain[0], xDomain[1]);
190         this._x.domain(xDomain);
191         this._y.domain(yDomain);
192         return xDomain;
193     },
194     _updateDimensionsIfNeeded: function (newSelection)
195     {
196         var element = $(this.get('element'));
197
198         var newTotalWidth = element.width();
199         var newTotalHeight = element.height();
200         if (this._totalWidth == newTotalWidth && this._totalHeight == newTotalHeight)
201             return;
202
203         this._totalWidth = newTotalWidth;
204         this._totalHeight = newTotalHeight;
205
206         if (!this._rem)
207             this._rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
208         var rem = this._rem;
209
210         var padding = 0.5 * rem;
211         var margin = {top: padding, right: padding, bottom: padding, left: padding};
212         if (this._xAxis)
213             margin.bottom += rem;
214         if (this._yAxis)
215             margin.left += 3 * rem;
216
217         this._margin = margin;
218         this._contentWidth = this._totalWidth - margin.left - margin.right;
219         this._contentHeight = this._totalHeight - margin.top - margin.bottom;
220
221         this._svg.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
222
223         this._clipPath
224             .attr("width", this._contentWidth)
225             .attr("height", this._contentHeight);
226
227         this._x.range([0, this._contentWidth]);
228         this._y.range([this._contentHeight, 0]);
229
230         if (this._xAxis) {
231             this._xAxis.tickSize(-this._contentHeight);
232             this._xAxisLabels.attr("transform", "translate(0," + this._contentHeight + ")");
233         }
234
235         if (this._yAxis)
236             this._yAxis.tickSize(-this._contentWidth);
237
238         if (this._currentItemLine) {
239             this._currentItemLine
240                 .attr("y1", 0)
241                 .attr("y2", margin.top + this._contentHeight);
242         }
243
244         this._relayoutDataAndAxes(this._currentSelection());
245     },
246     _updateBrush: function ()
247     {
248         this._brushRect
249             .call(this._brush)
250         .selectAll("rect")
251             .attr("y", 1)
252             .attr("height", this._contentHeight - 2);
253         this._updateSelectionToolbar();
254     },
255     _relayoutDataAndAxes: function (selection)
256     {
257         var timeline = this._timeLine;
258         this._paths.forEach(function (path) { path.attr("d", timeline); });
259
260         var confidenceArea = this._confidenceArea;
261         this._areas.forEach(function (path) { path.attr("d", confidenceArea); });
262
263         var xScale = this._x;
264         var yScale = this._y;
265         this._dots.forEach(function (dot) {
266             dot
267                 .attr("cx", function(measurement) { return xScale(measurement.time); })
268                 .attr("cy", function(measurement) { return yScale(measurement.value); });
269         });
270         this._updateHighlightPositions();
271         this._updateRangeBarRects();
272
273         if (this._brush) {
274             if (selection)
275                 this._brush.extent(selection);
276             else
277                 this._brush.clear();
278             this._updateBrush();
279         }
280
281         this._updateCurrentItemIndicators();
282
283         if (this._xAxis)
284             this._xAxisLabels.call(this._xAxis);
285         if (!this._yAxis)
286             return;
287
288         this._yAxisLabels.call(this._yAxis);
289         if (this._yAxisUnitContainer)
290             this._yAxisUnitContainer.remove();
291         var x = - 3 * this._rem;
292         var y = this._contentHeight / 2;
293         this._yAxisUnitContainer = this._yAxisLabels.append("text")
294             .attr("transform", "rotate(90 0 0) translate(" + y + ", " + (-x) + ")")
295             .style("text-anchor", "middle")
296             .text(this._yAxisUnit);
297     },
298     _updateHighlightPositions: function () {
299         if (!this._highlights)
300             return;
301
302         var xScale = this._x;
303         var yScale = this._y;
304         this._highlights
305             .attr("cy", function(point) { return yScale(point.value); })
306             .attr("cx", function(point) { return xScale(point.time); });
307     },
308     _computeXAxisDomain: function (timeSeries)
309     {
310         var extent = d3.extent(timeSeries, function(point) { return point.time; });
311         var margin = 3600 * 1000; // Use x.inverse to compute the right amount from a margin.
312         return [+extent[0] - margin, +extent[1] + margin];
313     },
314     _computeYAxisDomain: function (startTime, endTime)
315     {
316         var range = this._minMaxForAllTimeSeries(startTime, endTime);
317         var min = range[0];
318         var max = range[1];
319         if (max < min)
320             min = max = 0;
321         var diff = max - min;
322         var margin = diff * 0.05;
323
324         yExtent = [min - margin, max + margin];
325 //        if (yMin !== undefined)
326 //            yExtent[0] = parseInt(yMin);
327         return yExtent;
328     },
329     _minMaxForAllTimeSeries: function (startTime, endTime)
330     {
331         var currentRange = this._currentTimeSeries.minMaxForTimeRange(startTime, endTime);
332         var baselineRange = this._baselineTimeSeries ? this._baselineTimeSeries.minMaxForTimeRange(startTime, endTime) : [Number.MAX_VALUE, Number.MIN_VALUE];
333         var targetRange = this._targetTimeSeries ? this._targetTimeSeries.minMaxForTimeRange(startTime, endTime) : [Number.MAX_VALUE, Number.MIN_VALUE];
334         return [
335             Math.min(currentRange[0], baselineRange[0], targetRange[0]),
336             Math.max(currentRange[1], baselineRange[1], targetRange[1]),
337         ];
338     },
339     _currentSelection: function ()
340     {
341         return this._brush && !this._brush.empty() ? this._brush.extent() : null;
342     },
343     _domainChanged: function ()
344     {
345         var selection = this._currentSelection() || this.get('sharedSelection');
346         var newXDomain = this._updateDomain();
347
348         if (selection && newXDomain && selection[0] <= newXDomain[0] && newXDomain[1] <= selection[1])
349             selection = null; // Otherwise the user has no way of clearing the selection.
350
351         this._relayoutDataAndAxes(selection);
352     }.observes('domain'),
353     _selectionChanged: function ()
354     {
355         this._updateSelection(this.get('selection'));
356     }.observes('selection'),
357     _updateSelection: function (newSelection)
358     {
359         if (!this._brush)
360             return;
361
362         var currentSelection = this._currentSelection();
363         if (newSelection && currentSelection && App.domainsAreEqual(newSelection, currentSelection))
364             return;
365
366         var domain = this._x.domain();
367         if (!newSelection || App.domainsAreEqual(domain, newSelection))
368             this._brush.clear();
369         else
370             this._brush.extent(newSelection);
371         this._updateBrush();
372
373         this._setCurrentSelection(newSelection);
374     },
375     _brushChanged: function ()
376     {
377         if (this._brush.empty()) {
378             if (!this._brushExtent)
379                 return;
380
381             this.set('selectionIsLocked', false);
382             this._setCurrentSelection(undefined);
383
384             // Avoid locking the indicator in _mouseDown when the brush was cleared in the same mousedown event.
385             this._brushJustChanged = true;
386             var self = this;
387             setTimeout(function () {
388                 self._brushJustChanged = false;
389             }, 0);
390
391             return;
392         }
393
394         this.set('selectionIsLocked', true);
395         this._setCurrentSelection(this._brush.extent());
396     },
397     _keyPressed: function (event)
398     {
399         if (!this._currentItemIndex || this._currentSelection())
400             return;
401
402         var newIndex;
403         switch (event.keyCode) {
404         case 37: // Left
405             newIndex = this._currentItemIndex - 1;
406             break;
407         case 39: // Right
408             newIndex = this._currentItemIndex + 1;
409             break;
410         case 38: // Up
411         case 40: // Down
412         default:
413             return;
414         }
415
416         // Unlike mousemove, keydown shouldn't move off the edge.
417         if (this._currentTimeSeriesData[newIndex])
418             this._setCurrentItem(newIndex);
419     },
420     _mouseMoved: function (event)
421     {
422         if (!this._margin || this._currentSelection() || this._currentItemLocked)
423             return;
424
425         var point = this._mousePointInGraph(event);
426
427         this._selectClosestPointToMouseAsCurrentItem(point);
428     },
429     _mouseLeft: function (event)
430     {
431         if (!this._margin || this._currentItemLocked)
432             return;
433
434         this._selectClosestPointToMouseAsCurrentItem(null);
435     },
436     _mouseDown: function (event)
437     {
438         if (!this._margin || this._currentSelection() || this._brushJustChanged)
439             return;
440
441         var point = this._mousePointInGraph(event);
442         if (!point)
443             return;
444
445         if (this._currentItemLocked) {
446             this._currentItemLocked = false;
447             this.set('selectedItem', null);
448             return;
449         }
450
451         this._currentItemLocked = true;
452         this._selectClosestPointToMouseAsCurrentItem(point);
453     },
454     _mousePointInGraph: function (event)
455     {
456         var offset = $(this.get('element')).offset();
457         if (!offset || !$(event.target).closest('svg').length)
458             return null;
459
460         var point = {
461             x: event.pageX - offset.left - this._margin.left,
462             y: event.pageY - offset.top - this._margin.top
463         };
464
465         var xScale = this._x;
466         var yScale = this._y;
467         var xDomain = xScale.domain();
468         var yDomain = yScale.domain();
469         if (point.x >= xScale(xDomain[0]) && point.x <= xScale(xDomain[1])
470             && point.y <= yScale(yDomain[0]) && point.y >= yScale(yDomain[1]))
471             return point;
472
473         return null;
474     },
475     _selectClosestPointToMouseAsCurrentItem: function (point)
476     {
477         var xScale = this._x;
478         var yScale = this._y;
479         var distanceHeuristics = function (m) {
480             var mX = xScale(m.time);
481             var mY = yScale(m.value);
482             var xDiff = mX - point.x;
483             var yDiff = mY - point.y;
484             return xDiff * xDiff + yDiff * yDiff / 16; // Favor horizontal affinity.
485         };
486         distanceHeuristics = function (m) {
487             return Math.abs(xScale(m.time) - point.x);
488         }
489
490         var newItemIndex;
491         if (point && !this._currentSelection()) {
492             var distances = this._currentTimeSeriesData.map(distanceHeuristics);
493             var minDistance = Number.MAX_VALUE;
494             for (var i = 0; i < distances.length; i++) {
495                 if (distances[i] < minDistance) {
496                     newItemIndex = i;
497                     minDistance = distances[i];
498                 }
499             }
500         }
501
502         this._setCurrentItem(newItemIndex);
503         this._updateSelectionToolbar();
504     },
505     _currentTimeChanged: function ()
506     {
507         if (!this._margin || this._currentSelection() || this._currentItemLocked)
508             return
509
510         var currentTime = this.get('currentTime');
511         if (currentTime) {
512             for (var i = 0; i < this._currentTimeSeriesData.length; i++) {
513                 var point = this._currentTimeSeriesData[i];
514                 if (point.time >= currentTime) {
515                     this._setCurrentItem(i, /* doNotNotify */ true);
516                     return;
517                 }
518             }
519         }
520         this._setCurrentItem(undefined, /* doNotNotify */ true);
521     }.observes('currentTime'),
522     _setCurrentItem: function (newItemIndex, doNotNotify)
523     {
524         if (newItemIndex === this._currentItemIndex) {
525             if (this._currentItemLocked)
526                 this.set('selectedItem', this.get('currentItem') ? this.get('currentItem').measurement.id() : null);
527             return;
528         }
529
530         var newItem = this._currentTimeSeriesData[newItemIndex];
531         this._brushExtent = undefined;
532         this._currentItemIndex = newItemIndex;
533
534         if (!newItem) {
535             this._currentItemLocked = false;
536             this.set('selectedItem', null);
537         }
538
539         this._updateCurrentItemIndicators();
540
541         if (!doNotNotify)
542             this.set('currentTime', newItem ? newItem.time : undefined);
543
544         this.set('currentItem', newItem);
545         if (this._currentItemLocked)
546             this.set('selectedItem', newItem ? newItem.measurement.id() : null);
547     },
548     _selectedItemChanged: function ()
549     {
550         if (!this._margin)
551             return;
552
553         var selectedId = this.get('selectedItem');
554         var currentItem = this.get('currentItem');
555         if (currentItem && currentItem.measurement.id() == selectedId)
556             return;
557
558         var series = this._currentTimeSeriesData;
559         var selectedItemIndex = undefined;
560         for (var i = 0; i < series.length; i++) {
561             if (series[i].measurement.id() == selectedId) {
562                 this._updateSelection(null);
563                 this._currentItemLocked = true;
564                 this._setCurrentItem(i);
565                 this._updateSelectionToolbar();
566                 return;
567             }
568         }
569     }.observes('selectedItem').on('init'),
570     _highlightedItemsChanged: function () {
571         var highlightedItems = this.get('highlightedItems');
572
573         if (!this._clippedContainer || !highlightedItems)
574             return;
575
576         var data = this._currentTimeSeriesData.filter(function (item) { return highlightedItems[item.measurement.id()]; });
577
578         if (this._highlights)
579             this._highlights.remove();
580         this._highlights = this._clippedContainer
581             .selectAll(".highlight")
582                 .data(data)
583             .enter().append("circle")
584                 .attr("class", "highlight")
585                 .attr("r", (this.get('chartPointRadius') || 1) * 1.8);
586
587         this._updateHighlightPositions();
588     }.observes('highlightedItems'),
589     _rangesChanged: function ()
590     {
591         if (!this._currentTimeSeries)
592             return;
593
594         function midPoint(firstPoint, secondPoint) {
595             if (firstPoint && secondPoint)
596                 return (+firstPoint.time + +secondPoint.time) / 2;
597             if (firstPoint)
598                 return firstPoint.time;
599             return secondPoint.time;
600         }
601         var currentTimeSeries = this._currentTimeSeries;
602         var linkRoute = this.get('rangeRoute');
603         this.set('rangeBars', (this.get('ranges') || []).map(function (range) {
604             var start = currentTimeSeries.findPointByMeasurementId(range.get('startRun'));
605             var end = currentTimeSeries.findPointByMeasurementId(range.get('endRun'));
606             return Ember.Object.create({
607                 startTime: midPoint(currentTimeSeries.previousPoint(start), start),
608                 endTime: midPoint(end, currentTimeSeries.nextPoint(end)),
609                 range: range,
610                 left: null,
611                 right: null,
612                 rowIndex: null,
613                 top: null,
614                 bottom: null,
615                 linkRoute: linkRoute,
616                 linkId: range.get('id'),
617                 label: range.get('label'),
618             });
619         }));
620
621         this._updateRangeBarRects();
622     }.observes('ranges'),
623     _updateRangeBarRects: function () {
624         var rangeBars = this.get('rangeBars');
625         if (!rangeBars || !rangeBars.length)
626             return;
627
628         var xScale = this._x;
629         var yScale = this._y;
630
631         // Expand the width of each range as needed and sort ranges by the left-edge of ranges.
632         var minWidth = 3;
633         var sortedBars = rangeBars.map(function (bar) {
634             var left = xScale(bar.get('startTime'));
635             var right = xScale(bar.get('endTime'));
636             if (right - left < minWidth) {
637                 left -= minWidth / 2;
638                 right += minWidth / 2;
639             }
640             bar.set('left', left);
641             bar.set('right', right);
642             return bar;
643         }).sort(function (first, second) { return first.get('left') - second.get('left'); });
644
645         // At this point, left edges of all ranges prior to a range R1 is on the left of R1.
646         // Place R1 into a row in which right edges of all ranges prior to R1 is on the left of R1 to avoid overlapping ranges.
647         var rows = [];
648         sortedBars.forEach(function (bar) {
649             var rowIndex = 0;
650             for (; rowIndex < rows.length; rowIndex++) {
651                 var currentRow = rows[rowIndex];
652                 if (currentRow[currentRow.length - 1].get('right') < bar.get('left')) {
653                     currentRow.push(bar);
654                     break;
655                 }
656             }
657             if (rowIndex >= rows.length)
658                 rows.push([bar]);
659             bar.set('rowIndex', rowIndex);
660         });
661         var rowHeight = 0.6 * this._rem;
662         var firstRowTop = this._contentHeight - rows.length * rowHeight;
663         var barHeight = 0.5 * this._rem;
664
665         $(this.get('element')).find('.rangeBarsContainerInlineStyle').css({
666             left: this._margin.left + 'px',
667             top: this._margin.top + firstRowTop + 'px',
668             width: this._contentWidth + 'px',
669             height: rows.length * barHeight + 'px',
670             overflow: 'hidden',
671             position: 'absolute',
672         });
673
674         var margin = this._margin;
675         sortedBars.forEach(function (bar) {
676             var top = bar.get('rowIndex') * rowHeight;
677             var height = barHeight;
678             var left = bar.get('left');
679             var width = bar.get('right') - left;
680             bar.set('inlineStyle', 'left: ' + left + 'px; top: ' + top + 'px; width: ' + width + 'px; height: ' + height + 'px;');
681         });
682     },
683     _updateCurrentItemIndicators: function ()
684     {
685         if (!this._currentItemLine)
686             return;
687
688         var item = this._currentTimeSeriesData[this._currentItemIndex];
689         if (!item) {
690             this._currentItemLine.attr("x1", -1000).attr("x2", -1000);
691             this._currentItemCircle.attr("cx", -1000);
692             return;
693         }
694
695         var x = this._x(item.time);
696         var y = this._y(item.value);
697
698         this._currentItemLine
699             .attr("x1", x)
700             .attr("x2", x);
701
702         this._currentItemCircle
703             .attr("cx", x)
704             .attr("cy", y);
705     },
706     _setCurrentSelection: function (newSelection)
707     {
708         if (this._brushExtent === newSelection)
709             return;
710
711         var points = null;
712         if (newSelection) {
713             points = this._currentTimeSeriesData
714                 .filter(function (point) { return point.time >= newSelection[0] && point.time <= newSelection[1]; });
715             if (!points.length)
716                 points = null;
717         }
718
719         this._brushExtent = newSelection;
720         this._setCurrentItem(undefined);
721         this._updateSelectionToolbar();
722
723         if (!App.domainsAreEqual(this.get('selection'), newSelection))
724             this.set('selection', newSelection);
725         this.set('selectedPoints', points);
726     },
727     _updateSelectionToolbar: function ()
728     {
729         if (!this.get('interactive'))
730             return;
731
732         var selection = this._currentSelection();
733         var selectionToolbar = $(this.get('element')).children('.selection-toolbar');
734         if (selection) {
735             var left = this._x(selection[0]);
736             var right = this._x(selection[1]);
737             selectionToolbar
738                 .css({left: this._margin.left + right, top: this._margin.top + this._contentHeight})
739                 .show();
740         } else
741             selectionToolbar.hide();
742     },
743     actions: {
744         zoom: function ()
745         {
746             this.sendAction('zoom', this._currentSelection());
747             this.set('selection', null);
748         },
749         openRange: function (range)
750         {
751             this.sendAction('openRange', range);
752         },
753     },
754 });