OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. |
| 3 * |
| 4 * Use of this source code is governed by a BSD-style license |
| 5 * that can be found in the LICENSE file in the root of the source |
| 6 * tree. |
| 7 */ |
| 8 // taken from chrome://webrtc-internals with jshint adaptions |
| 9 |
| 10 'use strict'; |
| 11 /* exported TimelineDataSeries, TimelineGraphView */ |
| 12 |
| 13 // The maximum number of data points bufferred for each stats. Old data points |
| 14 // will be shifted out when the buffer is full. |
| 15 var MAX_STATS_DATA_POINT_BUFFER_SIZE = 1000; |
| 16 |
| 17 var TimelineDataSeries = (function() { |
| 18 /** |
| 19 * @constructor |
| 20 */ |
| 21 function TimelineDataSeries() { |
| 22 // List of DataPoints in chronological order. |
| 23 this.dataPoints_ = []; |
| 24 |
| 25 // Default color. Should always be overridden prior to display. |
| 26 this.color_ = 'red'; |
| 27 // Whether or not the data series should be drawn. |
| 28 this.isVisible_ = true; |
| 29 |
| 30 this.cacheStartTime_ = null; |
| 31 this.cacheStepSize_ = 0; |
| 32 this.cacheValues_ = []; |
| 33 } |
| 34 |
| 35 TimelineDataSeries.prototype = { |
| 36 /** |
| 37 * @override |
| 38 */ |
| 39 toJSON: function() { |
| 40 if (this.dataPoints_.length < 1) { |
| 41 return {}; |
| 42 } |
| 43 |
| 44 var values = []; |
| 45 for (var i = 0; i < this.dataPoints_.length; ++i) { |
| 46 values.push(this.dataPoints_[i].value); |
| 47 } |
| 48 return { |
| 49 startTime: this.dataPoints_[0].time, |
| 50 endTime: this.dataPoints_[this.dataPoints_.length - 1].time, |
| 51 values: JSON.stringify(values), |
| 52 }; |
| 53 }, |
| 54 |
| 55 /** |
| 56 * Adds a DataPoint to |this| with the specified time and value. |
| 57 * DataPoints are assumed to be received in chronological order. |
| 58 */ |
| 59 addPoint: function(timeTicks, value) { |
| 60 var time = new Date(timeTicks); |
| 61 this.dataPoints_.push(new DataPoint(time, value)); |
| 62 |
| 63 if (this.dataPoints_.length > MAX_STATS_DATA_POINT_BUFFER_SIZE) { |
| 64 this.dataPoints_.shift(); |
| 65 } |
| 66 }, |
| 67 |
| 68 isVisible: function() { |
| 69 return this.isVisible_; |
| 70 }, |
| 71 |
| 72 show: function(isVisible) { |
| 73 this.isVisible_ = isVisible; |
| 74 }, |
| 75 |
| 76 getColor: function() { |
| 77 return this.color_; |
| 78 }, |
| 79 |
| 80 setColor: function(color) { |
| 81 this.color_ = color; |
| 82 }, |
| 83 |
| 84 getCount: function() { |
| 85 return this.dataPoints_.length; |
| 86 }, |
| 87 /** |
| 88 * Returns a list containing the values of the data series at |count| |
| 89 * points, starting at |startTime|, and |stepSize| milliseconds apart. |
| 90 * Caches values, so showing/hiding individual data series is fast. |
| 91 */ |
| 92 getValues: function(startTime, stepSize, count) { |
| 93 // Use cached values, if we can. |
| 94 if (this.cacheStartTime_ === startTime && |
| 95 this.cacheStepSize_ === stepSize && |
| 96 this.cacheValues_.length === count) { |
| 97 return this.cacheValues_; |
| 98 } |
| 99 |
| 100 // Do all the work. |
| 101 this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count); |
| 102 this.cacheStartTime_ = startTime; |
| 103 this.cacheStepSize_ = stepSize; |
| 104 |
| 105 return this.cacheValues_; |
| 106 }, |
| 107 |
| 108 /** |
| 109 * Returns the cached |values| in the specified time period. |
| 110 */ |
| 111 getValuesInternal_: function(startTime, stepSize, count) { |
| 112 var values = []; |
| 113 var nextPoint = 0; |
| 114 var currentValue = 0; |
| 115 var time = startTime; |
| 116 for (var i = 0; i < count; ++i) { |
| 117 while (nextPoint < this.dataPoints_.length && |
| 118 this.dataPoints_[nextPoint].time < time) { |
| 119 currentValue = this.dataPoints_[nextPoint].value; |
| 120 ++nextPoint; |
| 121 } |
| 122 values[i] = currentValue; |
| 123 time += stepSize; |
| 124 } |
| 125 return values; |
| 126 } |
| 127 }; |
| 128 |
| 129 /** |
| 130 * A single point in a data series. Each point has a time, in the form of |
| 131 * milliseconds since the Unix epoch, and a numeric value. |
| 132 * @constructor |
| 133 */ |
| 134 function DataPoint(time, value) { |
| 135 this.time = time; |
| 136 this.value = value; |
| 137 } |
| 138 |
| 139 return TimelineDataSeries; |
| 140 })(); |
| 141 |
| 142 var TimelineGraphView = (function() { |
| 143 // Maximum number of labels placed vertically along the sides of the graph. |
| 144 var MAX_VERTICAL_LABELS = 6; |
| 145 |
| 146 // Vertical spacing between labels and between the graph and labels. |
| 147 var LABEL_VERTICAL_SPACING = 4; |
| 148 // Horizontal spacing between vertically placed labels and the edges of the |
| 149 // graph. |
| 150 var LABEL_HORIZONTAL_SPACING = 3; |
| 151 // Horizintal spacing between two horitonally placed labels along the bottom |
| 152 // of the graph. |
| 153 //var LABEL_LABEL_HORIZONTAL_SPACING = 25; |
| 154 |
| 155 // Length of ticks, in pixels, next to y-axis labels. The x-axis only has |
| 156 // one set of labels, so it can use lines instead. |
| 157 var Y_AXIS_TICK_LENGTH = 10; |
| 158 |
| 159 var GRID_COLOR = '#CCC'; |
| 160 var TEXT_COLOR = '#000'; |
| 161 var BACKGROUND_COLOR = '#FFF'; |
| 162 |
| 163 var MAX_DECIMAL_PRECISION = 2; |
| 164 /** |
| 165 * @constructor |
| 166 */ |
| 167 function TimelineGraphView(divId, canvasId) { |
| 168 this.scrollbar_ = {position_: 0, range_: 0}; |
| 169 |
| 170 this.graphDiv_ = document.getElementById(divId); |
| 171 this.canvas_ = document.getElementById(canvasId); |
| 172 |
| 173 // Set the range and scale of the graph. Times are in milliseconds since |
| 174 // the Unix epoch. |
| 175 |
| 176 // All measurements we have must be after this time. |
| 177 this.startTime_ = 0; |
| 178 // The current rightmost position of the graph is always at most this. |
| 179 this.endTime_ = 1; |
| 180 |
| 181 this.graph_ = null; |
| 182 |
| 183 // Horizontal scale factor, in terms of milliseconds per pixel. |
| 184 this.scale_ = 1000; |
| 185 |
| 186 // Initialize the scrollbar. |
| 187 this.updateScrollbarRange_(true); |
| 188 } |
| 189 |
| 190 TimelineGraphView.prototype = { |
| 191 setScale: function(scale) { |
| 192 this.scale_ = scale; |
| 193 }, |
| 194 |
| 195 // Returns the total length of the graph, in pixels. |
| 196 getLength_: function() { |
| 197 var timeRange = this.endTime_ - this.startTime_; |
| 198 // Math.floor is used to ignore the last partial area, of length less |
| 199 // than this.scale_. |
| 200 return Math.floor(timeRange / this.scale_); |
| 201 }, |
| 202 |
| 203 /** |
| 204 * Returns true if the graph is scrolled all the way to the right. |
| 205 */ |
| 206 graphScrolledToRightEdge_: function() { |
| 207 return this.scrollbar_.position_ === this.scrollbar_.range_; |
| 208 }, |
| 209 |
| 210 /** |
| 211 * Update the range of the scrollbar. If |resetPosition| is true, also |
| 212 * sets the slider to point at the rightmost position and triggers a |
| 213 * repaint. |
| 214 */ |
| 215 updateScrollbarRange_: function(resetPosition) { |
| 216 var scrollbarRange = this.getLength_() - this.canvas_.width; |
| 217 if (scrollbarRange < 0) { |
| 218 scrollbarRange = 0; |
| 219 } |
| 220 |
| 221 // If we've decreased the range to less than the current scroll position, |
| 222 // we need to move the scroll position. |
| 223 if (this.scrollbar_.position_ > scrollbarRange) { |
| 224 resetPosition = true; |
| 225 } |
| 226 |
| 227 this.scrollbar_.range_ = scrollbarRange; |
| 228 if (resetPosition) { |
| 229 this.scrollbar_.position_ = scrollbarRange; |
| 230 this.repaint(); |
| 231 } |
| 232 }, |
| 233 |
| 234 /** |
| 235 * Sets the date range displayed on the graph, switches to the default |
| 236 * scale factor, and moves the scrollbar all the way to the right. |
| 237 */ |
| 238 setDateRange: function(startDate, endDate) { |
| 239 this.startTime_ = startDate.getTime(); |
| 240 this.endTime_ = endDate.getTime(); |
| 241 |
| 242 // Safety check. |
| 243 if (this.endTime_ <= this.startTime_) { |
| 244 this.startTime_ = this.endTime_ - 1; |
| 245 } |
| 246 |
| 247 this.updateScrollbarRange_(true); |
| 248 }, |
| 249 |
| 250 /** |
| 251 * Updates the end time at the right of the graph to be the current time. |
| 252 * Specifically, updates the scrollbar's range, and if the scrollbar is |
| 253 * all the way to the right, keeps it all the way to the right. Otherwise, |
| 254 * leaves the view as-is and doesn't redraw anything. |
| 255 */ |
| 256 updateEndDate: function(optDate) { |
| 257 this.endTime_ = optDate || (new Date()).getTime(); |
| 258 this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); |
| 259 }, |
| 260 |
| 261 getStartDate: function() { |
| 262 return new Date(this.startTime_); |
| 263 }, |
| 264 |
| 265 /** |
| 266 * Replaces the current TimelineDataSeries with |dataSeries|. |
| 267 */ |
| 268 setDataSeries: function(dataSeries) { |
| 269 // Simply recreates the Graph. |
| 270 this.graph_ = new Graph(); |
| 271 for (var i = 0; i < dataSeries.length; ++i) { |
| 272 this.graph_.addDataSeries(dataSeries[i]); |
| 273 } |
| 274 this.repaint(); |
| 275 }, |
| 276 |
| 277 /** |
| 278 * Adds |dataSeries| to the current graph. |
| 279 */ |
| 280 addDataSeries: function(dataSeries) { |
| 281 if (!this.graph_) { |
| 282 this.graph_ = new Graph(); |
| 283 } |
| 284 this.graph_.addDataSeries(dataSeries); |
| 285 this.repaint(); |
| 286 }, |
| 287 |
| 288 /** |
| 289 * Draws the graph on |canvas_|. |
| 290 */ |
| 291 repaint: function() { |
| 292 this.repaintTimerRunning_ = false; |
| 293 |
| 294 var width = this.canvas_.width; |
| 295 var height = this.canvas_.height; |
| 296 var context = this.canvas_.getContext('2d'); |
| 297 |
| 298 // Clear the canvas. |
| 299 context.fillStyle = BACKGROUND_COLOR; |
| 300 context.fillRect(0, 0, width, height); |
| 301 |
| 302 // Try to get font height in pixels. Needed for layout. |
| 303 var fontHeightString = context.font.match(/([0-9]+)px/)[1]; |
| 304 var fontHeight = parseInt(fontHeightString); |
| 305 |
| 306 // Safety check, to avoid drawing anything too ugly. |
| 307 if (fontHeightString.length === 0 || fontHeight <= 0 || |
| 308 fontHeight * 4 > height || width < 50) { |
| 309 return; |
| 310 } |
| 311 |
| 312 // Save current transformation matrix so we can restore it later. |
| 313 context.save(); |
| 314 |
| 315 // The center of an HTML canvas pixel is technically at (0.5, 0.5). This |
| 316 // makes near straight lines look bad, due to anti-aliasing. This |
| 317 // translation reduces the problem a little. |
| 318 context.translate(0.5, 0.5); |
| 319 |
| 320 // Figure out what time values to display. |
| 321 var position = this.scrollbar_.position_; |
| 322 // If the entire time range is being displayed, align the right edge of |
| 323 // the graph to the end of the time range. |
| 324 if (this.scrollbar_.range_ === 0) { |
| 325 position = this.getLength_() - this.canvas_.width; |
| 326 } |
| 327 var visibleStartTime = this.startTime_ + position * this.scale_; |
| 328 |
| 329 // Make space at the bottom of the graph for the time labels, and then |
| 330 // draw the labels. |
| 331 var textHeight = height; |
| 332 height -= fontHeight + LABEL_VERTICAL_SPACING; |
| 333 this.drawTimeLabels(context, width, height, textHeight, visibleStartTime); |
| 334 |
| 335 // Draw outline of the main graph area. |
| 336 context.strokeStyle = GRID_COLOR; |
| 337 context.strokeRect(0, 0, width - 1, height - 1); |
| 338 |
| 339 if (this.graph_) { |
| 340 // Layout graph and have them draw their tick marks. |
| 341 this.graph_.layout( |
| 342 width, height, fontHeight, visibleStartTime, this.scale_); |
| 343 this.graph_.drawTicks(context); |
| 344 |
| 345 // Draw the lines of all graphs, and then draw their labels. |
| 346 this.graph_.drawLines(context); |
| 347 this.graph_.drawLabels(context); |
| 348 } |
| 349 |
| 350 // Restore original transformation matrix. |
| 351 context.restore(); |
| 352 }, |
| 353 |
| 354 /** |
| 355 * Draw time labels below the graph. Takes in start time as an argument |
| 356 * since it may not be |startTime_|, when we're displaying the entire |
| 357 * time range. |
| 358 */ |
| 359 drawTimeLabels: function(context, width, height, textHeight, startTime) { |
| 360 // Draw the labels 1 minute apart. |
| 361 var timeStep = 1000 * 60; |
| 362 |
| 363 // Find the time for the first label. This time is a perfect multiple of |
| 364 // timeStep because of how UTC times work. |
| 365 var time = Math.ceil(startTime / timeStep) * timeStep; |
| 366 |
| 367 context.textBaseline = 'bottom'; |
| 368 context.textAlign = 'center'; |
| 369 context.fillStyle = TEXT_COLOR; |
| 370 context.strokeStyle = GRID_COLOR; |
| 371 |
| 372 // Draw labels and vertical grid lines. |
| 373 while (true) { |
| 374 var x = Math.round((time - startTime) / this.scale_); |
| 375 if (x >= width) { |
| 376 break; |
| 377 } |
| 378 var text = (new Date(time)).toLocaleTimeString(); |
| 379 context.fillText(text, x, textHeight); |
| 380 context.beginPath(); |
| 381 context.lineTo(x, 0); |
| 382 context.lineTo(x, height); |
| 383 context.stroke(); |
| 384 time += timeStep; |
| 385 } |
| 386 }, |
| 387 |
| 388 getDataSeriesCount: function() { |
| 389 if (this.graph_) { |
| 390 return this.graph_.dataSeries_.length; |
| 391 } |
| 392 return 0; |
| 393 }, |
| 394 |
| 395 hasDataSeries: function(dataSeries) { |
| 396 if (this.graph_) { |
| 397 return this.graph_.hasDataSeries(dataSeries); |
| 398 } |
| 399 return false; |
| 400 }, |
| 401 |
| 402 }; |
| 403 |
| 404 /** |
| 405 * A Graph is responsible for drawing all the TimelineDataSeries that have |
| 406 * the same data type. Graphs are responsible for scaling the values, laying |
| 407 * out labels, and drawing both labels and lines for its data series. |
| 408 */ |
| 409 var Graph = (function() { |
| 410 /** |
| 411 * @constructor |
| 412 */ |
| 413 function Graph() { |
| 414 this.dataSeries_ = []; |
| 415 |
| 416 // Cached properties of the graph, set in layout. |
| 417 this.width_ = 0; |
| 418 this.height_ = 0; |
| 419 this.fontHeight_ = 0; |
| 420 this.startTime_ = 0; |
| 421 this.scale_ = 0; |
| 422 |
| 423 // The lowest/highest values adjusted by the vertical label step size |
| 424 // in the displayed range of the graph. Used for scaling and setting |
| 425 // labels. Set in layoutLabels. |
| 426 this.min_ = 0; |
| 427 this.max_ = 0; |
| 428 |
| 429 // Cached text of equally spaced labels. Set in layoutLabels. |
| 430 this.labels_ = []; |
| 431 } |
| 432 |
| 433 /** |
| 434 * A Label is the label at a particular position along the y-axis. |
| 435 * @constructor |
| 436 */ |
| 437 /* |
| 438 function Label(height, text) { |
| 439 this.height = height; |
| 440 this.text = text; |
| 441 } |
| 442 */ |
| 443 |
| 444 Graph.prototype = { |
| 445 addDataSeries: function(dataSeries) { |
| 446 this.dataSeries_.push(dataSeries); |
| 447 }, |
| 448 |
| 449 hasDataSeries: function(dataSeries) { |
| 450 for (var i = 0; i < this.dataSeries_.length; ++i) { |
| 451 if (this.dataSeries_[i] === dataSeries) { |
| 452 return true; |
| 453 } |
| 454 } |
| 455 return false; |
| 456 }, |
| 457 |
| 458 /** |
| 459 * Returns a list of all the values that should be displayed for a given |
| 460 * data series, using the current graph layout. |
| 461 */ |
| 462 getValues: function(dataSeries) { |
| 463 if (!dataSeries.isVisible()) { |
| 464 return null; |
| 465 } |
| 466 return dataSeries.getValues(this.startTime_, this.scale_, this.width_); |
| 467 }, |
| 468 |
| 469 /** |
| 470 * Updates the graph's layout. In particular, both the max value and |
| 471 * label positions are updated. Must be called before calling any of the |
| 472 * drawing functions. |
| 473 */ |
| 474 layout: function(width, height, fontHeight, startTime, scale) { |
| 475 this.width_ = width; |
| 476 this.height_ = height; |
| 477 this.fontHeight_ = fontHeight; |
| 478 this.startTime_ = startTime; |
| 479 this.scale_ = scale; |
| 480 |
| 481 // Find largest value. |
| 482 var max = 0; |
| 483 var min = 0; |
| 484 for (var i = 0; i < this.dataSeries_.length; ++i) { |
| 485 var values = this.getValues(this.dataSeries_[i]); |
| 486 if (!values) { |
| 487 continue; |
| 488 } |
| 489 for (var j = 0; j < values.length; ++j) { |
| 490 if (values[j] > max) { |
| 491 max = values[j]; |
| 492 } else if (values[j] < min) { |
| 493 min = values[j]; |
| 494 } |
| 495 } |
| 496 } |
| 497 |
| 498 this.layoutLabels_(min, max); |
| 499 }, |
| 500 |
| 501 /** |
| 502 * Lays out labels and sets |max_|/|min_|, taking the time units into |
| 503 * consideration. |maxValue| is the actual maximum value, and |
| 504 * |max_| will be set to the value of the largest label, which |
| 505 * will be at least |maxValue|. Similar for |min_|. |
| 506 */ |
| 507 layoutLabels_: function(minValue, maxValue) { |
| 508 if (maxValue - minValue < 1024) { |
| 509 this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION); |
| 510 return; |
| 511 } |
| 512 |
| 513 // Find appropriate units to use. |
| 514 var units = ['', 'k', 'M', 'G', 'T', 'P']; |
| 515 // Units to use for labels. 0 is '1', 1 is K, etc. |
| 516 // We start with 1, and work our way up. |
| 517 var unit = 1; |
| 518 minValue /= 1024; |
| 519 maxValue /= 1024; |
| 520 while (units[unit + 1] && maxValue - minValue >= 1024) { |
| 521 minValue /= 1024; |
| 522 maxValue /= 1024; |
| 523 ++unit; |
| 524 } |
| 525 |
| 526 // Calculate labels. |
| 527 this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION); |
| 528 |
| 529 // Append units to labels. |
| 530 for (var i = 0; i < this.labels_.length; ++i) { |
| 531 this.labels_[i] += ' ' + units[unit]; |
| 532 } |
| 533 |
| 534 // Convert |min_|/|max_| back to unit '1'. |
| 535 this.min_ *= Math.pow(1024, unit); |
| 536 this.max_ *= Math.pow(1024, unit); |
| 537 }, |
| 538 |
| 539 /** |
| 540 * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the |
| 541 * maximum number of decimal digits allowed. The minimum allowed |
| 542 * difference between two adjacent labels is 10^-|maxDecimalDigits|. |
| 543 */ |
| 544 layoutLabelsBasic_: function(minValue, maxValue, maxDecimalDigits) { |
| 545 this.labels_ = []; |
| 546 var range = maxValue - minValue; |
| 547 // No labels if the range is 0. |
| 548 if (range === 0) { |
| 549 this.min_ = this.max_ = maxValue; |
| 550 return; |
| 551 } |
| 552 |
| 553 // The maximum number of equally spaced labels allowed. |fontHeight_| |
| 554 // is doubled because the top two labels are both drawn in the same |
| 555 // gap. |
| 556 var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING; |
| 557 |
| 558 // The + 1 is for the top label. |
| 559 var maxLabels = 1 + this.height_ / minLabelSpacing; |
| 560 if (maxLabels < 2) { |
| 561 maxLabels = 2; |
| 562 } else if (maxLabels > MAX_VERTICAL_LABELS) { |
| 563 maxLabels = MAX_VERTICAL_LABELS; |
| 564 } |
| 565 |
| 566 // Initial try for step size between conecutive labels. |
| 567 var stepSize = Math.pow(10, -maxDecimalDigits); |
| 568 // Number of digits to the right of the decimal of |stepSize|. |
| 569 // Used for formating label strings. |
| 570 var stepSizeDecimalDigits = maxDecimalDigits; |
| 571 |
| 572 // Pick a reasonable step size. |
| 573 while (true) { |
| 574 // If we use a step size of |stepSize| between labels, we'll need: |
| 575 // |
| 576 // Math.ceil(range / stepSize) + 1 |
| 577 // |
| 578 // labels. The + 1 is because we need labels at both at 0 and at |
| 579 // the top of the graph. |
| 580 |
| 581 // Check if we can use steps of size |stepSize|. |
| 582 if (Math.ceil(range / stepSize) + 1 <= maxLabels) { |
| 583 break; |
| 584 } |
| 585 // Check |stepSize| * 2. |
| 586 if (Math.ceil(range / (stepSize * 2)) + 1 <= maxLabels) { |
| 587 stepSize *= 2; |
| 588 break; |
| 589 } |
| 590 // Check |stepSize| * 5. |
| 591 if (Math.ceil(range / (stepSize * 5)) + 1 <= maxLabels) { |
| 592 stepSize *= 5; |
| 593 break; |
| 594 } |
| 595 stepSize *= 10; |
| 596 if (stepSizeDecimalDigits > 0) { |
| 597 --stepSizeDecimalDigits; |
| 598 } |
| 599 } |
| 600 |
| 601 // Set the min/max so it's an exact multiple of the chosen step size. |
| 602 this.max_ = Math.ceil(maxValue / stepSize) * stepSize; |
| 603 this.min_ = Math.floor(minValue / stepSize) * stepSize; |
| 604 |
| 605 // Create labels. |
| 606 for (var label = this.max_; label >= this.min_; label -= stepSize) { |
| 607 this.labels_.push(label.toFixed(stepSizeDecimalDigits)); |
| 608 } |
| 609 }, |
| 610 |
| 611 /** |
| 612 * Draws tick marks for each of the labels in |labels_|. |
| 613 */ |
| 614 drawTicks: function(context) { |
| 615 var x1; |
| 616 var x2; |
| 617 x1 = this.width_ - 1; |
| 618 x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH; |
| 619 |
| 620 context.fillStyle = GRID_COLOR; |
| 621 context.beginPath(); |
| 622 for (var i = 1; i < this.labels_.length - 1; ++i) { |
| 623 // The rounding is needed to avoid ugly 2-pixel wide anti-aliased |
| 624 // lines. |
| 625 var y = Math.round(this.height_ * i / (this.labels_.length - 1)); |
| 626 context.moveTo(x1, y); |
| 627 context.lineTo(x2, y); |
| 628 } |
| 629 context.stroke(); |
| 630 }, |
| 631 |
| 632 /** |
| 633 * Draws a graph line for each of the data series. |
| 634 */ |
| 635 drawLines: function(context) { |
| 636 // Factor by which to scale all values to convert them to a number from |
| 637 // 0 to height - 1. |
| 638 var scale = 0; |
| 639 var bottom = this.height_ - 1; |
| 640 if (this.max_) { |
| 641 scale = bottom / (this.max_ - this.min_); |
| 642 } |
| 643 |
| 644 // Draw in reverse order, so earlier data series are drawn on top of |
| 645 // subsequent ones. |
| 646 for (var i = this.dataSeries_.length - 1; i >= 0; --i) { |
| 647 var values = this.getValues(this.dataSeries_[i]); |
| 648 if (!values) { |
| 649 continue; |
| 650 } |
| 651 context.strokeStyle = this.dataSeries_[i].getColor(); |
| 652 context.beginPath(); |
| 653 for (var x = 0; x < values.length; ++x) { |
| 654 // The rounding is needed to avoid ugly 2-pixel wide anti-aliased |
| 655 // horizontal lines. |
| 656 context.lineTo( |
| 657 x, bottom - Math.round((values[x] - this.min_) * scale)); |
| 658 } |
| 659 context.stroke(); |
| 660 } |
| 661 }, |
| 662 |
| 663 /** |
| 664 * Draw labels in |labels_|. |
| 665 */ |
| 666 drawLabels: function(context) { |
| 667 if (this.labels_.length === 0) { |
| 668 return; |
| 669 } |
| 670 var x = this.width_ - LABEL_HORIZONTAL_SPACING; |
| 671 |
| 672 // Set up the context. |
| 673 context.fillStyle = TEXT_COLOR; |
| 674 context.textAlign = 'right'; |
| 675 |
| 676 // Draw top label, which is the only one that appears below its tick |
| 677 // mark. |
| 678 context.textBaseline = 'top'; |
| 679 context.fillText(this.labels_[0], x, 0); |
| 680 |
| 681 // Draw all the other labels. |
| 682 context.textBaseline = 'bottom'; |
| 683 var step = (this.height_ - 1) / (this.labels_.length - 1); |
| 684 for (var i = 1; i < this.labels_.length; ++i) { |
| 685 context.fillText(this.labels_[i], x, step * i); |
| 686 } |
| 687 } |
| 688 }; |
| 689 |
| 690 return Graph; |
| 691 })(); |
| 692 |
| 693 return TimelineGraphView; |
| 694 })(); |
OLD | NEW |