15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Copyright (c) 2012 The Chromium Authors. All rights reserved.
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// found in the LICENSE file.
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/**
65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * A TimelineGraphView displays a timeline graph on a canvas element.
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)var TimelineGraphView = (function() {
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  'use strict';
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // We inherit from TopMidBottomView.
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var superClass = TopMidBottomView;
125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Default starting scale factor, in terms of milliseconds per pixel.
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var DEFAULT_SCALE = 1000;
155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Maximum number of labels placed vertically along the sides of the graph.
175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var MAX_VERTICAL_LABELS = 6;
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Vertical spacing between labels and between the graph and labels.
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var LABEL_VERTICAL_SPACING = 4;
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Horizontal spacing between vertically placed labels and the edges of the
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // graph.
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var LABEL_HORIZONTAL_SPACING = 3;
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Horizintal spacing between two horitonally placed labels along the bottom
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // of the graph.
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var LABEL_LABEL_HORIZONTAL_SPACING = 25;
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Length of ticks, in pixels, next to y-axis labels.  The x-axis only has
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // one set of labels, so it can use lines instead.
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var Y_AXIS_TICK_LENGTH = 10;
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // The number of units mouse wheel deltas increase for each tick of the
335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // wheel.
345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var MOUSE_WHEEL_UNITS_PER_CLICK = 120;
355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Amount we zoom for one vertical tick of the mouse wheel, as a ratio.
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var MOUSE_WHEEL_ZOOM_RATE = 1.25;
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Amount we scroll for one horizontal tick of the mouse wheel, in pixels.
395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var MOUSE_WHEEL_SCROLL_RATE = MOUSE_WHEEL_UNITS_PER_CLICK;
405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Number of pixels to scroll per pixel the mouse is dragged.
415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var MOUSE_WHEEL_DRAG_RATE = 3;
425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var GRID_COLOR = '#CCC';
445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var TEXT_COLOR = '#000';
455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var BACKGROUND_COLOR = '#FFF';
465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Which side of the canvas y-axis labels should go on, for a given Graph.
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // TODO(mmenke):  Figure out a reasonable way to handle more than 2 sets
495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  //                of labels.
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var LabelAlign = {
515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    LEFT: 0,
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    RIGHT: 1
535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  };
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  /**
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)   * @constructor
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)   */
585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  function TimelineGraphView(divId, canvasId, scrollbarId, scrollbarInnerId) {
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.scrollbar_ = new HorizontalScrollbarView(scrollbarId,
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                                  scrollbarInnerId,
615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                                  this.onScroll_.bind(this));
625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Call superclass's constructor.
635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    superClass.call(this, null, new DivView(divId), this.scrollbar_);
645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.graphDiv_ = $(divId);
665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.canvas_ = $(canvasId);
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.canvas_.onmousewheel = this.onMouseWheel_.bind(this);
685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.canvas_.onmousedown = this.onMouseDown_.bind(this);
695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.canvas_.onmousemove = this.onMouseMove_.bind(this);
705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.canvas_.onmouseup = this.onMouseUp_.bind(this);
715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.canvas_.onmouseout = this.onMouseUp_.bind(this);
725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Used for click and drag scrolling of graph.  Drag-zooming not supported,
745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // for a more stable scrolling experience.
755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.isDragging_ = false;
765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.dragX_ = 0;
775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Set the range and scale of the graph.  Times are in milliseconds since
795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // the Unix epoch.
805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // All measurements we have must be after this time.
825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.startTime_ = 0;
835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // The current rightmost position of the graph is always at most this.
845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // We may have some later events.  When actively capturing new events, it's
855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // updated on a timer.
865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.endTime_ = 1;
875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Current scale, in terms of milliseconds per pixel.  Each column of
895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // pixels represents a point in time |scale_| milliseconds after the
905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // previous one.  We only display times that are of the form
915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // |startTime_| + K * |scale_| to avoid jittering, and the rightmost
925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // pixel that we can display has a time <= |endTime_|.  Non-integer values
935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // are allowed.
945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.scale_ = DEFAULT_SCALE;
955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.graphs_ = [];
975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Initialize the scrollbar.
995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    this.updateScrollbarRange_(true);
1005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
1015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Smallest allowed scaling factor.
1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  TimelineGraphView.MIN_SCALE = 5;
1045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  TimelineGraphView.prototype = {
1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Inherit the superclass's methods.
1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    __proto__: superClass.prototype,
1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    setGeometry: function(left, top, width, height) {
1105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      superClass.prototype.setGeometry.call(this, left, top, width, height);
1115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // The size of the canvas can only be set by using its |width| and
1135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // |height| properties, which do not take padding into account, so we
1145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // need to use them ourselves.
1155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var style = getComputedStyle(this.canvas_);
1165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var horizontalPadding = parseInt(style.paddingRight) +
1175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                  parseInt(style.paddingLeft);
1185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var verticalPadding = parseInt(style.paddingTop) +
1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                parseInt(style.paddingBottom);
1205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var canvasWidth =
1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          parseInt(this.graphDiv_.style.width) - horizontalPadding;
1225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // For unknown reasons, there's an extra 3 pixels border between the
1235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // bottom of the canvas and the bottom margin of the enclosing div.
1245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var canvasHeight =
1255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          parseInt(this.graphDiv_.style.height) - verticalPadding - 3;
1265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Protect against degenerates.
1285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (canvasWidth < 10)
1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        canvasWidth = 10;
1305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (canvasHeight < 10)
1315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        canvasHeight = 10;
1325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.canvas_.width = canvasWidth;
1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.canvas_.height = canvasHeight;
1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Use the same font style for the canvas as we use elsewhere.
1375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Has to be updated every resize.
1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.canvas_.getContext('2d').font = getComputedStyle(this.canvas_).font;
1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.updateScrollbarRange_(this.graphScrolledToRightEdge_());
1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.repaint();
1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
1435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    show: function(isVisible) {
1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      superClass.prototype.show.call(this, isVisible);
1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (isVisible)
1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.repaint();
1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Returns the total length of the graph, in pixels.
1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    getLength_: function() {
1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var timeRange = this.endTime_ - this.startTime_;
1535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Math.floor is used to ignore the last partial area, of length less
1545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // than |scale_|.
1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return Math.floor(timeRange / this.scale_);
1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
1575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
1595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Returns true if the graph is scrolled all the way to the right.
1605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
1615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    graphScrolledToRightEdge_: function() {
1625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return this.scrollbar_.getPosition() == this.scrollbar_.getRange();
1635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
1645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
1665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Update the range of the scrollbar.  If |resetPosition| is true, also
1675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * sets the slider to point at the rightmost position and triggers a
1685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * repaint.
1695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
1705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    updateScrollbarRange_: function(resetPosition) {
1715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var scrollbarRange = this.getLength_() - this.canvas_.width;
1725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (scrollbarRange < 0)
1735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        scrollbarRange = 0;
1745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // If we've decreased the range to less than the current scroll position,
1765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // we need to move the scroll position.
1775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (this.scrollbar_.getPosition() > scrollbarRange)
1785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        resetPosition = true;
1795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.scrollbar_.setRange(scrollbarRange);
1815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (resetPosition) {
1825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.scrollbar_.setPosition(scrollbarRange);
1835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.repaint();
1845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
1855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
1865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
1885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Sets the date range displayed on the graph, switches to the default
1895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * scale factor, and moves the scrollbar all the way to the right.
1905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
1915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    setDateRange: function(startDate, endDate) {
1925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.startTime_ = startDate.getTime();
1935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.endTime_ = endDate.getTime();
1945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Safety check.
1965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (this.endTime_ <= this.startTime_)
1975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.startTime_ = this.endTime_ - 1;
1985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.scale_ = DEFAULT_SCALE;
2005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.updateScrollbarRange_(true);
2015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Updates the end time at the right of the graph to be the current time.
2055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Specifically, updates the scrollbar's range, and if the scrollbar is
2065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * all the way to the right, keeps it all the way to the right.  Otherwise,
2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * leaves the view as-is and doesn't redraw anything.
2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
2095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    updateEndDate: function() {
2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.endTime_ = timeutil.getCurrentTime();
2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.updateScrollbarRange_(this.graphScrolledToRightEdge_());
2125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    getStartDate: function() {
2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return new Date(this.startTime_);
2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Scrolls the graph horizontally by the specified amount.
2205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    horizontalScroll_: function(delta) {
2225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var newPosition = this.scrollbar_.getPosition() + Math.round(delta);
2235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Make sure the new position is in the right range.
2245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (newPosition < 0) {
2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        newPosition = 0;
2265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      } else if (newPosition > this.scrollbar_.getRange()) {
2275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        newPosition = this.scrollbar_.getRange();
2285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
2295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (this.scrollbar_.getPosition() == newPosition)
2315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return;
2325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.scrollbar_.setPosition(newPosition);
2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.onScroll_();
2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Zooms the graph by the specified amount.
2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
2395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    zoom_: function(ratio) {
2405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var oldScale = this.scale_;
2415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.scale_ *= ratio;
2425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (this.scale_ < TimelineGraphView.MIN_SCALE)
2435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.scale_ = TimelineGraphView.MIN_SCALE;
2445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (this.scale_ == oldScale)
2465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return;
2475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // If we were at the end of the range before, remain at the end of the
2495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // range.
2505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (this.graphScrolledToRightEdge_()) {
2515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.updateScrollbarRange_(true);
2525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return;
2535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
2545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Otherwise, do our best to maintain the old position.  We use the
2565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // position at the far right of the graph for consistency.
2575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var oldMaxTime =
2585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          oldScale * (this.scrollbar_.getPosition() + this.canvas_.width);
2595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var newMaxTime = Math.round(oldMaxTime / this.scale_);
2605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var newPosition = newMaxTime - this.canvas_.width;
2615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Update range and scroll position.
2635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.updateScrollbarRange_(false);
2645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.horizontalScroll_(newPosition - this.scrollbar_.getPosition());
2655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    onMouseWheel_: function(event) {
2685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      event.preventDefault();
2695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.horizontalScroll_(
2705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          MOUSE_WHEEL_SCROLL_RATE *
2715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)              -event.wheelDeltaX / MOUSE_WHEEL_UNITS_PER_CLICK);
2725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.zoom_(Math.pow(MOUSE_WHEEL_ZOOM_RATE,
2735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                 -event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK));
2745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    onMouseDown_: function(event) {
2775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      event.preventDefault();
2785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.isDragging_ = true;
2795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.dragX_ = event.clientX;
2805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    onMouseMove_: function(event) {
2835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (!this.isDragging_)
2845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return;
2855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      event.preventDefault();
2865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.horizontalScroll_(
2875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          MOUSE_WHEEL_DRAG_RATE * (event.clientX - this.dragX_));
2885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.dragX_ = event.clientX;
2895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    onMouseUp_: function(event) {
2925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.isDragging_ = false;
2935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    onScroll_: function() {
2965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.repaint();
2975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
2985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
3005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Replaces the current TimelineDataSeries with |dataSeries|.
3015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
3025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    setDataSeries: function(dataSeries) {
3035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Simplest just to recreate the Graphs.
3045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.graphs_ = [];
3055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.graphs_[TimelineDataType.BYTES_PER_SECOND] =
3065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          new Graph(TimelineDataType.BYTES_PER_SECOND, LabelAlign.RIGHT);
3075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.graphs_[TimelineDataType.SOURCE_COUNT] =
3085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          new Graph(TimelineDataType.SOURCE_COUNT, LabelAlign.LEFT);
3095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      for (var i = 0; i < dataSeries.length; ++i)
3105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.graphs_[dataSeries[i].getDataType()].addDataSeries(dataSeries[i]);
3115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.repaint();
3135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
3145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
3165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Draws the graph on |canvas_|.
3175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
3185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    repaint: function() {
3195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.repaintTimerRunning_ = false;
3205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (!this.isVisible())
3215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return;
3225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var width = this.canvas_.width;
3245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var height = this.canvas_.height;
3255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var context = this.canvas_.getContext('2d');
3265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Clear the canvas.
3285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.fillStyle = BACKGROUND_COLOR;
3295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.fillRect(0, 0, width, height);
3305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Try to get font height in pixels.  Needed for layout.
3325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var fontHeightString = context.font.match(/([0-9]+)px/)[1];
3335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var fontHeight = parseInt(fontHeightString);
3345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Safety check, to avoid drawing anything too ugly.
3365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (fontHeightString.length == 0 || fontHeight <= 0 ||
3375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          fontHeight * 4 > height || width < 50) {
3385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return;
3395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
3405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Save current transformation matrix so we can restore it later.
3425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.save();
3435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // The center of an HTML canvas pixel is technically at (0.5, 0.5).  This
3455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // makes near straight lines look bad, due to anti-aliasing.  This
3465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // translation reduces the problem a little.
3475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.translate(0.5, 0.5);
3485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Figure out what time values to display.
3505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var position = this.scrollbar_.getPosition();
3515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // If the entire time range is being displayed, align the right edge of
3525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // the graph to the end of the time range.
3535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (this.scrollbar_.getRange() == 0)
3545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        position = this.getLength_() - this.canvas_.width;
3555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var visibleStartTime = this.startTime_ + position * this.scale_;
3565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Make space at the bottom of the graph for the time labels, and then
3585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // draw the labels.
3595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var textHeight = height;
3605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      height -= fontHeight + LABEL_VERTICAL_SPACING;
3615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.drawTimeLabels(context, width, height, textHeight, visibleStartTime);
3625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Draw outline of the main graph area.
3645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.strokeStyle = GRID_COLOR;
3655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.strokeRect(0, 0, width - 1, height - 1);
3665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Layout graphs and have them draw their tick marks.
3685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      for (var i = 0; i < this.graphs_.length; ++i) {
3695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.graphs_[i].layout(width, height, fontHeight, visibleStartTime,
3705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                               this.scale_);
3715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.graphs_[i].drawTicks(context);
3725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
3735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Draw the lines of all graphs, and then draw their labels.
3755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      for (var i = 0; i < this.graphs_.length; ++i)
3765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.graphs_[i].drawLines(context);
3775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      for (var i = 0; i < this.graphs_.length; ++i)
3785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.graphs_[i].drawLabels(context);
3795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Restore original transformation matrix.
3815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.restore();
3825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    },
3835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
3855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Draw time labels below the graph.  Takes in start time as an argument
3865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * since it may not be |startTime_|, when we're displaying the entire
3875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * time range.
3885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
3895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    drawTimeLabels: function(context, width, height, textHeight, startTime) {
3905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Text for a time string to use in determining how far apart
3915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // to place text labels.
3925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var sampleText = (new Date(startTime)).toLocaleTimeString();
3935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // The desired spacing for text labels.
3955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var targetSpacing = context.measureText(sampleText).width +
3965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                              LABEL_LABEL_HORIZONTAL_SPACING;
3975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // The allowed time step values between adjacent labels.  Anything much
3995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // over a couple minutes isn't terribly realistic, given how much memory
4005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // we use, and how slow a lot of the net-internals code is.
4015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var timeStepValues = [
4025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        1000,  // 1 second
4035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        1000 * 5,
4045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        1000 * 30,
4055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        1000 * 60,  // 1 minute
4065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        1000 * 60 * 5,
4075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        1000 * 60 * 30,
4085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        1000 * 60 * 60,  // 1 hour
4095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        1000 * 60 * 60 * 5
4105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      ];
4115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Find smallest time step value that gives us at least |targetSpacing|,
4135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // if any.
4145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var timeStep = null;
4155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      for (var i = 0; i < timeStepValues.length; ++i) {
4165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (timeStepValues[i] / this.scale_ >= targetSpacing) {
4175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          timeStep = timeStepValues[i];
4185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          break;
4195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
4205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
4215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // If no such value, give up.
4235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (!timeStep)
4245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return;
4255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Find the time for the first label.  This time is a perfect multiple of
4275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // timeStep because of how UTC times work.
4285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      var time = Math.ceil(startTime / timeStep) * timeStep;
4295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.textBaseline = 'bottom';
4315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.textAlign = 'center';
4325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.fillStyle = TEXT_COLOR;
4335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      context.strokeStyle = GRID_COLOR;
4345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Draw labels and vertical grid lines.
4365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      while (true) {
4375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var x = Math.round((time - startTime) / this.scale_);
4385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (x >= width)
4395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          break;
4405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var text = (new Date(time)).toLocaleTimeString();
4415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.fillText(text, x, textHeight);
4425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.beginPath();
4435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.lineTo(x, 0);
4445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.lineTo(x, height);
4455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.stroke();
4465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        time += timeStep;
4475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
4485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
4495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  };
4505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  /**
4525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)   * A Graph is responsible for drawing all the TimelineDataSeries that have
4535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)   * the same data type.  Graphs are responsible for scaling the values, laying
4545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)   * out labels, and drawing both labels and lines for its data series.
4555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)   */
4565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var Graph = (function() {
4575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
4585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * |dataType| is the DataType that will be shared by all its DataSeries.
4595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * |labelAlign| is the LabelAlign value indicating whether the labels
4605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * should be aligned to the right of left of the graph.
4615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * @constructor
4625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
4635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    function Graph(dataType, labelAlign) {
4645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.dataType_ = dataType;
4655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.dataSeries_ = [];
4665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.labelAlign_ = labelAlign;
4675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Cached properties of the graph, set in layout.
4695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.width_ = 0;
4705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.height_ = 0;
4715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.fontHeight_ = 0;
4725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.startTime_ = 0;
4735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.scale_ = 0;
4745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // At least the highest value in the displayed range of the graph.
4765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Used for scaling and setting labels.  Set in layoutLabels.
4775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.max_ = 0;
4785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // Cached text of equally spaced labels.  Set in layoutLabels.
4805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.labels_ = [];
4815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
4825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
4845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * A Label is the label at a particular position along the y-axis.
4855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * @constructor
4865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
4875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    function Label(height, text) {
4885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.height = height;
4895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this.text = text;
4905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
4915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    Graph.prototype = {
4935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      addDataSeries: function(dataSeries) {
4945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.dataSeries_.push(dataSeries);
4955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      },
4965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
4975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      /**
4985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * Returns a list of all the values that should be displayed for a given
4995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * data series, using the current graph layout.
5005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       */
5015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      getValues: function(dataSeries) {
5025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (!dataSeries.isVisible())
5035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          return null;
5045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return dataSeries.getValues(this.startTime_, this.scale_, this.width_);
5055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      },
5065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      /**
5085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * Updates the graph's layout.  In particular, both the max value and
5095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * label positions are updated.  Must be called before calling any of the
5105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * drawing functions.
5115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       */
5125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      layout: function(width, height, fontHeight, startTime, scale) {
5135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.width_ = width;
5145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.height_ = height;
5155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.fontHeight_ = fontHeight;
5165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.startTime_ = startTime;
5175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.scale_ = scale;
5185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Find largest value.
5205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var max = 0;
5215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        for (var i = 0; i < this.dataSeries_.length; ++i) {
5225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          var values = this.getValues(this.dataSeries_[i]);
5235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if (!values)
5245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            continue;
5255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          for (var j = 0; j < values.length; ++j) {
5265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (values[j] > max)
5275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)              max = values[j];
5285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          }
5295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
5305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.layoutLabels_(max);
5325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      },
5335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      /**
5355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * Lays out labels and sets |max_|, taking the time units into
5365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * consideration.  |maxValue| is the actual maximum value, and
5375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * |max_| will be set to the value of the largest label, which
5385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * will be at least |maxValue|.
5395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       */
5405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      layoutLabels_: function(maxValue) {
5415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (this.dataType_ != TimelineDataType.BYTES_PER_SECOND) {
5425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          this.layoutLabelsBasic_(maxValue, 0);
5435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          return;
5445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
5455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Special handling for data rates.
5475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Find appropriate units to use.
5495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var units = ['B/s', 'kB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s'];
5505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Units to use for labels.  0 is bytes, 1 is kilobytes, etc.
5515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // We start with kilobytes, and work our way up.
5525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var unit = 1;
5535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Update |maxValue| to be in the right units.
5545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        maxValue = maxValue / 1024;
5555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        while (units[unit + 1] && maxValue >= 999) {
5565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          maxValue /= 1024;
5575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          ++unit;
5585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
5595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Calculate labels.
5615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.layoutLabelsBasic_(maxValue, 1);
5625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Append units to labels.
5645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        for (var i = 0; i < this.labels_.length; ++i)
5655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          this.labels_[i] += ' ' + units[unit];
5665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Convert |max_| back to bytes, so it can be used when scaling values
5685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // for display.
5695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.max_ *= Math.pow(1024, unit);
5705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      },
5715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      /**
5735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * Same as layoutLabels_, but ignores units.  |maxDecimalDigits| is the
5745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * maximum number of decimal digits allowed.  The minimum allowed
5755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * difference between two adjacent labels is 10^-|maxDecimalDigits|.
5765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       */
5775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      layoutLabelsBasic_: function(maxValue, maxDecimalDigits) {
5785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.labels_ = [];
5795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // No labels if |maxValue| is 0.
5805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (maxValue == 0) {
5815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          this.max_ = maxValue;
5825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          return;
5835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
5845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // The maximum number of equally spaced labels allowed.  |fontHeight_|
5865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // is doubled because the top two labels are both drawn in the same
5875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // gap.
5885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING;
5895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // The + 1 is for the top label.
5915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var maxLabels = 1 + this.height_ / minLabelSpacing;
5925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (maxLabels < 2) {
5935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          maxLabels = 2;
5945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        } else if (maxLabels > MAX_VERTICAL_LABELS) {
5955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          maxLabels = MAX_VERTICAL_LABELS;
5965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
5975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
5985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Initial try for step size between conecutive labels.
5995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var stepSize = Math.pow(10, -maxDecimalDigits);
6005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Number of digits to the right of the decimal of |stepSize|.
6015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Used for formating label strings.
6025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var stepSizeDecimalDigits = maxDecimalDigits;
6035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Pick a reasonable step size.
6055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        while (true) {
6065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // If we use a step size of |stepSize| between labels, we'll need:
6075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          //
6085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // Math.ceil(maxValue / stepSize) + 1
6095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          //
6105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // labels.  The + 1 is because we need labels at both at 0 and at
6115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // the top of the graph.
6125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // Check if we can use steps of size |stepSize|.
6145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if (Math.ceil(maxValue / stepSize) + 1 <= maxLabels)
6155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            break;
6165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // Check |stepSize| * 2.
6175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if (Math.ceil(maxValue / (stepSize * 2)) + 1 <= maxLabels) {
6185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            stepSize *= 2;
6195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            break;
6205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          }
6215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // Check |stepSize| * 5.
6225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if (Math.ceil(maxValue / (stepSize * 5)) + 1 <= maxLabels) {
6235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            stepSize *= 5;
6245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            break;
6255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          }
6265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          stepSize *= 10;
6275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if (stepSizeDecimalDigits > 0)
6285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            --stepSizeDecimalDigits;
6295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
6305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Set the max so it's an exact multiple of the chosen step size.
6325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        this.max_ = Math.ceil(maxValue / stepSize) * stepSize;
6335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Create labels.
6355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        for (var label = this.max_; label >= 0; label -= stepSize)
6365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          this.labels_.push(label.toFixed(stepSizeDecimalDigits));
6375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      },
6385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      /**
6405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * Draws tick marks for each of the labels in |labels_|.
6415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       */
6425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      drawTicks: function(context) {
6435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var x1;
6445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var x2;
6455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (this.labelAlign_ == LabelAlign.RIGHT) {
6465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          x1 = this.width_ - 1;
6475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH;
6485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        } else {
6495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          x1 = 0;
6505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          x2 = Y_AXIS_TICK_LENGTH;
6515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
6525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.fillStyle = GRID_COLOR;
6545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.beginPath();
6555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        for (var i = 1; i < this.labels_.length - 1; ++i) {
6565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
6575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // lines.
6585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          var y = Math.round(this.height_ * i / (this.labels_.length - 1));
6595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          context.moveTo(x1, y);
6605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          context.lineTo(x2, y);
6615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
6625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.stroke();
6635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      },
6645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      /**
6665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * Draws a graph line for each of the data series.
6675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       */
6685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      drawLines: function(context) {
6695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Factor by which to scale all values to convert them to a number from
6705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // 0 to height - 1.
6715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var scale = 0;
6725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var bottom = this.height_ - 1;
6735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (this.max_)
6745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          scale = bottom / this.max_;
6755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Draw in reverse order, so earlier data series are drawn on top of
6775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // subsequent ones.
6785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        for (var i = this.dataSeries_.length - 1; i >= 0; --i) {
6795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          var values = this.getValues(this.dataSeries_[i]);
6805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if (!values)
6815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            continue;
6825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          context.strokeStyle = this.dataSeries_[i].getColor();
6835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          context.beginPath();
6845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          for (var x = 0; x < values.length; ++x) {
6855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
6865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            // horizontal lines.
6875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            context.lineTo(x, bottom - Math.round(values[x] * scale));
6885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          }
6895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          context.stroke();
6905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
6915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      },
6925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
6935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      /**
6945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       * Draw labels in |labels_|.
6955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)       */
6965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      drawLabels: function(context) {
6975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (this.labels_.length == 0)
6985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          return;
6995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var x;
7005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (this.labelAlign_ == LabelAlign.RIGHT) {
7015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          x = this.width_ - LABEL_HORIZONTAL_SPACING;
7025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        } else {
7035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          // Find the width of the widest label.
7045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          var maxTextWidth = 0;
7055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          for (var i = 0; i < this.labels_.length; ++i) {
7065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            var textWidth = context.measureText(this.labels_[i]).width;
7075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (maxTextWidth < textWidth)
7085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)              maxTextWidth = textWidth;
7095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          }
7105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          x = maxTextWidth + LABEL_HORIZONTAL_SPACING;
7115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
7125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
7135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Set up the context.
7145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.fillStyle = TEXT_COLOR;
7155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.textAlign = 'right';
7165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
7175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Draw top label, which is the only one that appears below its tick
7185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // mark.
7195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.textBaseline = 'top';
7205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.fillText(this.labels_[0], x, 0);
7215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
7225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Draw all the other labels.
7235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        context.textBaseline = 'bottom';
7245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        var step = (this.height_ - 1) / (this.labels_.length - 1);
7255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        for (var i = 1; i < this.labels_.length; ++i)
7265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          context.fillText(this.labels_[i], x, step * i);
7275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
7285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    };
7295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
7305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return Graph;
7315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  })();
7325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
7335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return TimelineGraphView;
7345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)})();
735