1 // Copyright 2013 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 * This visualizer displays the log in a timeline graph 7 * 8 * - Use HTML5 canvas 9 * - Can zoom in result by select time range 10 * - Display different levels of logs in different layers of canvases 11 * 12 */ 13 var CrosLogVisualizer = (function() { 14 'use strict'; 15 16 // HTML attributes of canvas 17 var LOG_VISUALIZER_CANVAS_CLASS = 'cros-log-analyzer-visualizer-canvas'; 18 var LOG_VISUALIZER_CANVAS_WIDTH = 980; 19 var LOG_VISUALIZER_CANVAS_HEIGHT = 100; 20 21 // Special HTML classes 22 var LOG_VISUALIZER_TIMELINE_ID = 'cros-log-analyzer-visualizer-timeline'; 23 var LOG_VISUALIZER_TIME_DISPLAY_CLASS = 24 'cros-log-analyzer-visualizer-time-display'; 25 var LOG_VISUALIZER_RESET_BTN_ID = 26 'cros-log-analyzer-visualizer-reset-btn'; 27 var LOG_VISUALIZER_TRACKING_LAYER_ID = 28 'cros-log-analyzer-visualizer-tracking-layer'; 29 30 /** 31 * Event level list 32 * This list is used for intialization of canvases. And the canvas 33 * with lowest priority should be created first. Hence the list is 34 * sorted in decreasing order. 35 */ 36 var LOG_EVENT_LEVEL_PRIORITY_LIST = { 37 'Unknown': 4, 38 'Warning': 2, 39 'Info': 3, 40 'Error': 1 41 }; 42 43 // Color mapping of different levels 44 var LOG_EVENT_COLORS_LIST = { 45 'Error': '#FF99A3', 46 'Warning': '#FAE5C3', 47 'Info': '#C3E3FA', 48 'Unknown': 'gray' 49 }; 50 51 /** 52 * @constructor 53 */ 54 function CrosLogVisualizer(logAnalyzer, containerID) { 55 /** 56 * Pass the LogAnalyzer in as a reference so the visualizer can 57 * synchrous with the log filter. 58 */ 59 this.logAnalyzer = logAnalyzer; 60 61 // If the data is initialized 62 this.dataIntialized = false; 63 // Stores all the log entries as events 64 this.events = []; 65 // A front layer that handles control events 66 this.trackingLayer = this.createTrackingLayer(); 67 68 // References to HTML elements 69 this.container = document.getElementById(containerID); 70 this.timeline = this.createTimeline(); 71 this.timeDisplay = this.createTimeDisplay(); 72 this.btnReset = this.createBtnReset(); 73 // Canvases 74 this.canvases = {}; 75 for (var level in LOG_EVENT_LEVEL_PRIORITY_LIST) { 76 this.canvases[level] = this.createCanvas(); 77 this.container.appendChild(this.canvases[level]); 78 } 79 80 // Append all the elements to the container 81 this.container.appendChild(this.timeline); 82 this.container.appendChild(this.timeDisplay); 83 this.container.appendChild(this.trackingLayer); 84 this.container.appendChild(this.btnReset); 85 86 this.container.addEventListener('webkitAnimationEnd', function() { 87 this.container.classList.remove('cros-log-analyzer-flash'); 88 }.bind(this), false); 89 } 90 91 CrosLogVisualizer.prototype = { 92 /** 93 * Called during the initialization of the View. Create a overlay 94 * DIV on top of the canvas that handles the mouse events 95 */ 96 createTrackingLayer: function() { 97 var trackingLayer = document.createElement('div'); 98 trackingLayer.setAttribute('id', LOG_VISUALIZER_TRACKING_LAYER_ID); 99 trackingLayer.addEventListener('mousemove', this.onHovered_.bind(this)); 100 trackingLayer.addEventListener('mousedown', this.onMouseDown_.bind(this)); 101 trackingLayer.addEventListener('mouseup', this.onMouseUp_.bind(this)); 102 return trackingLayer; 103 }, 104 105 /** 106 * This function is called during the initialization of the view. 107 * It creates the timeline that moves along with the mouse on canvas. 108 * When user click, a rectangle can be dragged out to select the range 109 * to zoom. 110 */ 111 createTimeline: function() { 112 var timeline = document.createElement('div'); 113 timeline.setAttribute('id', LOG_VISUALIZER_TIMELINE_ID); 114 timeline.style.height = LOG_VISUALIZER_CANVAS_HEIGHT + 'px'; 115 timeline.addEventListener('mousedown', function(event) { return false; }); 116 return timeline; 117 }, 118 119 /** 120 * This function is called during the initialization of the view. 121 * It creates a time display that moves with the timeline 122 */ 123 createTimeDisplay: function() { 124 var timeDisplay = document.createElement('p'); 125 timeDisplay.className = LOG_VISUALIZER_TIME_DISPLAY_CLASS; 126 timeDisplay.style.top = LOG_VISUALIZER_CANVAS_HEIGHT + 'px'; 127 return timeDisplay; 128 }, 129 130 /** 131 * Called during the initialization of the View. Create a button that 132 * resets the canvas to initial status (without zoom) 133 */ 134 createBtnReset: function() { 135 var btnReset = document.createElement('input'); 136 btnReset.setAttribute('type', 'button'); 137 btnReset.setAttribute('value', 'Reset'); 138 btnReset.setAttribute('id', LOG_VISUALIZER_RESET_BTN_ID); 139 btnReset.addEventListener('click', this.reset.bind(this)); 140 return btnReset; 141 }, 142 143 /** 144 * Called during the initialization of the View. Create a empty canvas 145 * that visualizes log when the data is ready 146 */ 147 createCanvas: function() { 148 var canvas = document.createElement('canvas'); 149 canvas.width = LOG_VISUALIZER_CANVAS_WIDTH; 150 canvas.height = LOG_VISUALIZER_CANVAS_HEIGHT; 151 canvas.className = LOG_VISUALIZER_CANVAS_CLASS; 152 return canvas; 153 }, 154 155 /** 156 * Returns the context of corresponding canvas based on level 157 */ 158 getContext: function(level) { 159 return this.canvases[level].getContext('2d'); 160 }, 161 162 /** 163 * Erases everything from all the canvases 164 */ 165 clearCanvas: function() { 166 for (var level in LOG_EVENT_LEVEL_PRIORITY_LIST) { 167 var ctx = this.getContext(level); 168 ctx.clearRect(0, 0, LOG_VISUALIZER_CANVAS_WIDTH, 169 LOG_VISUALIZER_CANVAS_HEIGHT); 170 } 171 }, 172 173 /** 174 * Initializes the parameters needed for drawing: 175 * - lower/upperBound: Time range (Events out of range will be skipped) 176 * - totalDuration: The length of time range 177 * - unitDuration: The unit time length per pixel 178 */ 179 initialize: function() { 180 if (this.events.length == 0) 181 return; 182 this.dragMode = false; 183 this.dataIntialized = true; 184 this.events.sort(this.compareTime); 185 this.lowerBound = this.events[0].time; 186 this.upperBound = this.events[this.events.length - 1].time; 187 this.totalDuration = Math.abs(this.upperBound.getTime() - 188 this.lowerBound.getTime()); 189 this.unitDuration = this.totalDuration / LOG_VISUALIZER_CANVAS_WIDTH; 190 }, 191 192 /** 193 * CSS3 fadeIn/fadeOut effects 194 */ 195 flashEffect: function() { 196 this.container.classList.add('cros-log-analyzer-flash'); 197 }, 198 199 /** 200 * Reset the canvas to the initial time range 201 * Redraw everything on the canvas 202 * Fade in/out effects while redrawing 203 */ 204 reset: function() { 205 // Reset all the parameters as initial 206 this.initialize(); 207 // Reset the visibility of the entries in the log table 208 this.logAnalyzer.filterLog(); 209 this.flashEffect(); 210 }, 211 212 /** 213 * A wrapper function for drawing 214 */ 215 drawEvents: function() { 216 if (this.events.length == 0) 217 return; 218 for (var i in this.events) { 219 this.drawEvent(this.events[i]); 220 } 221 }, 222 223 /** 224 * The main function that handles drawing on the canvas. 225 * Every event is represented as a vertical line. 226 */ 227 drawEvent: function(event) { 228 if (!event.visibility) { 229 // Skip hidden events 230 return; 231 } 232 var ctx = this.getContext(event.level); 233 ctx.beginPath(); 234 // Get the x-coordinate of the line 235 var startPosition = this.getPosition(event.time); 236 if (startPosition != this.old) { 237 this.old = startPosition; 238 } 239 ctx.rect(startPosition, 0, 2, LOG_VISUALIZER_CANVAS_HEIGHT); 240 // Get the color of the line 241 ctx.fillStyle = LOG_EVENT_COLORS_LIST[event.level]; 242 ctx.fill(); 243 ctx.closePath(); 244 }, 245 246 /** 247 * This function is called every time the graph is zoomed. 248 * It recalculates all the parameters based on the distance and direction 249 * of dragging. 250 */ 251 reCalculate: function() { 252 if (this.dragDistance >= 0) { 253 // if user drags to right 254 this.upperBound = new Date((this.timelineLeft + this.dragDistance) * 255 this.unitDuration + this.lowerBound.getTime()); 256 this.lowerBound = new Date(this.timelineLeft * this.unitDuration + 257 this.lowerBound.getTime()); 258 } else { 259 // if user drags to left 260 this.upperBound = new Date(this.timelineLeft * this.unitDuration + 261 this.lowerBound.getTime()); 262 this.lowerBound = new Date((this.timelineLeft + this.dragDistance) * 263 this.unitDuration + this.lowerBound.getTime()); 264 } 265 this.totalDuration = this.upperBound.getTime() - 266 this.lowerBound.getTime(); 267 this.unitDuration = this.totalDuration / LOG_VISUALIZER_CANVAS_WIDTH; 268 }, 269 270 /** 271 * Check if the time of a event is out of bound 272 */ 273 isOutOfBound: function(event) { 274 return event.time.getTime() < this.lowerBound.getTime() || 275 event.time.getTime() > this.upperBound.getTime(); 276 }, 277 278 /** 279 * This function returns the offset on x-coordinate of canvas based on 280 * the time 281 */ 282 getPosition: function(time) { 283 return (time.getTime() - this.lowerBound.getTime()) / this.unitDuration; 284 }, 285 286 /** 287 * This function updates the events array and refresh the canvas. 288 */ 289 updateEvents: function(newEvents) { 290 this.events.length = 0; 291 for (var i in newEvents) { 292 this.events.push(newEvents[i]); 293 } 294 if (!this.dataIntialized) { 295 this.initialize(); 296 } 297 this.clearCanvas(); 298 this.drawEvents(); 299 }, 300 301 /** 302 * This is a helper function that returns the time object based on the 303 * offset of x-coordinate on the canvs. 304 */ 305 getOffsetTime: function(offset) { 306 return new Date(this.lowerBound.getTime() + offset * this.unitDuration); 307 }, 308 309 /** 310 * This function is triggered when the hovering event is detected 311 * When the mouse is hovering we have two control mode: 312 * - If it is in drag mode, we need to resize the width of the timeline 313 * - If not, we need to move the timeline and time display to the 314 * x-coordinate position of the mouse 315 */ 316 onHovered_: function(event) { 317 var offsetX = event.offsetX; 318 if (this.lastOffsetX == offsetX) { 319 // If the mouse does not move, we just skip the event 320 return; 321 } 322 323 if (this.dragMode == true) { 324 // If the mouse is in drag mode 325 this.dragDistance = offsetX - this.timelineLeft; 326 if (this.dragDistance >= 0) { 327 // If the mouse is moving right 328 this.timeline.style.width = this.dragDistance + 'px'; 329 } else { 330 // If the mouse is moving left 331 this.timeline.style.width = -this.dragDistance + 'px'; 332 this.timeline.style.left = offsetX + 'px'; 333 } 334 } else { 335 // If the mouse is not in drag mode we just move the timeline 336 this.timeline.style.width = '2px'; 337 this.timeline.style.left = offsetX + 'px'; 338 } 339 340 // update time display 341 this.timeDisplay.style.left = offsetX + 'px'; 342 this.timeDisplay.textContent = 343 this.getOffsetTime(offsetX).toTimeString().substr(0, 8); 344 // update the last offset 345 this.lastOffsetX = offsetX; 346 }, 347 348 /** 349 * This function is the handler for the onMouseDown event on the canvas 350 */ 351 onMouseDown_: function(event) { 352 // Enter drag mode which let user choose a time range to zoom in 353 this.dragMode = true; 354 this.timelineLeft = event.offsetX; 355 // Create a duration display to indicate the duration of range. 356 this.timeDurationDisplay = this.createTimeDisplay(); 357 this.container.appendChild(this.timeDurationDisplay); 358 }, 359 360 /** 361 * This function is the handler for the onMouseUp event on the canvas 362 */ 363 onMouseUp_: function(event) { 364 // Remove the duration display 365 this.container.removeChild(this.timeDurationDisplay); 366 // End the drag mode 367 this.dragMode = false; 368 // Recalculate the pamameter based on the range user select 369 this.reCalculate(); 370 // Filter the log table and hide the entries that are not in the range 371 this.logAnalyzer.filterLog(); 372 }, 373 }; 374 375 return CrosLogVisualizer; 376 })(); 377