• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!DOCTYPE html>
2<!--
3Copyright (c) 2015 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/tracing/base/range.html">
9<link rel="import" href="/tracing/ui/base/event_presenter.html">
10<link rel="import" href="/tracing/model/proxy_selectable_item.html">
11<link rel="import" href="/tracing/model/selection_state.html">
12
13<script>
14'use strict';
15
16tr.exportTo('tr.ui.tracks', function() {
17  var EventPresenter = tr.ui.b.EventPresenter;
18  var SelectionState = tr.model.SelectionState;
19
20  /**
21   * The type of a chart series.
22   * @enum
23   */
24  var ChartSeriesType = {
25    LINE: 0,
26    AREA: 1
27  };
28
29  // The default rendering configuration for ChartSeries.
30  var DEFAULT_RENDERING_CONFIG = {
31    // The type of the chart series.
32    chartType: ChartSeriesType.LINE,
33
34    // The size of a selected point dot in device-independent pixels (circle
35    // diameter).
36    selectedPointSize: 4,
37
38    // The size of an unselected point dot in device-independent pixels (square
39    // width/height).
40    unselectedPointSize: 3,
41
42    // The color of the chart.
43    colorId: 0,
44
45    // The width of the top line in device-independent pixels.
46    lineWidth: 1,
47
48    // Minimum distance between points in physical pixels. Points which are
49    // closer than this distance will be skipped.
50    skipDistance: 1,
51
52    // Density in points per physical pixel at which unselected point dots
53    // become transparent.
54    unselectedPointDensityTransparent: 0.10,
55
56    // Density in points per physical pixel at which unselected point dots
57    // become fully opaque.
58    unselectedPointDensityOpaque: 0.05,
59
60    // Opacity of area chart background.
61    backgroundOpacity: 0.5
62  };
63
64  // The virtual width of the last point in a series (whose rectangle has zero
65  // width) in world timestamps difference for the purposes of selection.
66  var LAST_POINT_WIDTH = 16;
67
68  /**
69   * Visual components of a ChartSeries.
70   * @enum
71   */
72  var ChartSeriesComponent = {
73    BACKGROUND: 0,
74    LINE: 1,
75    DOTS: 2
76  };
77
78  /**
79   * A series of points corresponding to a single chart on a chart track.
80   * This class is responsible for drawing the actual chart onto canvas.
81   *
82   * @constructor
83   */
84  function ChartSeries(points, axis, opt_renderingConfig) {
85    this.points = points;
86    this.axis = axis;
87
88    this.useRenderingConfig_(opt_renderingConfig);
89  }
90
91  ChartSeries.prototype = {
92    useRenderingConfig_: function(opt_renderingConfig) {
93      var config = opt_renderingConfig || {};
94
95      // Store all configuration flags as private properties.
96      tr.b.iterItems(DEFAULT_RENDERING_CONFIG, function(key, defaultValue) {
97        var value = config[key];
98        if (value === undefined)
99          value = defaultValue;
100        this[key + '_'] = value;
101      }, this);
102
103      // Avoid unnecessary recomputation in getters.
104      this.topPadding = this.bottomPadding = Math.max(
105          this.selectedPointSize_, this.unselectedPointSize_) / 2;
106    },
107
108    get range() {
109      var range = new tr.b.Range();
110      this.points.forEach(function(point) {
111        range.addValue(point.y);
112      }, this);
113      return range;
114    },
115
116    draw: function(ctx, transform, highDetails) {
117      if (this.points === undefined || this.points.length === 0)
118        return;
119
120      // Draw the background.
121      if (this.chartType_ === ChartSeriesType.AREA) {
122        this.drawComponent_(ctx, transform, ChartSeriesComponent.BACKGROUND,
123            highDetails);
124      }
125
126      // Draw the line at the top.
127      if (this.chartType_ === ChartSeriesType.LINE || highDetails) {
128        this.drawComponent_(ctx, transform, ChartSeriesComponent.LINE,
129            highDetails);
130      }
131
132      // Draw the points.
133      this.drawComponent_(ctx, transform, ChartSeriesComponent.DOTS,
134          highDetails);
135    },
136
137    drawComponent_: function(ctx, transform, component, highDetails) {
138      // We need to consider extra pixels outside the visible area to avoid
139      // visual glitches due to non-zero width of dots.
140      var extraPixels = 0;
141      if (component === ChartSeriesComponent.DOTS) {
142        extraPixels = Math.max(
143            this.selectedPointSize_, this.unselectedPointSize_);
144      }
145      var leftViewX = transform.leftViewX - extraPixels * transform.pixelRatio;
146      var rightViewX = transform.rightViewX +
147          extraPixels * transform.pixelRatio;
148      var leftTimestamp = transform.leftTimestamp - extraPixels;
149      var rightTimestamp = transform.rightTimestamp + extraPixels;
150
151      // Find the index of the first and last (partially) visible points.
152      var firstVisibleIndex = tr.b.findLowIndexInSortedArray(
153          this.points,
154          function(point) { return point.x; },
155          leftTimestamp);
156      var lastVisibleIndex = tr.b.findLowIndexInSortedArray(
157          this.points,
158          function(point) { return point.x; },
159          rightTimestamp);
160      if (lastVisibleIndex >= this.points.length ||
161          this.points[lastVisibleIndex].x > rightTimestamp) {
162        lastVisibleIndex--;
163      }
164
165      // Pre-calculate component style which does not depend on individual
166      // points:
167      //   * Skip distance between points,
168      //   * Selected (circle) and unselected (square) dot size,
169      //   * Unselected dot opacity,
170      //   * Selected dot edge color and width, and
171      //   * Line component color and width.
172      var viewSkipDistance = this.skipDistance_ * transform.pixelRatio;
173      var circleRadius;
174      var squareSize;
175      var squareHalfSize;
176      var squareOpacity;
177
178      switch (component) {
179        case ChartSeriesComponent.DOTS:
180          // Selected dot edge color and width.
181          ctx.strokeStyle = EventPresenter.getCounterSeriesColor(
182              this.colorId_, SelectionState.NONE);
183          ctx.lineWidth = transform.pixelRatio;
184
185          // Selected (circle) and unselected (square) dot size.
186          circleRadius = (this.selectedPointSize_ / 2) * transform.pixelRatio;
187          squareSize = this.unselectedPointSize_ * transform.pixelRatio;
188          squareHalfSize = squareSize / 2;
189
190          // Unselected dot opacity.
191          if (!highDetails) {
192            // Unselected dots are not displayed in 'low details' mode.
193            squareOpacity = 0;
194            break;
195          }
196          var visibleIndexRange = lastVisibleIndex - firstVisibleIndex;
197          if (visibleIndexRange <= 0) {
198            // There is at most one visible point.
199            squareOpacity = 1;
200            break;
201          }
202          var visibleViewXRange =
203              transform.worldXToViewX(this.points[lastVisibleIndex].x) -
204              transform.worldXToViewX(this.points[firstVisibleIndex].x);
205          if (visibleViewXRange === 0) {
206            // Multiple visible points which all have the same timestamp.
207            squareOpacity = 1;
208            break;
209          }
210          var density = visibleIndexRange / visibleViewXRange;
211          var clampedDensity = tr.b.clamp(density,
212              this.unselectedPointDensityOpaque_,
213              this.unselectedPointDensityTransparent_);
214          var densityRange = this.unselectedPointDensityTransparent_ -
215              this.unselectedPointDensityOpaque_;
216          squareOpacity =
217              (this.unselectedPointDensityTransparent_ - clampedDensity) /
218              densityRange;
219          break;
220
221        case ChartSeriesComponent.LINE:
222          // Line component color and width.
223          ctx.strokeStyle = EventPresenter.getCounterSeriesColor(
224              this.colorId_, SelectionState.NONE);
225          ctx.lineWidth = this.lineWidth_ * transform.pixelRatio;
226          break;
227
228        case ChartSeriesComponent.BACKGROUND:
229          // Style depends on the selection state of individual points.
230          break;
231
232        default:
233          throw new Error('Invalid component: ' + component);
234      }
235
236      // The main loop which draws the given component of visible points from
237      // left to right. Given the potentially large number of points to draw,
238      // it should be considered performance-critical and function calls should
239      // be avoided when possible.
240      //
241      // Note that the background and line components are drawn in a delayed
242      // fashion: the rectangle/line that we draw in an iteration corresponds
243      // to the *previous* point. This does not apply to the dots, whose
244      // position is independent of the surrounding dots.
245      var previousViewX = undefined;
246      var previousViewY = undefined;
247      var previousViewYBase = undefined;
248      var lastSelectionState = undefined;
249      var baseSteps = undefined;
250      var startIndex = Math.max(firstVisibleIndex - 1, 0);
251
252      for (var i = startIndex; i < this.points.length; i++) {
253        var currentPoint = this.points[i];
254        var currentViewX = transform.worldXToViewX(currentPoint.x);
255
256        // Stop drawing the points once we are to the right of the visible area.
257        if (currentViewX > rightViewX) {
258          if (previousViewX !== undefined) {
259            previousViewX = currentViewX = rightViewX;
260            if (component === ChartSeriesComponent.BACKGROUND ||
261                component === ChartSeriesComponent.LINE) {
262              ctx.lineTo(currentViewX, previousViewY);
263            }
264          }
265          break;
266        }
267
268        if (i + 1 < this.points.length) {
269          var nextPoint = this.points[i + 1];
270          var nextViewX = transform.worldXToViewX(nextPoint.x);
271
272          // Skip points that are too close to each other.
273          if (previousViewX !== undefined &&
274              nextViewX - previousViewX <= viewSkipDistance &&
275              nextViewX < rightViewX) {
276            continue;
277          }
278
279          // Start drawing right at the left side of the visible are (instead
280          // of potentially very far to the left).
281          if (currentViewX < leftViewX) {
282            currentViewX = leftViewX;
283          }
284        }
285
286        if (previousViewX !== undefined &&
287            currentViewX - previousViewX < viewSkipDistance) {
288          // We know that nextViewX > previousViewX + viewSkipDistance, so we
289          // can safely move this points's x over that much without passing
290          // nextViewX. This ensures that the previous point is visible when
291          // zoomed out very far.
292          currentViewX = previousViewX + viewSkipDistance;
293        }
294
295        var currentViewY = Math.round(transform.worldYToViewY(currentPoint.y));
296        var currentViewYBase;
297        if (currentPoint.yBase === undefined) {
298          currentViewYBase = transform.outerBottomViewY;
299        } else {
300          currentViewYBase = Math.round(
301              transform.worldYToViewY(currentPoint.yBase));
302        }
303        var currentSelectionState = currentPoint.selectionState;
304
305        // Actually draw the given component of the point.
306        switch (component) {
307          case ChartSeriesComponent.DOTS:
308            // Change dot style when the selection state changes (and at the
309            // beginning).
310            if (currentSelectionState !== lastSelectionState) {
311              if (currentSelectionState === SelectionState.SELECTED) {
312                ctx.fillStyle = EventPresenter.getCounterSeriesColor(
313                    this.colorId_, currentSelectionState);
314              } else if (squareOpacity > 0) {
315                ctx.fillStyle = EventPresenter.getCounterSeriesColor(
316                    this.colorId_, currentSelectionState, squareOpacity);
317              }
318            }
319
320            // Draw the dot for the current point.
321            if (currentSelectionState === SelectionState.SELECTED) {
322              ctx.beginPath();
323              ctx.arc(currentViewX, currentViewY, circleRadius, 0, 2 * Math.PI);
324              ctx.fill();
325              ctx.stroke();
326            } else if (squareOpacity > 0) {
327              ctx.fillRect(currentViewX - squareHalfSize,
328                  currentViewY - squareHalfSize, squareSize, squareSize);
329            }
330            break;
331
332          case ChartSeriesComponent.LINE:
333            // Draw the top line for the previous point (if applicable), or
334            // prepare for drawing the top line of the current point in the next
335            // iteration.
336            if (previousViewX === undefined) {
337              ctx.beginPath();
338              ctx.moveTo(currentViewX, currentViewY);
339            } else {
340              ctx.lineTo(currentViewX, previousViewY);
341            }
342
343            // Move to the current point coordinate.
344            ctx.lineTo(currentViewX, currentViewY);
345            break;
346
347          case ChartSeriesComponent.BACKGROUND:
348            // Draw the background for the previous point (if applicable).
349            if (previousViewX !== undefined)
350              ctx.lineTo(currentViewX, previousViewY);
351
352            // Finish the bottom part of the backgound polygon, change
353            // background color and start a new polygon when the selection state
354            // changes (and at the beginning).
355            if (currentSelectionState !== lastSelectionState) {
356              if (previousViewX !== undefined) {
357                var previousBaseStepViewX = currentViewX;
358                for (var j = baseSteps.length - 1; j >= 0; j--) {
359                  var baseStep = baseSteps[j];
360                  var baseStepViewX = baseStep.viewX;
361                  var baseStepViewY = baseStep.viewY;
362                  ctx.lineTo(previousBaseStepViewX, baseStepViewY);
363                  ctx.lineTo(baseStepViewX, baseStepViewY);
364                  previousBaseStepViewX = baseStepViewX;
365                }
366                ctx.closePath();
367                ctx.fill();
368              }
369              ctx.beginPath();
370              ctx.fillStyle = EventPresenter.getCounterSeriesColor(
371                  this.colorId_, currentSelectionState,
372                  this.backgroundOpacity_);
373              ctx.moveTo(currentViewX, currentViewYBase);
374              baseSteps = [];
375            }
376
377            if (currentViewYBase !== previousViewYBase ||
378                currentSelectionState !== lastSelectionState) {
379              baseSteps.push({viewX: currentViewX, viewY: currentViewYBase});
380            }
381
382            // Move to the current point coordinate.
383            ctx.lineTo(currentViewX, currentViewY);
384            break;
385
386          default:
387            throw new Error('Not reachable');
388        }
389
390        previousViewX = currentViewX;
391        previousViewY = currentViewY;
392        previousViewYBase = currentViewYBase;
393        lastSelectionState = currentSelectionState;
394      }
395
396      // If we still have an open background or top line polygon (which is
397      // always the case once we have started drawing due to the delayed fashion
398      // of drawing), we must close it.
399      if (previousViewX !== undefined) {
400        switch (component) {
401          case ChartSeriesComponent.DOTS:
402            // All dots were drawn in the main loop.
403            break;
404
405          case ChartSeriesComponent.LINE:
406            ctx.stroke();
407            break;
408
409          case ChartSeriesComponent.BACKGROUND:
410            var previousBaseStepViewX = currentViewX;
411            for (var j = baseSteps.length - 1; j >= 0; j--) {
412              var baseStep = baseSteps[j];
413              var baseStepViewX = baseStep.viewX;
414              var baseStepViewY = baseStep.viewY;
415              ctx.lineTo(previousBaseStepViewX, baseStepViewY);
416              ctx.lineTo(baseStepViewX, baseStepViewY);
417              previousBaseStepViewX = baseStepViewX;
418            }
419            ctx.closePath();
420            ctx.fill();
421            break;
422
423          default:
424            throw new Error('Not reachable');
425        }
426      }
427    },
428
429    addIntersectingEventsInRangeToSelectionInWorldSpace: function(
430        loWX, hiWX, viewPixWidthWorld, selection) {
431      var points = this.points;
432
433      function getPointWidth(point, i) {
434        if (i === points.length - 1)
435          return LAST_POINT_WIDTH * viewPixWidthWorld;
436        var nextPoint = points[i + 1];
437        return nextPoint.x - point.x;
438      }
439
440      function selectPoint(point) {
441        point.addToSelection(selection);
442      }
443
444      tr.b.iterateOverIntersectingIntervals(
445          this.points,
446          function(point) { return point.x },
447          getPointWidth,
448          loWX,
449          hiWX,
450          selectPoint);
451    },
452
453    addEventNearToProvidedEventToSelection: function(event, offset, selection) {
454      if (this.points === undefined)
455        return false;
456
457      var index = tr.b.findFirstIndexInArray(this.points, function(point) {
458        return point.modelItem === event;
459      }, this);
460      if (index === -1)
461        return false;
462
463      var newIndex = index + offset;
464      if (newIndex < 0 || newIndex >= this.points.length)
465        return false;
466
467      this.points[newIndex].addToSelection(selection);
468      return true;
469    },
470
471    addClosestEventToSelection: function(worldX, worldMaxDist, loY, hiY,
472                                         selection) {
473      if (this.points === undefined)
474        return;
475
476      var item = tr.b.findClosestElementInSortedArray(
477          this.points,
478          function(point) { return point.x },
479          worldX,
480          worldMaxDist);
481
482      if (!item)
483        return;
484
485      item.addToSelection(selection);
486    }
487  };
488
489  return {
490    ChartSeries: ChartSeries,
491    ChartSeriesType: ChartSeriesType
492  };
493});
494</script>
495