// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This visualizer displays the log in a timeline graph * * - Use HTML5 canvas * - Can zoom in result by select time range * - Display different levels of logs in different layers of canvases * */ var CrosLogVisualizer = (function() { 'use strict'; // HTML attributes of canvas var LOG_VISUALIZER_CANVAS_CLASS = 'cros-log-visualizer-visualizer-canvas'; var LOG_VISUALIZER_CANVAS_WIDTH = 980; var LOG_VISUALIZER_CANVAS_HEIGHT = 100; // Special HTML classes var LOG_VISUALIZER_TIMELINE_ID = 'cros-log-visualizer-visualizer-timeline'; var LOG_VISUALIZER_TIME_DISPLAY_CLASS = 'cros-log-visualizer-visualizer-time-display'; var LOG_VISUALIZER_RESET_BTN_ID = 'cros-log-visualizer-visualizer-reset-btn'; var LOG_VISUALIZER_TRACKING_LAYER_ID = 'cros-log-visualizer-visualizer-tracking-layer'; /** * Event level list * This list is used for intialization of canvases. And the canvas * with lowest priority should be created first. Hence the list is * sorted in decreasing order. */ var LOG_EVENT_LEVEL_PRIORITY_LIST = { 'Unknown': 4, 'Warning': 2, 'Info': 3, 'Error': 1 }; // Color mapping of different levels var LOG_EVENT_COLORS_LIST = { 'Error': '#FF99A3', 'Warning': '#FAE5C3', 'Info': '#C3E3FA', 'Unknown': 'gray' }; /** * @constructor */ function CrosLogVisualizer(logVisualizer, containerID) { /** * Pass the LogVisualizer in as a reference so the visualizer can * synchrous with the log filter. */ this.logVisualizer = logVisualizer; // If the data is initialized this.dataIntialized = false; // Stores all the log entries as events this.events = []; // A front layer that handles control events this.trackingLayer = this.createTrackingLayer(); // References to HTML elements this.container = document.getElementById(containerID); this.timeline = this.createTimeline(); this.timeDisplay = this.createTimeDisplay(); this.btnReset = this.createBtnReset(); // Canvases this.canvases = {}; for (var level in LOG_EVENT_LEVEL_PRIORITY_LIST) { this.canvases[level] = this.createCanvas(); this.container.appendChild(this.canvases[level]); } // Append all the elements to the container this.container.appendChild(this.timeline); this.container.appendChild(this.timeDisplay); this.container.appendChild(this.trackingLayer); this.container.appendChild(this.btnReset); this.container.addEventListener('webkitAnimationEnd', function() { this.container.classList.remove('cros-log-visualizer-flash'); }.bind(this), false); } CrosLogVisualizer.prototype = { /** * Called during the initialization of the View. Create a overlay * DIV on top of the canvas that handles the mouse events */ createTrackingLayer: function() { var trackingLayer = document.createElement('div'); trackingLayer.setAttribute('id', LOG_VISUALIZER_TRACKING_LAYER_ID); trackingLayer.addEventListener('mousemove', this.onHovered_.bind(this)); trackingLayer.addEventListener('mousedown', this.onMouseDown_.bind(this)); trackingLayer.addEventListener('mouseup', this.onMouseUp_.bind(this)); return trackingLayer; }, /** * This function is called during the initialization of the view. * It creates the timeline that moves along with the mouse on canvas. * When user click, a rectangle can be dragged out to select the range * to zoom. */ createTimeline: function() { var timeline = document.createElement('div'); timeline.setAttribute('id', LOG_VISUALIZER_TIMELINE_ID); timeline.style.height = LOG_VISUALIZER_CANVAS_HEIGHT + 'px'; timeline.addEventListener('mousedown', function(event) { return false; }); return timeline; }, /** * This function is called during the initialization of the view. * It creates a time display that moves with the timeline */ createTimeDisplay: function() { var timeDisplay = document.createElement('p'); timeDisplay.className = LOG_VISUALIZER_TIME_DISPLAY_CLASS; timeDisplay.style.top = LOG_VISUALIZER_CANVAS_HEIGHT + 'px'; return timeDisplay; }, /** * Called during the initialization of the View. Create a button that * resets the canvas to initial status (without zoom) */ createBtnReset: function() { var btnReset = document.createElement('input'); btnReset.setAttribute('type', 'button'); btnReset.setAttribute('value', 'Reset'); btnReset.setAttribute('id', LOG_VISUALIZER_RESET_BTN_ID); btnReset.addEventListener('click', this.reset.bind(this)); return btnReset; }, /** * Called during the initialization of the View. Create a empty canvas * that visualizes log when the data is ready */ createCanvas: function() { var canvas = document.createElement('canvas'); canvas.width = LOG_VISUALIZER_CANVAS_WIDTH; canvas.height = LOG_VISUALIZER_CANVAS_HEIGHT; canvas.className = LOG_VISUALIZER_CANVAS_CLASS; return canvas; }, /** * Returns the context of corresponding canvas based on level */ getContext: function(level) { return this.canvases[level].getContext('2d'); }, /** * Erases everything from all the canvases */ clearCanvas: function() { for (var level in LOG_EVENT_LEVEL_PRIORITY_LIST) { var ctx = this.getContext(level); ctx.clearRect(0, 0, LOG_VISUALIZER_CANVAS_WIDTH, LOG_VISUALIZER_CANVAS_HEIGHT); } }, /** * Initializes the parameters needed for drawing: * - lower/upperBound: Time range (Events out of range will be skipped) * - totalDuration: The length of time range * - unitDuration: The unit time length per pixel */ initialize: function() { if (this.events.length == 0) return; this.dragMode = false; this.dataIntialized = true; this.events.sort(this.compareTime); this.lowerBound = this.events[0].time; this.upperBound = this.events[this.events.length - 1].time; this.totalDuration = Math.abs(this.upperBound.getTime() - this.lowerBound.getTime()); this.unitDuration = this.totalDuration / LOG_VISUALIZER_CANVAS_WIDTH; }, /** * CSS3 fadeIn/fadeOut effects */ flashEffect: function() { this.container.classList.add('cros-log-visualizer-flash'); }, /** * Reset the canvas to the initial time range * Redraw everything on the canvas * Fade in/out effects while redrawing */ reset: function() { // Reset all the parameters as initial this.initialize(); // Reset the visibility of the entries in the log table this.logVisualizer.filterLog(); this.flashEffect(); }, /** * A wrapper function for drawing */ drawEvents: function() { if (this.events.length == 0) return; for (var i in this.events) { this.drawEvent(this.events[i]); } }, /** * The main function that handles drawing on the canvas. * Every event is represented as a vertical line. */ drawEvent: function(event) { if (!event.visibility) { // Skip hidden events return; } var ctx = this.getContext(event.level); ctx.beginPath(); // Get the x-coordinate of the line var startPosition = this.getPosition(event.time); if (startPosition != this.old) { this.old = startPosition; } ctx.rect(startPosition, 0, 2, LOG_VISUALIZER_CANVAS_HEIGHT); // Get the color of the line ctx.fillStyle = LOG_EVENT_COLORS_LIST[event.level]; ctx.fill(); ctx.closePath(); }, /** * This function is called every time the graph is zoomed. * It recalculates all the parameters based on the distance and direction * of dragging. */ reCalculate: function() { if (this.dragDistance >= 0) { // if user drags to right this.upperBound = new Date((this.timelineLeft + this.dragDistance) * this.unitDuration + this.lowerBound.getTime()); this.lowerBound = new Date(this.timelineLeft * this.unitDuration + this.lowerBound.getTime()); } else { // if user drags to left this.upperBound = new Date(this.timelineLeft * this.unitDuration + this.lowerBound.getTime()); this.lowerBound = new Date((this.timelineLeft + this.dragDistance) * this.unitDuration + this.lowerBound.getTime()); } this.totalDuration = this.upperBound.getTime() - this.lowerBound.getTime(); this.unitDuration = this.totalDuration / LOG_VISUALIZER_CANVAS_WIDTH; }, /** * Check if the time of a event is out of bound */ isOutOfBound: function(event) { return event.time.getTime() < this.lowerBound.getTime() || event.time.getTime() > this.upperBound.getTime(); }, /** * This function returns the offset on x-coordinate of canvas based on * the time */ getPosition: function(time) { return (time.getTime() - this.lowerBound.getTime()) / this.unitDuration; }, /** * This function updates the events array and refresh the canvas. */ updateEvents: function(newEvents) { this.events.length = 0; for (var i in newEvents) { this.events.push(newEvents[i]); } if (!this.dataIntialized) { this.initialize(); } this.clearCanvas(); this.drawEvents(); }, /** * This is a helper function that returns the time object based on the * offset of x-coordinate on the canvs. */ getOffsetTime: function(offset) { return new Date(this.lowerBound.getTime() + offset * this.unitDuration); }, /** * This function is triggered when the hovering event is detected * When the mouse is hovering we have two control mode: * - If it is in drag mode, we need to resize the width of the timeline * - If not, we need to move the timeline and time display to the * x-coordinate position of the mouse */ onHovered_: function(event) { var offsetX = event.offsetX; if (this.lastOffsetX == offsetX) { // If the mouse does not move, we just skip the event return; } if (this.dragMode == true) { // If the mouse is in drag mode this.dragDistance = offsetX - this.timelineLeft; if (this.dragDistance >= 0) { // If the mouse is moving right this.timeline.style.width = this.dragDistance + 'px'; } else { // If the mouse is moving left this.timeline.style.width = -this.dragDistance + 'px'; this.timeline.style.left = offsetX + 'px'; } } else { // If the mouse is not in drag mode we just move the timeline this.timeline.style.width = '2px'; this.timeline.style.left = offsetX + 'px'; } // update time display this.timeDisplay.style.left = offsetX + 'px'; this.timeDisplay.textContent = this.getOffsetTime(offsetX).toTimeString().substr(0, 8); // update the last offset this.lastOffsetX = offsetX; }, /** * This function is the handler for the onMouseDown event on the canvas */ onMouseDown_: function(event) { // Enter drag mode which let user choose a time range to zoom in this.dragMode = true; this.timelineLeft = event.offsetX; // Create a duration display to indicate the duration of range. this.timeDurationDisplay = this.createTimeDisplay(); this.container.appendChild(this.timeDurationDisplay); }, /** * This function is the handler for the onMouseUp event on the canvas */ onMouseUp_: function(event) { // Remove the duration display this.container.removeChild(this.timeDurationDisplay); // End the drag mode this.dragMode = false; // Recalculate the pamameter based on the range user select this.reCalculate(); // Filter the log table and hide the entries that are not in the range this.logVisualizer.filterLog(); }, }; return CrosLogVisualizer; })();