| <!-- |
| This in an HTML Import-able file that contains the definition |
| of the following elements: |
| |
| <commits-table-d3-sk> |
| |
| This uses d3 to draw the commit table. Unlike previous versions which used a table and basically |
| drew the table row by row, this implementation draws it column by column. Because filtering |
| happens on a task spec (column) basis, drawing it column by column results in simpler code. |
| |
| D3 is much much faster than Polymer for this type of data visualization, although this element |
| still offers a Polymer interface. If any of the inputs change, the table will be redrawn. |
| |
| To use this file import it: |
| |
| <link href="/res/imp/commits-table-d3-sk" rel="import" /> |
| |
| Usage: |
| |
| <commits-table-d3-sk></commits-table-d3-sk> |
| |
| Properties: |
| // inputs |
| task_details: Object, a map of commit hash to an object that has the task results by task spec. |
| task_specs: Object, a map of the task spec names to an object that has, among other things, category, |
| subcategory, and comments. |
| tasks: Object, a map of the task spec names to an object that maps task IDs to task results. |
| categories: Object, a map of the task spec categories to an object that has the subcategories and |
| the colspan (total number of included task specs). |
| category_list: Array<String>, an array of the task spec category names. |
| commits: Array<Object>, the commit objects, in chronological detail. |
| commits_map: Object, a map of commit hash to commit objects. |
| logged_in: Boolean, if the links should be for internal or external pages. |
| relanded_map: Object, a map of a commit hash that was relanded to the commit object that relands it. |
| repo: String, the current repo. Used to direct comments to the right place. |
| repo_base: The base URL for commits. Commit hashes will be appended to this. |
| reverted_map: Object, a map of a commit hash that was reverted to the commit object that reverts it. |
| swarming_url: String, the URL of the Swarming server. |
| task_scheduler_url: String, the URL of the Task Scheduler. |
| |
| Methods: |
| None. |
| |
| Events: |
| None. |
| --> |
| <script src="/res/imp/bower_components/d3/d3.min.js"></script> |
| |
| <link rel="import" href="/res/imp/bower_components/iron-flex-layout/iron-flex-layout-classes.html"> |
| <link rel="import" href="/res/imp/bower_components/iron-icons/iron-icons.html"> |
| <link rel="import" href="/res/imp/bower_components/iron-icons/communication-icons.html"> |
| <link rel="import" href="/res/imp/bower_components/iron-icons/image-icons.html"> |
| <link rel="import" href="/res/imp/bower_components/paper-dialog/paper-dialog.html"> |
| |
| <link rel="import" href="/res/common/imp/styles-sk.html"> |
| |
| <link rel="import" href="task-popup-sk.html"> |
| <link rel="import" href="taskspec-popup-sk.html"> |
| <link rel="import" href="commit-popup-sk.html"> |
| |
| <link rel="stylesheet" href="commits-table-d3.css"> |
| <dom-module id="commits-table-d3-sk"> |
| <template> |
| <style include="iron-flex styles-sk"> |
| #commits { |
| min-height: 780px; |
| min-width: 50px; |
| } |
| #tasks { |
| border: 1px solid blue; |
| min-height: 400px; |
| } |
| #legend { |
| border-right: 1px solid black; |
| font-size: 10px; |
| height: 68px; |
| } |
| #table { |
| /* 80 pixels of header and 35 * 20px of commits. |
| It is important to preallocate this size, otherwise the flexbox layout |
| behaves strangely (i.e. wrong) when the data is first added. */ |
| min-height: 780px; |
| } |
| iframe { |
| border: 0; |
| } |
| </style> |
| |
| <div id="container" class="horizontal layout"> |
| <div id="commits"> |
| <div id="legend"> |
| <iron-icon class="tiny" icon="communication:chat"></iron-icon> comment<br/> |
| <iron-icon class="tiny" icon="image:texture"></iron-icon> flaky<br/> |
| <iron-icon class="tiny" icon="icons:block"></iron-icon> ignore failure<br/> |
| <iron-icon class="tiny fg-failure" icon="icons:undo"></iron-icon> reverted<br/> |
| <iron-icon class="tiny fg-success" icon="icons:redo"></iron-icon> relanded<br/> |
| </div> |
| </div> |
| <div id="table" class="flex horizontal layout"></div> |
| </div> |
| |
| <div id="infoDialog"></div> |
| |
| </template> |
| <script> |
| (function(){ |
| var TASK_HEIGHT = 20; |
| Polymer({ |
| is: 'commits-table-d3-sk', |
| properties: { |
| // inputs from data source to render. |
| task_specs: { |
| type: Object, |
| }, |
| task_details: { |
| type: Object, |
| }, |
| tasks: { |
| type: Object, |
| }, |
| categories: { |
| type: Object, |
| }, |
| category_list: { |
| type: Array, |
| }, |
| commit_label: { |
| type: String, |
| }, |
| commits: { |
| type: Array, |
| }, |
| commits_map: { |
| type: Object, |
| }, |
| logged_in: { |
| type: Boolean, |
| }, |
| relanded_map: { |
| type: Object, |
| }, |
| repo: { |
| type: String, |
| }, |
| repo_base: { |
| type: String, |
| }, |
| reverted_map: { |
| type: Object, |
| }, |
| swarming_url: { |
| type: String, |
| }, |
| time_points: { |
| type: Object, |
| }, |
| task_scheduler_url: { |
| type: String, |
| }, |
| |
| // outputs |
| drawing: { |
| type: Boolean, |
| notify: true, |
| value: false, |
| } |
| }, |
| observers: [ |
| "redraw(task_specs, task_details, categories, category_list, commit_label, commits, commits_map, relanded_map, repo, reverted_map, time_points)", |
| ], |
| |
| ready: function() { |
| // We redraw, so as to try to better fit the screen size. |
| d3.select(window).on('resize', this.redraw.bind(this)); |
| this.scopeSubtree(this.$.container, true); |
| }, |
| |
| redraw:function() { |
| // This gets called any time one of the values changes. Since the values are |
| // updated simultaneously, we don't want to try to draw the table many times at the |
| // same time, so we debounce it. No timeout on the debounce because all the this.set |
| // calls will happen before a paint call, so this just basically collates all the requests. |
| this.debounce("redraw-commits-table", function(){ |
| this._redraw(); |
| }.bind(this)); |
| }, |
| |
| _redraw:function() { |
| this.set("drawing", true); |
| console.time("d3 rendering"); |
| |
| // Display pictures instead of tasks if there aren't any to display. |
| var iframe = null; |
| for (var i = 0; i < this.$.table.childNodes.length; i++) { |
| var child = this.$.table.childNodes[i]; |
| if (child.tagName == "IFRAME") { |
| iframe = child; |
| break; |
| } |
| } |
| if (this.category_list.length === 0) { |
| if (!iframe) { |
| iframe = document.createElement("iframe"); |
| iframe.src = "https://clients3.google.com/cast/chromecast/home?slideshow-period=300"; |
| this.$.table.appendChild(iframe); |
| } |
| // Images are 1920x1080. Scale to fit the size of the div. |
| srcWidth = 1920; |
| srcHeight = 1080; |
| var scale = Math.min(this.$.table.clientWidth / srcWidth, this.$.table.clientHeight / srcHeight); |
| iframe.style.width = (srcWidth * scale) + "px"; |
| iframe.style.height = (srcHeight * scale) + "px"; |
| } else { |
| if (iframe) { |
| this.$.table.removeChild(iframe); |
| } |
| } |
| |
| this._drawCommitMessages(this.commits, this.relanded_map, this.reverted_map, this.time_points); |
| this._drawCategories(this.category_list, this.categories); |
| this._drawSubCategories(this.categories); |
| this._drawTaskSpecColumns(this.categories, this.task_specs, this.commits.length); |
| this._drawTasks(this.commits, this.task_details); |
| console.timeEnd("d3 rendering"); |
| this.set("drawing", false); |
| }, |
| |
| // Draw all of the commit divs that are on the left. |
| _drawCommitMessages: function(commits, relanded_map, reverted_map, time_points){ |
| var openCommitDialog = this._openCommitDialog.bind(this); |
| var commitLabel = this.commit_label; |
| // It's easiest to delete all .commits and redraw them. Normally, we would only update |
| // the ones we want, but since we have a composed element and there aren't very many |
| // commits, starting from scratch is simplest. |
| d3.select(this.$.commits).selectAll(".commit").remove(); |
| var data = d3.select(this.$.commits).selectAll(".commit") |
| .data(commits, function(commit, i){ |
| commit.index = i; |
| return commit.hash; |
| }); |
| // Create a commit div for each commit with id "patch_storage + codereview_number". |
| // Eg: "gerrit12345" for Gerrit. |
| // For posterity: Element selectors choke if an id starts with a number, thus the |
| // patch_storage prefix helps with that. |
| var newCommits = data |
| .enter() |
| .append("div") |
| .classed("commit",true) |
| .attr("id", function(commit){ |
| return commit.patchStorage + commit.issue; |
| }) |
| .on("tap", function(commit) { |
| // Stop the propogation so we don't immediately hide the popup we show. |
| d3.event.stopPropagation(); |
| openCommitDialog(commit); |
| }); |
| // Sort the commits to be in the order we are presented with them. |
| data.order(); |
| // Set the mouseover to be the opposite of what the label says. |
| newCommits |
| .attr("title", function(commit) { |
| if (commitLabel == "author") { |
| return commit.shortSubject; |
| } |
| return commit.author; |
| }); |
| |
| // Add a spacer to every commit where the time bubble could be. This allows all |
| // the bubbles and non-bubble commits to line up vertically. |
| var spacers = newCommits |
| .insert("div") |
| .classed("time-spacer",true); |
| |
| spacers.each(function(commit) { |
| var point = time_points[commit.hash]; |
| if (point) { |
| d3.select(this) |
| .append("span") |
| .classed("time-underline", true); |
| d3.select(this) |
| .insert("span") |
| .classed("time", true) |
| .attr("title", point.label) |
| .text(point.label); |
| } |
| }); |
| |
| // Insert a span with the commit label. |
| newCommits |
| .insert("span") |
| .classed("author", function(){ |
| return commitLabel == "author"; |
| }) |
| .classed("subject", function(){ |
| return commitLabel == "subject"; |
| }) |
| .text(function(commit) { |
| if (commitLabel == "author") { |
| return commit.shortAuthor || "[no author]"; |
| } |
| return commit.shortSubject || "[no subject]"; |
| }); |
| |
| newCommits |
| .insert("div") |
| .classed("flex", true); |
| newCommits |
| .insert("span") |
| .classed("commit-comment", true); |
| newCommits |
| .insert("span") |
| .classed("commit-ignore", true); |
| newCommits |
| .insert("span") |
| .classed("commit-revert", true); |
| newCommits |
| .insert("span") |
| .classed("commit-reland", true); |
| |
| // On the other side, conditionally draw iron icons for comments, relands, reverts. |
| this._addIronIcon(newCommits.select(".commit-comment"), "communication:chat", "tiny", function(commit){ |
| return commit.comments && commit.comments.length > 0; |
| }); |
| this._addIronIcon(newCommits.select(".commit-ignore"), "icons:block", "tiny", function(commit){ |
| return commit.ignoreFailure; |
| }); |
| var reverts = this._addIronIcon(newCommits.select(".commit-revert"), "icons:undo", "tiny fg-failure", function(commit){ |
| var reverterCommit = reverted_map[commit.hash]; |
| return (reverterCommit !== undefined && reverterCommit.timestamp > commit.timestamp); |
| }); |
| reverts.on("mouseover", function(commit){ |
| var reverterIssue = reverted_map[commit.hash].issue; |
| d3.select("#"+commit.patchStorage+reverterIssue).classed("bg-failure", true); |
| }) |
| reverts.on("mouseleave", function(commit){ |
| d3.selectAll(".commit.bg-failure").classed("bg-failure", false); |
| }) |
| var relands = this._addIronIcon(newCommits.select(".commit-reland"), "icons:redo", "tiny fg-success", function(commit){ |
| var relanderCommit = relanded_map[commit.hash]; |
| return (relanderCommit !== undefined && relanderCommit.timestamp > commit.timestamp); |
| }); |
| relands.on("mouseover", function(commit){ |
| var relanderIssue = relanded_map[commit.hash].issue; |
| d3.select("#"+commit.patchStorage+relanderIssue).classed("bg-success", true); |
| }); |
| relands.on("mouseleave", function(commit){ |
| d3.selectAll(".commit.bg-success").classed("bg-success", false); |
| }); |
| }, |
| |
| // Adds iron icons to everything in the selection if shouldAddIcon returns truthy. It |
| // returns all icons that were added. |
| _addIronIcon: function(selection, icon, classes, shouldAddIcon) { |
| return selection |
| .html(function(commit){ |
| // The Polymer templates will pick this up and make it a real iron-icon element. |
| // However, if you try to do .insert("iron-icon")... that does not work. |
| if (shouldAddIcon === undefined || shouldAddIcon(commit)) { |
| return "<iron-icon class=\""+classes+"\" icon=\"" + icon + "\"></iron-icon>"; |
| } |
| }); |
| }, |
| |
| // Draw all of the divs for the categories, including the header. |
| _drawCategories: function(category_list, categories){ |
| var data = d3.select(this.$.table).selectAll(".category") |
| .data(category_list, function(category){ |
| return category; |
| }); |
| data.exit().remove(); |
| var newCategories = data |
| .enter() |
| .append("div") |
| .classed("category",true); |
| data.order(); |
| data |
| .style("flex-grow", function(category){ |
| // This keeps the rows about as evenly sized as possible. |
| // colspan is the total number of task specs in this category. |
| return categories[category].colspan; |
| }); |
| |
| newCategories |
| .insert("div") |
| .classed("category-title", true) |
| .text(function(category){ |
| return category; |
| }) |
| // .subcategories will hold all of the .subcategory elements. |
| newCategories |
| .insert("div") |
| .classed("subcategories", true); |
| }, |
| |
| // Draw all of the divs for the subcategories, including the header |
| _drawSubCategories: function(categories) { |
| d3.select(this.$.table).selectAll(".subcategories").each(function(category){ |
| if (!categories[category]) { |
| return; |
| } |
| var list = categories[category].subcategoryList.map(function(subcat){ |
| return { |
| "category": category, |
| "subcategory": subcat, |
| }; |
| }); |
| |
| var data = d3.select(this) |
| .selectAll(".subcategory") |
| .data(list, function(d) { |
| return d.category + d.subcategory; |
| }); |
| data.exit().remove(); |
| var subcategories = data |
| .enter() |
| .insert("div") |
| .classed("subcategory", true); |
| data.order(); |
| data |
| .style("flex-grow", function(d){ |
| // This keeps the rows about as evenly sized as possible. |
| return categories[d.category].subcategories[d.subcategory].task_specs.length; |
| }); |
| subcategories |
| .insert("div") |
| .classed("subcategory-title", true) |
| .text(function(d){ |
| return d.subcategory; |
| }); |
| // .task-specs will hold all of the .task-spec elements. |
| subcategories |
| .insert("div") |
| .classed("task-specs", true); |
| }); |
| }, |
| |
| // draw all of the task spec columns, including the header box with the flaky, ignore_failure, |
| // and comment icons |
| _drawTaskSpecColumns: function(categories, taskSpecs, num_commits) { |
| var addIronIcon = this._addIronIcon.bind(this); |
| var openTaskSpecDialog = this._openTaskSpecDialog.bind(this); |
| d3.select(this.$.table).selectAll(".task-specs").each(function(d){ |
| if (!categories[d.category].subcategories[d.subcategory]) { |
| return; |
| } |
| var list = categories[d.category].subcategories[d.subcategory].task_specs.map(function(taskSpec){ |
| return { |
| "category": d.category, |
| "subcategory": d.subcategory, |
| "taskSpec": taskSpec, |
| }; |
| }); |
| |
| var data = d3.select(this) |
| .selectAll(".task-spec") |
| .data(list, function(d) { |
| return d.category + d.subcategory + d.taskSpec; |
| }); |
| data.exit().remove(); |
| var newTaskSpecs = data |
| .enter() |
| .insert("div") |
| .classed("task-spec", true); |
| data.order(); |
| var titles = newTaskSpecs |
| .insert("div") |
| .classed("task-spec-title", true) |
| .attr("title", function(d){ |
| return d.taskSpec; |
| }) |
| .on("tap", function(d){ |
| // Stop the propogation so we don't immediately hide the popup we show. |
| d3.event.stopPropagation(); |
| openTaskSpecDialog(d.taskSpec); |
| }); |
| titles |
| .insert("span") |
| .classed("taskspec-comment", true); |
| titles |
| .insert("span") |
| .classed("taskspec-ignore", true); |
| titles |
| .insert("span") |
| .classed("taskspec-flaky", true); |
| |
| addIronIcon(data.selectAll(".taskspec-ignore"), "icons:block", "tiny", function(d){ |
| return taskSpecs[d.taskSpec].ignoreFailure; |
| }); |
| addIronIcon(data.selectAll(".taskspec-flaky"), "image:texture", "tiny", function(d){ |
| return taskSpecs[d.taskSpec].flaky; |
| }); |
| addIronIcon(data.selectAll(".taskspec-comment"), "communication:chat", "tiny", function(d){ |
| return taskSpecs[d.taskSpec].comments && taskSpecs[d.taskSpec].comments.length > 0; |
| }); |
| |
| newTaskSpecs |
| .insert("div") |
| .classed("task-spec-spacer", true); |
| |
| // Set the height so that flexing doesn't throw off the multi column alignment. |
| newTaskSpecs |
| .insert("div") |
| .classed("tasks", true) |
| .style("max-height", function(){ |
| return (TASK_HEIGHT * num_commits) + "px"; |
| }); |
| // .style("min-height", function(){ |
| // return (TASK_HEIGHT * num_commits) + "px"; |
| // }); |
| }); |
| }, |
| |
| // Draws the task divs. We forego flexbox helping us align these. First, we create a task |
| // div (which is a fixed height) in side of it create a div with the various task classes to |
| // set the heights/widths/margins etc. This is easier to line up with the adjacent columns |
| // and allows for more consistent zooming behavior. |
| _drawTasks: function(commits, task_details) { |
| var openTaskDialog = this._openTaskDialog.bind(this); |
| var addIronIcon = this._addIronIcon.bind(this); |
| |
| d3.select(this.$.table).selectAll(".tasks").each(function(d){ |
| var list = commits.map(function(commit) { |
| return { |
| "category": d.category, |
| "subcategory": d.subcategory, |
| "taskSpec": d.taskSpec, |
| "commit": commit, |
| "displayClass": commit.displayClass[d.taskSpec] || [], |
| }; |
| }); |
| var data = d3.select(this) |
| .selectAll(".task") |
| .data(list, function(d) { |
| return d.category + d.subcategory + d.taskSpec + d.commit.hash; |
| }); |
| data.exit().remove(); |
| var newTasks = data |
| .enter() |
| .insert("div") |
| .classed("task",true) |
| .insert("div") |
| .on("tap", function(d){ |
| // Stop the propogation so we don't immediately hide the popup we show. |
| d3.event.stopPropagation(); |
| openTaskDialog(d); |
| }); |
| newTasks |
| .insert("span") |
| .classed("task-comment", true); |
| data.order(); |
| // Style the inside div. |
| data.each(function(d){ |
| var task = this.children[0]; |
| // Remove any previously-set classes. |
| var removeClasses = []; |
| for (var i = 0; i < task.classList.length; i++) { |
| var c = task.classList.item(i); |
| if (c.indexOf("bg-") === 0 || c.indexOf("task_") === 0) { |
| removeClasses.push(c); |
| } |
| } |
| removeClasses.forEach(function(c) { |
| task.classList.remove(c); |
| }); |
| |
| // Add task display class. |
| for (var i = 0; i < d.displayClass.length; i++) { |
| task.classList.add(d.displayClass[i]); |
| } |
| |
| task.title = d.taskSpec + " @ " + d.commit.shortHash; |
| |
| var details = task_details[d.commit.hash] && task_details[d.commit.hash][d.taskSpec]; |
| if (details) { |
| // Add the new class. |
| task.classList.add(task_details[d.commit.hash][d.taskSpec].colorClass); |
| addIronIcon(d3.select(task).select(".task-comment"), "communication:chat", "tiny", function(){ |
| return details.comments && details.comments.length > 0 && |
| (task.classList.contains(CLASS_TASK_SINGLE) |
| || task.classList.contains(CLASS_TASK_TOP)); |
| }); |
| } else { |
| task.style["background-color"] = ""; |
| task.title = ""; |
| } |
| }); |
| }); |
| }, |
| |
| _openTaskDialog: function(task) { |
| if (this._infoPopupOpen()) { |
| return; |
| } |
| var details = this.task_details[task.commit.hash][task.taskSpec]; |
| if (details) { |
| var taskInfo = document.createElement("task-popup-sk"); |
| taskInfo.task = details; |
| taskInfo.repo = this.repo; |
| taskInfo.repo_base = this.repo_base; |
| taskInfo.commit_details = this.commits_map; |
| taskInfo.parent = this; |
| taskInfo.swarming_url = this.swarming_url; |
| taskInfo.task_scheduler_url = this.task_scheduler_url; |
| this._openDialog(taskInfo); |
| } |
| }, |
| |
| _openTaskSpecDialog: function(taskSpec) { |
| if (this._infoPopupOpen()) { |
| return; |
| } |
| if (taskSpec) { |
| var taskSpecInfo = document.createElement("taskspec-popup-sk"); |
| taskSpecInfo.task_spec = this.task_specs[taskSpec]; |
| taskSpecInfo.repo = this.repo; |
| taskSpecInfo.swarming_url = this.swarming_url; |
| this._openDialog(taskSpecInfo); |
| } |
| }, |
| |
| _openCommitDialog: function(commit) { |
| if (this._infoPopupOpen()) { |
| return; |
| } |
| var commitInfo = document.createElement("commit-popup-sk"); |
| commitInfo.commit = commit; |
| commitInfo.repo = this.repo; |
| commitInfo.repo_base = this.repo_base; |
| this._openDialog(commitInfo); |
| }, |
| |
| // Is the info popup open? |
| _infoPopupOpen: function() { |
| return this.$.infoDialog.opened; |
| }, |
| |
| // Set the dialog content and open the dialog. |
| _openDialog: function(child) { |
| this.$.infoDialog.innerHTML = ''; |
| this.$.infoDialog.appendChild(child); |
| child.show(); |
| }, |
| }); |
| })() |
| </script> |
| </dom-module> |