12a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// Copyright (c) 2013 The Chromium Authors. All rights reserved. 22a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 32a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// found in the LICENSE file. 42a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 52a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)/** 62a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * A TimelineGraphView displays a timeline graph on a canvas element. 72a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 82a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)var TimelineGraphView = (function() { 92a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 'use strict'; 102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Default starting scale factor, in terms of milliseconds per pixel. 122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var DEFAULT_SCALE = 1000; 132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Maximum number of labels placed vertically along the sides of the graph. 152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var MAX_VERTICAL_LABELS = 6; 162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Vertical spacing between labels and between the graph and labels. 182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var LABEL_VERTICAL_SPACING = 4; 192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Horizontal spacing between vertically placed labels and the edges of the 202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // graph. 212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var LABEL_HORIZONTAL_SPACING = 3; 222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Horizintal spacing between two horitonally placed labels along the bottom 232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // of the graph. 242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var LABEL_LABEL_HORIZONTAL_SPACING = 25; 252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Length of ticks, in pixels, next to y-axis labels. The x-axis only has 272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // one set of labels, so it can use lines instead. 282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var Y_AXIS_TICK_LENGTH = 10; 292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var GRID_COLOR = '#CCC'; 312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var TEXT_COLOR = '#000'; 322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var BACKGROUND_COLOR = '#FFF'; 332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * @constructor 362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) function TimelineGraphView(divId, canvasId) { 382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.scrollbar_ = {position_: 0, range_: 0}; 392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graphDiv_ = $(divId); 412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.canvas_ = $(canvasId); 422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Set the range and scale of the graph. Times are in milliseconds since 442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // the Unix epoch. 452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // All measurements we have must be after this time. 472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.startTime_ = 0; 482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // The current rightmost position of the graph is always at most this. 492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.endTime_ = 1; 502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_ = null; 522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Initialize the scrollbar. 542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.updateScrollbarRange_(true); 552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) TimelineGraphView.prototype = { 582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Returns the total length of the graph, in pixels. 592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) getLength_: function() { 602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var timeRange = this.endTime_ - this.startTime_; 612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Math.floor is used to ignore the last partial area, of length less 622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // than DEFAULT_SCALE. 632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return Math.floor(timeRange / DEFAULT_SCALE); 642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Returns true if the graph is scrolled all the way to the right. 682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) graphScrolledToRightEdge_: function() { 702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return this.scrollbar_.position_ == this.scrollbar_.range_; 712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Update the range of the scrollbar. If |resetPosition| is true, also 752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * sets the slider to point at the rightmost position and triggers a 762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * repaint. 772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) updateScrollbarRange_: function(resetPosition) { 792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var scrollbarRange = this.getLength_() - this.canvas_.width; 802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (scrollbarRange < 0) 812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) scrollbarRange = 0; 822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // If we've decreased the range to less than the current scroll position, 842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // we need to move the scroll position. 852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.scrollbar_.position_ > scrollbarRange) 862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) resetPosition = true; 872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.scrollbar_.range_ = scrollbarRange; 892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (resetPosition) { 902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.scrollbar_.position_ = scrollbarRange; 912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.repaint(); 922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Sets the date range displayed on the graph, switches to the default 972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * scale factor, and moves the scrollbar all the way to the right. 982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) setDateRange: function(startDate, endDate) { 1002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.startTime_ = startDate.getTime(); 1012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.endTime_ = endDate.getTime(); 1022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Safety check. 1042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.endTime_ <= this.startTime_) 1052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.startTime_ = this.endTime_ - 1; 1062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.updateScrollbarRange_(true); 1082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 1092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 1112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Updates the end time at the right of the graph to be the current time. 1122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Specifically, updates the scrollbar's range, and if the scrollbar is 1132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * all the way to the right, keeps it all the way to the right. Otherwise, 1142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * leaves the view as-is and doesn't redraw anything. 1152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 1162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) updateEndDate: function() { 1172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.endTime_ = (new Date()).getTime(); 1182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); 1192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 1202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) getStartDate: function() { 1222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return new Date(this.startTime_); 1232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 1242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 1262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Replaces the current TimelineDataSeries with |dataSeries|. 1272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 1282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) setDataSeries: function(dataSeries) { 1292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Simply recreates the Graph. 1302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_ = new Graph(); 1312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var i = 0; i < dataSeries.length; ++i) 1322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_.addDataSeries(dataSeries[i]); 1332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.repaint(); 1342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 1352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 1372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Adds |dataSeries| to the current graph. 1382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 1392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) addDataSeries: function(dataSeries) { 1402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (!this.graph_) 1412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_ = new Graph(); 1422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_.addDataSeries(dataSeries); 1432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.repaint(); 1442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 1452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 1472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Draws the graph on |canvas_|. 1482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 1492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) repaint: function() { 1502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.repaintTimerRunning_ = false; 1512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var width = this.canvas_.width; 1532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var height = this.canvas_.height; 1542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var context = this.canvas_.getContext('2d'); 1552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Clear the canvas. 1572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.fillStyle = BACKGROUND_COLOR; 1582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.fillRect(0, 0, width, height); 1592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Try to get font height in pixels. Needed for layout. 1612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var fontHeightString = context.font.match(/([0-9]+)px/)[1]; 1622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var fontHeight = parseInt(fontHeightString); 1632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Safety check, to avoid drawing anything too ugly. 1652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (fontHeightString.length == 0 || fontHeight <= 0 || 1662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) fontHeight * 4 > height || width < 50) { 1672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return; 1682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 1692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Save current transformation matrix so we can restore it later. 1712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.save(); 1722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // The center of an HTML canvas pixel is technically at (0.5, 0.5). This 1742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // makes near straight lines look bad, due to anti-aliasing. This 1752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // translation reduces the problem a little. 1762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.translate(0.5, 0.5); 1772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Figure out what time values to display. 1792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var position = this.scrollbar_.position_; 1802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // If the entire time range is being displayed, align the right edge of 1812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // the graph to the end of the time range. 1822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.scrollbar_.range_ == 0) 1832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) position = this.getLength_() - this.canvas_.width; 1842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var visibleStartTime = this.startTime_ + position * DEFAULT_SCALE; 1852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Make space at the bottom of the graph for the time labels, and then 1872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // draw the labels. 1882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var textHeight = height; 1892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) height -= fontHeight + LABEL_VERTICAL_SPACING; 1902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.drawTimeLabels(context, width, height, textHeight, visibleStartTime); 1912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Draw outline of the main graph area. 1932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.strokeStyle = GRID_COLOR; 1942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.strokeRect(0, 0, width - 1, height - 1); 1952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.graph_) { 1972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Layout graph and have them draw their tick marks. 1982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_.layout( 1992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) width, height, fontHeight, visibleStartTime, DEFAULT_SCALE); 2002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_.drawTicks(context); 2012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Draw the lines of all graphs, and then draw their labels. 2032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_.drawLines(context); 2042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.graph_.drawLabels(context); 2052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 2062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Restore original transformation matrix. 2082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.restore(); 2092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 2102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 2122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Draw time labels below the graph. Takes in start time as an argument 2132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * since it may not be |startTime_|, when we're displaying the entire 2142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * time range. 2152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 2162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) drawTimeLabels: function(context, width, height, textHeight, startTime) { 217b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles) // Draw the labels 1 minute apart. 218b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles) var timeStep = 1000 * 60; 2192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Find the time for the first label. This time is a perfect multiple of 2212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // timeStep because of how UTC times work. 2222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var time = Math.ceil(startTime / timeStep) * timeStep; 2232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.textBaseline = 'bottom'; 2252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.textAlign = 'center'; 2262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.fillStyle = TEXT_COLOR; 2272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.strokeStyle = GRID_COLOR; 2282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Draw labels and vertical grid lines. 2302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) while (true) { 2312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var x = Math.round((time - startTime) / DEFAULT_SCALE); 2322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (x >= width) 2332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) break; 2342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var text = (new Date(time)).toLocaleTimeString(); 2352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.fillText(text, x, textHeight); 2362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.beginPath(); 2372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.lineTo(x, 0); 2382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.lineTo(x, height); 2392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.stroke(); 2402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) time += timeStep; 2412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 2422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 2432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) getDataSeriesCount: function() { 2452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.graph_) 2462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return this.graph_.dataSeries_.length; 2472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return 0; 2482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 2492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) hasDataSeries: function(dataSeries) { 2512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.graph_) 2522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return this.graph_.hasDataSeries(dataSeries); 2532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return false; 2542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 2552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }; 2572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 2592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * A Graph is responsible for drawing all the TimelineDataSeries that have 2602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * the same data type. Graphs are responsible for scaling the values, laying 2612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * out labels, and drawing both labels and lines for its data series. 2622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 2632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var Graph = (function() { 2642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 2652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * @constructor 2662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 2672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) function Graph() { 2682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.dataSeries_ = []; 2692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Cached properties of the graph, set in layout. 2712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.width_ = 0; 2722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.height_ = 0; 2732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.fontHeight_ = 0; 2742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.startTime_ = 0; 2752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.scale_ = 0; 2762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // At least the highest value in the displayed range of the graph. 2782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Used for scaling and setting labels. Set in layoutLabels. 2792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.max_ = 0; 2802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Cached text of equally spaced labels. Set in layoutLabels. 2822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.labels_ = []; 2832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 2842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 2862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * A Label is the label at a particular position along the y-axis. 2872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * @constructor 2882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 2892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) function Label(height, text) { 2902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.height = height; 2912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.text = text; 2922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 2932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) Graph.prototype = { 2952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) addDataSeries: function(dataSeries) { 2962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.dataSeries_.push(dataSeries); 2972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 2982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 2992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) hasDataSeries: function(dataSeries) { 3002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var i = 0; i < this.dataSeries_.length; ++i) { 3012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.dataSeries_[i] == dataSeries) 3022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return true; 3032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 3042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return false; 3052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 3062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 3082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Returns a list of all the values that should be displayed for a given 3092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * data series, using the current graph layout. 3102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 3112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) getValues: function(dataSeries) { 3122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (!dataSeries.isVisible()) 3132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return null; 3142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return dataSeries.getValues(this.startTime_, this.scale_, this.width_); 3152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 3162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 3182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Updates the graph's layout. In particular, both the max value and 3192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * label positions are updated. Must be called before calling any of the 3202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * drawing functions. 3212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 3222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) layout: function(width, height, fontHeight, startTime, scale) { 3232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.width_ = width; 3242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.height_ = height; 3252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.fontHeight_ = fontHeight; 3262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.startTime_ = startTime; 3272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.scale_ = scale; 3282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Find largest value. 3302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var max = 0; 3312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var i = 0; i < this.dataSeries_.length; ++i) { 3322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var values = this.getValues(this.dataSeries_[i]); 3332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (!values) 3342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) continue; 3352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var j = 0; j < values.length; ++j) { 3362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (values[j] > max) 3372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) max = values[j]; 3382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 3392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 3402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.layoutLabels_(max); 3422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 3432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 3452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Lays out labels and sets |max_|, taking the time units into 3462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * consideration. |maxValue| is the actual maximum value, and 3472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * |max_| will be set to the value of the largest label, which 3482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * will be at least |maxValue|. 3492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 3502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) layoutLabels_: function(maxValue) { 3512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (maxValue < 1024) { 3522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.layoutLabelsBasic_(maxValue, 0); 3532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return; 3542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 3552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Find appropriate units to use. 3572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var units = ['', 'k', 'M', 'G', 'T', 'P']; 3582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Units to use for labels. 0 is '1', 1 is K, etc. 3592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // We start with 1, and work our way up. 3602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var unit = 1; 3612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) maxValue /= 1024; 3622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) while (units[unit + 1] && maxValue >= 1024) { 3632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) maxValue /= 1024; 3642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) ++unit; 3652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 3662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Calculate labels. 3682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.layoutLabelsBasic_(maxValue, 1); 3692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Append units to labels. 3712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var i = 0; i < this.labels_.length; ++i) 3722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.labels_[i] += ' ' + units[unit]; 3732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Convert |max_| back to unit '1'. 3752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.max_ *= Math.pow(1024, unit); 3762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 3772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 3792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the 3802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * maximum number of decimal digits allowed. The minimum allowed 3812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * difference between two adjacent labels is 10^-|maxDecimalDigits|. 3822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 3832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) layoutLabelsBasic_: function(maxValue, maxDecimalDigits) { 3842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.labels_ = []; 3852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // No labels if |maxValue| is 0. 3862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (maxValue == 0) { 3872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.max_ = maxValue; 3882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return; 3892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 3902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // The maximum number of equally spaced labels allowed. |fontHeight_| 3922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // is doubled because the top two labels are both drawn in the same 3932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // gap. 3942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING; 3952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // The + 1 is for the top label. 3972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var maxLabels = 1 + this.height_ / minLabelSpacing; 3982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (maxLabels < 2) { 3992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) maxLabels = 2; 4002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } else if (maxLabels > MAX_VERTICAL_LABELS) { 4012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) maxLabels = MAX_VERTICAL_LABELS; 4022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 4032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Initial try for step size between conecutive labels. 4052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var stepSize = Math.pow(10, -maxDecimalDigits); 4062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Number of digits to the right of the decimal of |stepSize|. 4072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Used for formating label strings. 4082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var stepSizeDecimalDigits = maxDecimalDigits; 4092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Pick a reasonable step size. 4112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) while (true) { 4122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // If we use a step size of |stepSize| between labels, we'll need: 4132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // 4142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Math.ceil(maxValue / stepSize) + 1 4152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // 4162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // labels. The + 1 is because we need labels at both at 0 and at 4172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // the top of the graph. 4182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Check if we can use steps of size |stepSize|. 4202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (Math.ceil(maxValue / stepSize) + 1 <= maxLabels) 4212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) break; 4222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Check |stepSize| * 2. 4232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (Math.ceil(maxValue / (stepSize * 2)) + 1 <= maxLabels) { 4242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) stepSize *= 2; 4252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) break; 4262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 4272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Check |stepSize| * 5. 4282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (Math.ceil(maxValue / (stepSize * 5)) + 1 <= maxLabels) { 4292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) stepSize *= 5; 4302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) break; 4312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 4322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) stepSize *= 10; 4332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (stepSizeDecimalDigits > 0) 4342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) --stepSizeDecimalDigits; 4352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 4362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Set the max so it's an exact multiple of the chosen step size. 4382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.max_ = Math.ceil(maxValue / stepSize) * stepSize; 4392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Create labels. 4412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var label = this.max_; label >= 0; label -= stepSize) 4422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) this.labels_.push(label.toFixed(stepSizeDecimalDigits)); 4432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 4442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 4462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Draws tick marks for each of the labels in |labels_|. 4472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 4482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) drawTicks: function(context) { 4492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var x1; 4502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var x2; 4512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) x1 = this.width_ - 1; 4522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH; 4532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.fillStyle = GRID_COLOR; 4552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.beginPath(); 4562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var i = 1; i < this.labels_.length - 1; ++i) { 4572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // The rounding is needed to avoid ugly 2-pixel wide anti-aliased 4582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // lines. 4592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var y = Math.round(this.height_ * i / (this.labels_.length - 1)); 4602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.moveTo(x1, y); 4612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.lineTo(x2, y); 4622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 4632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.stroke(); 4642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 4652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 4672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Draws a graph line for each of the data series. 4682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 4692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) drawLines: function(context) { 4702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Factor by which to scale all values to convert them to a number from 4712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // 0 to height - 1. 4722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var scale = 0; 4732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var bottom = this.height_ - 1; 4742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.max_) 4752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) scale = bottom / this.max_; 4762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Draw in reverse order, so earlier data series are drawn on top of 4782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // subsequent ones. 4792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var i = this.dataSeries_.length - 1; i >= 0; --i) { 4802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var values = this.getValues(this.dataSeries_[i]); 4812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (!values) 4822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) continue; 4832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.strokeStyle = this.dataSeries_[i].getColor(); 4842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.beginPath(); 4852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var x = 0; x < values.length; ++x) { 4862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // The rounding is needed to avoid ugly 2-pixel wide anti-aliased 4872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // horizontal lines. 4882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.lineTo(x, bottom - Math.round(values[x] * scale)); 4892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 4902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.stroke(); 4912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 4922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }, 4932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 4942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) /** 4952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Draw labels in |labels_|. 4962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 4972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) drawLabels: function(context) { 4982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (this.labels_.length == 0) 4992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return; 5002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var x = this.width_ - LABEL_HORIZONTAL_SPACING; 5012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 5022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Set up the context. 5032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.fillStyle = TEXT_COLOR; 5042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.textAlign = 'right'; 5052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 5062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Draw top label, which is the only one that appears below its tick 5072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // mark. 5082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.textBaseline = 'top'; 5092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.fillText(this.labels_[0], x, 0); 5102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 5112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Draw all the other labels. 5122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.textBaseline = 'bottom'; 5132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var step = (this.height_ - 1) / (this.labels_.length - 1); 5142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) for (var i = 1; i < this.labels_.length; ++i) 5152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) context.fillText(this.labels_[i], x, step * i); 5162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 5172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) }; 5182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 5192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return Graph; 5202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) })(); 5212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 5222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return TimelineGraphView; 5232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)})(); 524