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