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