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