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