1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * A TimelineGraphView displays a timeline graph on a canvas element. 7 */ 8var TimelineGraphView = (function() { 9 'use strict'; 10 // We inherit from TopMidBottomView. 11 var superClass = TopMidBottomView; 12 13 // Default starting scale factor, in terms of milliseconds per pixel. 14 var DEFAULT_SCALE = 1000; 15 16 // Maximum number of labels placed vertically along the sides of the graph. 17 var MAX_VERTICAL_LABELS = 6; 18 19 // Vertical spacing between labels and between the graph and labels. 20 var LABEL_VERTICAL_SPACING = 4; 21 // Horizontal spacing between vertically placed labels and the edges of the 22 // graph. 23 var LABEL_HORIZONTAL_SPACING = 3; 24 // Horizintal spacing between two horitonally placed labels along the bottom 25 // of the graph. 26 var LABEL_LABEL_HORIZONTAL_SPACING = 25; 27 28 // Length of ticks, in pixels, next to y-axis labels. The x-axis only has 29 // one set of labels, so it can use lines instead. 30 var Y_AXIS_TICK_LENGTH = 10; 31 32 // The number of units mouse wheel deltas increase for each tick of the 33 // wheel. 34 var MOUSE_WHEEL_UNITS_PER_CLICK = 120; 35 36 // Amount we zoom for one vertical tick of the mouse wheel, as a ratio. 37 var MOUSE_WHEEL_ZOOM_RATE = 1.25; 38 // Amount we scroll for one horizontal tick of the mouse wheel, in pixels. 39 var MOUSE_WHEEL_SCROLL_RATE = MOUSE_WHEEL_UNITS_PER_CLICK; 40 // Number of pixels to scroll per pixel the mouse is dragged. 41 var MOUSE_WHEEL_DRAG_RATE = 3; 42 43 var GRID_COLOR = '#CCC'; 44 var TEXT_COLOR = '#000'; 45 var BACKGROUND_COLOR = '#FFF'; 46 47 // Which side of the canvas y-axis labels should go on, for a given Graph. 48 // TODO(mmenke): Figure out a reasonable way to handle more than 2 sets 49 // of labels. 50 var LabelAlign = { 51 LEFT: 0, 52 RIGHT: 1 53 }; 54 55 /** 56 * @constructor 57 */ 58 function TimelineGraphView(divId, canvasId, scrollbarId, scrollbarInnerId) { 59 this.scrollbar_ = new HorizontalScrollbarView(scrollbarId, 60 scrollbarInnerId, 61 this.onScroll_.bind(this)); 62 // Call superclass's constructor. 63 superClass.call(this, null, new DivView(divId), this.scrollbar_); 64 65 this.graphDiv_ = $(divId); 66 this.canvas_ = $(canvasId); 67 this.canvas_.onmousewheel = this.onMouseWheel_.bind(this); 68 this.canvas_.onmousedown = this.onMouseDown_.bind(this); 69 this.canvas_.onmousemove = this.onMouseMove_.bind(this); 70 this.canvas_.onmouseup = this.onMouseUp_.bind(this); 71 this.canvas_.onmouseout = this.onMouseUp_.bind(this); 72 73 // Used for click and drag scrolling of graph. Drag-zooming not supported, 74 // for a more stable scrolling experience. 75 this.isDragging_ = false; 76 this.dragX_ = 0; 77 78 // Set the range and scale of the graph. Times are in milliseconds since 79 // the Unix epoch. 80 81 // All measurements we have must be after this time. 82 this.startTime_ = 0; 83 // The current rightmost position of the graph is always at most this. 84 // We may have some later events. When actively capturing new events, it's 85 // updated on a timer. 86 this.endTime_ = 1; 87 88 // Current scale, in terms of milliseconds per pixel. Each column of 89 // pixels represents a point in time |scale_| milliseconds after the 90 // previous one. We only display times that are of the form 91 // |startTime_| + K * |scale_| to avoid jittering, and the rightmost 92 // pixel that we can display has a time <= |endTime_|. Non-integer values 93 // are allowed. 94 this.scale_ = DEFAULT_SCALE; 95 96 this.graphs_ = []; 97 98 // Initialize the scrollbar. 99 this.updateScrollbarRange_(true); 100 } 101 102 // Smallest allowed scaling factor. 103 TimelineGraphView.MIN_SCALE = 5; 104 105 TimelineGraphView.prototype = { 106 // Inherit the superclass's methods. 107 __proto__: superClass.prototype, 108 109 setGeometry: function(left, top, width, height) { 110 superClass.prototype.setGeometry.call(this, left, top, width, height); 111 112 // The size of the canvas can only be set by using its |width| and 113 // |height| properties, which do not take padding into account, so we 114 // need to use them ourselves. 115 var style = getComputedStyle(this.canvas_); 116 var horizontalPadding = parseInt(style.paddingRight) + 117 parseInt(style.paddingLeft); 118 var verticalPadding = parseInt(style.paddingTop) + 119 parseInt(style.paddingBottom); 120 var canvasWidth = 121 parseInt(this.graphDiv_.style.width) - horizontalPadding; 122 // For unknown reasons, there's an extra 3 pixels border between the 123 // bottom of the canvas and the bottom margin of the enclosing div. 124 var canvasHeight = 125 parseInt(this.graphDiv_.style.height) - verticalPadding - 3; 126 127 // Protect against degenerates. 128 if (canvasWidth < 10) 129 canvasWidth = 10; 130 if (canvasHeight < 10) 131 canvasHeight = 10; 132 133 this.canvas_.width = canvasWidth; 134 this.canvas_.height = canvasHeight; 135 136 // Use the same font style for the canvas as we use elsewhere. 137 // Has to be updated every resize. 138 this.canvas_.getContext('2d').font = getComputedStyle(this.canvas_).font; 139 140 this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); 141 this.repaint(); 142 }, 143 144 show: function(isVisible) { 145 superClass.prototype.show.call(this, isVisible); 146 if (isVisible) 147 this.repaint(); 148 }, 149 150 // Returns the total length of the graph, in pixels. 151 getLength_: function() { 152 var timeRange = this.endTime_ - this.startTime_; 153 // Math.floor is used to ignore the last partial area, of length less 154 // than |scale_|. 155 return Math.floor(timeRange / this.scale_); 156 }, 157 158 /** 159 * Returns true if the graph is scrolled all the way to the right. 160 */ 161 graphScrolledToRightEdge_: function() { 162 return this.scrollbar_.getPosition() == this.scrollbar_.getRange(); 163 }, 164 165 /** 166 * Update the range of the scrollbar. If |resetPosition| is true, also 167 * sets the slider to point at the rightmost position and triggers a 168 * repaint. 169 */ 170 updateScrollbarRange_: function(resetPosition) { 171 var scrollbarRange = this.getLength_() - this.canvas_.width; 172 if (scrollbarRange < 0) 173 scrollbarRange = 0; 174 175 // If we've decreased the range to less than the current scroll position, 176 // we need to move the scroll position. 177 if (this.scrollbar_.getPosition() > scrollbarRange) 178 resetPosition = true; 179 180 this.scrollbar_.setRange(scrollbarRange); 181 if (resetPosition) { 182 this.scrollbar_.setPosition(scrollbarRange); 183 this.repaint(); 184 } 185 }, 186 187 /** 188 * Sets the date range displayed on the graph, switches to the default 189 * scale factor, and moves the scrollbar all the way to the right. 190 */ 191 setDateRange: function(startDate, endDate) { 192 this.startTime_ = startDate.getTime(); 193 this.endTime_ = endDate.getTime(); 194 195 // Safety check. 196 if (this.endTime_ <= this.startTime_) 197 this.startTime_ = this.endTime_ - 1; 198 199 this.scale_ = DEFAULT_SCALE; 200 this.updateScrollbarRange_(true); 201 }, 202 203 /** 204 * Updates the end time at the right of the graph to be the current time. 205 * Specifically, updates the scrollbar's range, and if the scrollbar is 206 * all the way to the right, keeps it all the way to the right. Otherwise, 207 * leaves the view as-is and doesn't redraw anything. 208 */ 209 updateEndDate: function() { 210 this.endTime_ = timeutil.getCurrentTime(); 211 this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); 212 }, 213 214 getStartDate: function() { 215 return new Date(this.startTime_); 216 }, 217 218 /** 219 * Scrolls the graph horizontally by the specified amount. 220 */ 221 horizontalScroll_: function(delta) { 222 var newPosition = this.scrollbar_.getPosition() + Math.round(delta); 223 // Make sure the new position is in the right range. 224 if (newPosition < 0) { 225 newPosition = 0; 226 } else if (newPosition > this.scrollbar_.getRange()) { 227 newPosition = this.scrollbar_.getRange(); 228 } 229 230 if (this.scrollbar_.getPosition() == newPosition) 231 return; 232 this.scrollbar_.setPosition(newPosition); 233 this.onScroll_(); 234 }, 235 236 /** 237 * Zooms the graph by the specified amount. 238 */ 239 zoom_: function(ratio) { 240 var oldScale = this.scale_; 241 this.scale_ *= ratio; 242 if (this.scale_ < TimelineGraphView.MIN_SCALE) 243 this.scale_ = TimelineGraphView.MIN_SCALE; 244 245 if (this.scale_ == oldScale) 246 return; 247 248 // If we were at the end of the range before, remain at the end of the 249 // range. 250 if (this.graphScrolledToRightEdge_()) { 251 this.updateScrollbarRange_(true); 252 return; 253 } 254 255 // Otherwise, do our best to maintain the old position. We use the 256 // position at the far right of the graph for consistency. 257 var oldMaxTime = 258 oldScale * (this.scrollbar_.getPosition() + this.canvas_.width); 259 var newMaxTime = Math.round(oldMaxTime / this.scale_); 260 var newPosition = newMaxTime - this.canvas_.width; 261 262 // Update range and scroll position. 263 this.updateScrollbarRange_(false); 264 this.horizontalScroll_(newPosition - this.scrollbar_.getPosition()); 265 }, 266 267 onMouseWheel_: function(event) { 268 event.preventDefault(); 269 this.horizontalScroll_( 270 MOUSE_WHEEL_SCROLL_RATE * 271 -event.wheelDeltaX / MOUSE_WHEEL_UNITS_PER_CLICK); 272 this.zoom_(Math.pow(MOUSE_WHEEL_ZOOM_RATE, 273 -event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK)); 274 }, 275 276 onMouseDown_: function(event) { 277 event.preventDefault(); 278 this.isDragging_ = true; 279 this.dragX_ = event.clientX; 280 }, 281 282 onMouseMove_: function(event) { 283 if (!this.isDragging_) 284 return; 285 event.preventDefault(); 286 this.horizontalScroll_( 287 MOUSE_WHEEL_DRAG_RATE * (event.clientX - this.dragX_)); 288 this.dragX_ = event.clientX; 289 }, 290 291 onMouseUp_: function(event) { 292 this.isDragging_ = false; 293 }, 294 295 onScroll_: function() { 296 this.repaint(); 297 }, 298 299 /** 300 * Replaces the current TimelineDataSeries with |dataSeries|. 301 */ 302 setDataSeries: function(dataSeries) { 303 // Simplest just to recreate the Graphs. 304 this.graphs_ = []; 305 this.graphs_[TimelineDataType.BYTES_PER_SECOND] = 306 new Graph(TimelineDataType.BYTES_PER_SECOND, LabelAlign.RIGHT); 307 this.graphs_[TimelineDataType.SOURCE_COUNT] = 308 new Graph(TimelineDataType.SOURCE_COUNT, LabelAlign.LEFT); 309 for (var i = 0; i < dataSeries.length; ++i) 310 this.graphs_[dataSeries[i].getDataType()].addDataSeries(dataSeries[i]); 311 312 this.repaint(); 313 }, 314 315 /** 316 * Draws the graph on |canvas_|. 317 */ 318 repaint: function() { 319 this.repaintTimerRunning_ = false; 320 if (!this.isVisible()) 321 return; 322 323 var width = this.canvas_.width; 324 var height = this.canvas_.height; 325 var context = this.canvas_.getContext('2d'); 326 327 // Clear the canvas. 328 context.fillStyle = BACKGROUND_COLOR; 329 context.fillRect(0, 0, width, height); 330 331 // Try to get font height in pixels. Needed for layout. 332 var fontHeightString = context.font.match(/([0-9]+)px/)[1]; 333 var fontHeight = parseInt(fontHeightString); 334 335 // Safety check, to avoid drawing anything too ugly. 336 if (fontHeightString.length == 0 || fontHeight <= 0 || 337 fontHeight * 4 > height || width < 50) { 338 return; 339 } 340 341 // Save current transformation matrix so we can restore it later. 342 context.save(); 343 344 // The center of an HTML canvas pixel is technically at (0.5, 0.5). This 345 // makes near straight lines look bad, due to anti-aliasing. This 346 // translation reduces the problem a little. 347 context.translate(0.5, 0.5); 348 349 // Figure out what time values to display. 350 var position = this.scrollbar_.getPosition(); 351 // If the entire time range is being displayed, align the right edge of 352 // the graph to the end of the time range. 353 if (this.scrollbar_.getRange() == 0) 354 position = this.getLength_() - this.canvas_.width; 355 var visibleStartTime = this.startTime_ + position * this.scale_; 356 357 // Make space at the bottom of the graph for the time labels, and then 358 // draw the labels. 359 var textHeight = height; 360 height -= fontHeight + LABEL_VERTICAL_SPACING; 361 this.drawTimeLabels(context, width, height, textHeight, visibleStartTime); 362 363 // Draw outline of the main graph area. 364 context.strokeStyle = GRID_COLOR; 365 context.strokeRect(0, 0, width - 1, height - 1); 366 367 // Layout graphs and have them draw their tick marks. 368 for (var i = 0; i < this.graphs_.length; ++i) { 369 this.graphs_[i].layout(width, height, fontHeight, visibleStartTime, 370 this.scale_); 371 this.graphs_[i].drawTicks(context); 372 } 373 374 // Draw the lines of all graphs, and then draw their labels. 375 for (var i = 0; i < this.graphs_.length; ++i) 376 this.graphs_[i].drawLines(context); 377 for (var i = 0; i < this.graphs_.length; ++i) 378 this.graphs_[i].drawLabels(context); 379 380 // Restore original transformation matrix. 381 context.restore(); 382 }, 383 384 /** 385 * Draw time labels below the graph. Takes in start time as an argument 386 * since it may not be |startTime_|, when we're displaying the entire 387 * time range. 388 */ 389 drawTimeLabels: function(context, width, height, textHeight, startTime) { 390 // Text for a time string to use in determining how far apart 391 // to place text labels. 392 var sampleText = (new Date(startTime)).toLocaleTimeString(); 393 394 // The desired spacing for text labels. 395 var targetSpacing = context.measureText(sampleText).width + 396 LABEL_LABEL_HORIZONTAL_SPACING; 397 398 // The allowed time step values between adjacent labels. Anything much 399 // over a couple minutes isn't terribly realistic, given how much memory 400 // we use, and how slow a lot of the net-internals code is. 401 var timeStepValues = [ 402 1000, // 1 second 403 1000 * 5, 404 1000 * 30, 405 1000 * 60, // 1 minute 406 1000 * 60 * 5, 407 1000 * 60 * 30, 408 1000 * 60 * 60, // 1 hour 409 1000 * 60 * 60 * 5 410 ]; 411 412 // Find smallest time step value that gives us at least |targetSpacing|, 413 // if any. 414 var timeStep = null; 415 for (var i = 0; i < timeStepValues.length; ++i) { 416 if (timeStepValues[i] / this.scale_ >= targetSpacing) { 417 timeStep = timeStepValues[i]; 418 break; 419 } 420 } 421 422 // If no such value, give up. 423 if (!timeStep) 424 return; 425 426 // Find the time for the first label. This time is a perfect multiple of 427 // timeStep because of how UTC times work. 428 var time = Math.ceil(startTime / timeStep) * timeStep; 429 430 context.textBaseline = 'bottom'; 431 context.textAlign = 'center'; 432 context.fillStyle = TEXT_COLOR; 433 context.strokeStyle = GRID_COLOR; 434 435 // Draw labels and vertical grid lines. 436 while (true) { 437 var x = Math.round((time - startTime) / this.scale_); 438 if (x >= width) 439 break; 440 var text = (new Date(time)).toLocaleTimeString(); 441 context.fillText(text, x, textHeight); 442 context.beginPath(); 443 context.lineTo(x, 0); 444 context.lineTo(x, height); 445 context.stroke(); 446 time += timeStep; 447 } 448 } 449 }; 450 451 /** 452 * A Graph is responsible for drawing all the TimelineDataSeries that have 453 * the same data type. Graphs are responsible for scaling the values, laying 454 * out labels, and drawing both labels and lines for its data series. 455 */ 456 var Graph = (function() { 457 /** 458 * |dataType| is the DataType that will be shared by all its DataSeries. 459 * |labelAlign| is the LabelAlign value indicating whether the labels 460 * should be aligned to the right of left of the graph. 461 * @constructor 462 */ 463 function Graph(dataType, labelAlign) { 464 this.dataType_ = dataType; 465 this.dataSeries_ = []; 466 this.labelAlign_ = labelAlign; 467 468 // Cached properties of the graph, set in layout. 469 this.width_ = 0; 470 this.height_ = 0; 471 this.fontHeight_ = 0; 472 this.startTime_ = 0; 473 this.scale_ = 0; 474 475 // At least the highest value in the displayed range of the graph. 476 // Used for scaling and setting labels. Set in layoutLabels. 477 this.max_ = 0; 478 479 // Cached text of equally spaced labels. Set in layoutLabels. 480 this.labels_ = []; 481 } 482 483 /** 484 * A Label is the label at a particular position along the y-axis. 485 * @constructor 486 */ 487 function Label(height, text) { 488 this.height = height; 489 this.text = text; 490 } 491 492 Graph.prototype = { 493 addDataSeries: function(dataSeries) { 494 this.dataSeries_.push(dataSeries); 495 }, 496 497 /** 498 * Returns a list of all the values that should be displayed for a given 499 * data series, using the current graph layout. 500 */ 501 getValues: function(dataSeries) { 502 if (!dataSeries.isVisible()) 503 return null; 504 return dataSeries.getValues(this.startTime_, this.scale_, this.width_); 505 }, 506 507 /** 508 * Updates the graph's layout. In particular, both the max value and 509 * label positions are updated. Must be called before calling any of the 510 * drawing functions. 511 */ 512 layout: function(width, height, fontHeight, startTime, scale) { 513 this.width_ = width; 514 this.height_ = height; 515 this.fontHeight_ = fontHeight; 516 this.startTime_ = startTime; 517 this.scale_ = scale; 518 519 // Find largest value. 520 var max = 0; 521 for (var i = 0; i < this.dataSeries_.length; ++i) { 522 var values = this.getValues(this.dataSeries_[i]); 523 if (!values) 524 continue; 525 for (var j = 0; j < values.length; ++j) { 526 if (values[j] > max) 527 max = values[j]; 528 } 529 } 530 531 this.layoutLabels_(max); 532 }, 533 534 /** 535 * Lays out labels and sets |max_|, taking the time units into 536 * consideration. |maxValue| is the actual maximum value, and 537 * |max_| will be set to the value of the largest label, which 538 * will be at least |maxValue|. 539 */ 540 layoutLabels_: function(maxValue) { 541 if (this.dataType_ != TimelineDataType.BYTES_PER_SECOND) { 542 this.layoutLabelsBasic_(maxValue, 0); 543 return; 544 } 545 546 // Special handling for data rates. 547 548 // Find appropriate units to use. 549 var units = ['B/s', 'kB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s']; 550 // Units to use for labels. 0 is bytes, 1 is kilobytes, etc. 551 // We start with kilobytes, and work our way up. 552 var unit = 1; 553 // Update |maxValue| to be in the right units. 554 maxValue = maxValue / 1024; 555 while (units[unit + 1] && maxValue >= 999) { 556 maxValue /= 1024; 557 ++unit; 558 } 559 560 // Calculate labels. 561 this.layoutLabelsBasic_(maxValue, 1); 562 563 // Append units to labels. 564 for (var i = 0; i < this.labels_.length; ++i) 565 this.labels_[i] += ' ' + units[unit]; 566 567 // Convert |max_| back to bytes, so it can be used when scaling values 568 // for display. 569 this.max_ *= Math.pow(1024, unit); 570 }, 571 572 /** 573 * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the 574 * maximum number of decimal digits allowed. The minimum allowed 575 * difference between two adjacent labels is 10^-|maxDecimalDigits|. 576 */ 577 layoutLabelsBasic_: function(maxValue, maxDecimalDigits) { 578 this.labels_ = []; 579 // No labels if |maxValue| is 0. 580 if (maxValue == 0) { 581 this.max_ = maxValue; 582 return; 583 } 584 585 // The maximum number of equally spaced labels allowed. |fontHeight_| 586 // is doubled because the top two labels are both drawn in the same 587 // gap. 588 var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING; 589 590 // The + 1 is for the top label. 591 var maxLabels = 1 + this.height_ / minLabelSpacing; 592 if (maxLabels < 2) { 593 maxLabels = 2; 594 } else if (maxLabels > MAX_VERTICAL_LABELS) { 595 maxLabels = MAX_VERTICAL_LABELS; 596 } 597 598 // Initial try for step size between conecutive labels. 599 var stepSize = Math.pow(10, -maxDecimalDigits); 600 // Number of digits to the right of the decimal of |stepSize|. 601 // Used for formating label strings. 602 var stepSizeDecimalDigits = maxDecimalDigits; 603 604 // Pick a reasonable step size. 605 while (true) { 606 // If we use a step size of |stepSize| between labels, we'll need: 607 // 608 // Math.ceil(maxValue / stepSize) + 1 609 // 610 // labels. The + 1 is because we need labels at both at 0 and at 611 // the top of the graph. 612 613 // Check if we can use steps of size |stepSize|. 614 if (Math.ceil(maxValue / stepSize) + 1 <= maxLabels) 615 break; 616 // Check |stepSize| * 2. 617 if (Math.ceil(maxValue / (stepSize * 2)) + 1 <= maxLabels) { 618 stepSize *= 2; 619 break; 620 } 621 // Check |stepSize| * 5. 622 if (Math.ceil(maxValue / (stepSize * 5)) + 1 <= maxLabels) { 623 stepSize *= 5; 624 break; 625 } 626 stepSize *= 10; 627 if (stepSizeDecimalDigits > 0) 628 --stepSizeDecimalDigits; 629 } 630 631 // Set the max so it's an exact multiple of the chosen step size. 632 this.max_ = Math.ceil(maxValue / stepSize) * stepSize; 633 634 // Create labels. 635 for (var label = this.max_; label >= 0; label -= stepSize) 636 this.labels_.push(label.toFixed(stepSizeDecimalDigits)); 637 }, 638 639 /** 640 * Draws tick marks for each of the labels in |labels_|. 641 */ 642 drawTicks: function(context) { 643 var x1; 644 var x2; 645 if (this.labelAlign_ == LabelAlign.RIGHT) { 646 x1 = this.width_ - 1; 647 x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH; 648 } else { 649 x1 = 0; 650 x2 = Y_AXIS_TICK_LENGTH; 651 } 652 653 context.fillStyle = GRID_COLOR; 654 context.beginPath(); 655 for (var i = 1; i < this.labels_.length - 1; ++i) { 656 // The rounding is needed to avoid ugly 2-pixel wide anti-aliased 657 // lines. 658 var y = Math.round(this.height_ * i / (this.labels_.length - 1)); 659 context.moveTo(x1, y); 660 context.lineTo(x2, y); 661 } 662 context.stroke(); 663 }, 664 665 /** 666 * Draws a graph line for each of the data series. 667 */ 668 drawLines: function(context) { 669 // Factor by which to scale all values to convert them to a number from 670 // 0 to height - 1. 671 var scale = 0; 672 var bottom = this.height_ - 1; 673 if (this.max_) 674 scale = bottom / this.max_; 675 676 // Draw in reverse order, so earlier data series are drawn on top of 677 // subsequent ones. 678 for (var i = this.dataSeries_.length - 1; i >= 0; --i) { 679 var values = this.getValues(this.dataSeries_[i]); 680 if (!values) 681 continue; 682 context.strokeStyle = this.dataSeries_[i].getColor(); 683 context.beginPath(); 684 for (var x = 0; x < values.length; ++x) { 685 // The rounding is needed to avoid ugly 2-pixel wide anti-aliased 686 // horizontal lines. 687 context.lineTo(x, bottom - Math.round(values[x] * scale)); 688 } 689 context.stroke(); 690 } 691 }, 692 693 /** 694 * Draw labels in |labels_|. 695 */ 696 drawLabels: function(context) { 697 if (this.labels_.length == 0) 698 return; 699 var x; 700 if (this.labelAlign_ == LabelAlign.RIGHT) { 701 x = this.width_ - LABEL_HORIZONTAL_SPACING; 702 } else { 703 // Find the width of the widest label. 704 var maxTextWidth = 0; 705 for (var i = 0; i < this.labels_.length; ++i) { 706 var textWidth = context.measureText(this.labels_[i]).width; 707 if (maxTextWidth < textWidth) 708 maxTextWidth = textWidth; 709 } 710 x = maxTextWidth + LABEL_HORIZONTAL_SPACING; 711 } 712 713 // Set up the context. 714 context.fillStyle = TEXT_COLOR; 715 context.textAlign = 'right'; 716 717 // Draw top label, which is the only one that appears below its tick 718 // mark. 719 context.textBaseline = 'top'; 720 context.fillText(this.labels_[0], x, 0); 721 722 // Draw all the other labels. 723 context.textBaseline = 'bottom'; 724 var step = (this.height_ - 1) / (this.labels_.length - 1); 725 for (var i = 1; i < this.labels_.length; ++i) 726 context.fillText(this.labels_[i], x, step * i); 727 } 728 }; 729 730 return Graph; 731 })(); 732 733 return TimelineGraphView; 734})(); 735