| <!-- The <dots-sk> custom element declaration. |
| |
| A custom element for displaying a dot chart of digests by trace, such as: |
| |
| ooo-o-o-oo••• |
| |
| |
| Attributes: |
| None. |
| |
| Events: |
| show-commits - Event generated when a dot is clicked. |
| e.detail contains a slice of commits that could have |
| made up that dot. |
| |
| hover - Event generated when the mouse hovers over a trace. |
| e.detail is the trace id. |
| |
| Methods: |
| setValue(value) - Where value is an object of the form: |
| |
| { |
| tileSize: 50, |
| traces: [ |
| { |
| label: "some:trace:id", |
| data: [ |
| { |
| x: 2, // Commit index. |
| y: 0, // Trace index. |
| s: 0 // Color code. |
| }, |
| { x: 5, y: 0, s: 1 }, |
| ... |
| ], |
| }, |
| ... |
| ] |
| } |
| |
| Where s is a color code, 0 is the target digest, while |
| 1-6 indicate unique digests that are different from |
| the target digest. A code of 7 means that there are more |
| than 7 unique digests in the trace and all digests |
| after the first 7 unique digests are represented by |
| this code. |
| |
| highlight(index, b) - Highlight all digests at the given index |
| if b is true, else remove the highlight at that index. |
| |
| setCommits(commits) - Where commits is an array of commits: |
| |
| [ |
| { |
| author: "committer@example.org" |
| commit_time: 1428445634 |
| hash: "c654e9016a15985ebeb24f94f819d113ad48a251" |
| }, |
| ... |
| ] |
| |
| --> |
| <link rel="import" href="dot-styles.html"> |
| |
| <dom-module id="dots-sk"> |
| <template> |
| <style type="text/css" media="screen"> |
| :host { |
| max-height: 10em; |
| display: inline-block; |
| overflow: auto; |
| } |
| </style> |
| <canvas id="dots" width=0 height=0></canvas> |
| </template> |
| <script> |
| (function () { |
| |
| const STROKE_WIDTH = 2; |
| const DOT_RADIUS = 3; |
| |
| // Constants that define how we go from dot space to canvas coordinates. |
| const DOT_SCALE_X = 10; |
| const DOT_SCALE_Y = 10; |
| const DOT_OFFSET_X = 10; |
| const DOT_OFFSET_Y = 10; |
| const xFn = x => x * DOT_SCALE_X + DOT_OFFSET_X; |
| const yFn = y => y * DOT_SCALE_Y + DOT_OFFSET_Y; |
| |
| const STROKE_COLORS = [ |
| '#000000', |
| '#1B9E77', |
| '#D95F02', |
| '#7570B3', |
| '#E7298A', |
| '#66A61E', |
| '#E6AB02', |
| '#A6761D', |
| '#999999', |
| ]; |
| const FILL_COLORS = [ |
| '#000000', |
| '#FFFFFF', |
| '#FFFFFF', |
| '#FFFFFF', |
| '#FFFFFF', |
| '#FFFFFF', |
| '#FFFFFF', |
| '#FFFFFF', |
| '#FFFFFF', |
| ]; |
| const HIGHLIGHT_COLORS = [ |
| '#FFFFFF', |
| '#1B9E77', |
| '#D95F02', |
| '#7570B3', |
| '#E7298A', |
| '#66A61E', |
| '#E6AB02', |
| '#A6761D', |
| '#999999', |
| ]; |
| |
| const LINE_COLOR = '#999999' |
| |
| // Maps each dots-sk _id to the element itself. Members are only |
| // present if they have a pending mousemove update. |
| let pendingMouseMoves = {}; |
| |
| // Periodically process all pending mousemoves. |
| window.setInterval(() => { |
| for (let key in pendingMouseMoves) { |
| pendingMouseMoves[key]._updatePendingMouseMove(); |
| } |
| pendingMouseMoves = {}; |
| }, 40); |
| |
| Polymer({ |
| is: "dots-sk", |
| |
| ready: function() { |
| this._commits = []; |
| this._value = { tileSize: 0, traces: [] }; |
| |
| // The index of the commit that should be highlighted. |
| this._highlightIndex = -1; |
| |
| // The index of the trace that should be highlighted. |
| this._hoverIndex = -1; |
| |
| // For capturing the last mousemove event, which is later processed in a timer. |
| this._lastMouseMove = null; |
| |
| // Each element needs a unique id, used as a key in pendingMouseMoves. |
| this._id = 'id' + Math.random(); |
| this._ctx = this.$.dots.getContext('2d'); |
| this.listen(this.$.dots, 'mousemove', '_onMouseMove'); |
| this.listen(this.$.dots, 'mouseleave', '_onMouseLeave'); |
| this.listen(this.$.dots, 'tap', '_onTap'); |
| }, |
| |
| setValue: function(value) { |
| if (!value || (value.tileSize === 0)) { |
| return; |
| } |
| |
| this._value = value; |
| this._draw(); |
| }, |
| |
| _draw: function() { |
| let width = this._value.tileSize * DOT_SCALE_X + 2 * DOT_OFFSET_X; |
| let height = this._value.traces.length * DOT_SCALE_Y + 2 * DOT_OFFSET_Y; |
| // Define functions that go from dot space to canvas coordinates. |
| this.$.dots.setAttribute('width', width+'px'); |
| this.$.dots.setAttribute('height', height+'px'); |
| |
| // First clear the canvas. |
| this._ctx.lineWidth = STROKE_WIDTH; |
| this._ctx.fillStyle = '#FFFFFF'; |
| this._ctx.fillRect(0, 0, width, height); |
| |
| // Draw lines and dots. |
| this._value.traces.forEach((trace, traceIndex) => { |
| this._ctx.strokeStyle = LINE_COLOR; |
| this._ctx.beginPath(); |
| this._ctx.moveTo(xFn(trace.data[0].x), yFn(trace.data[0].y)); |
| this._ctx.lineTo(xFn(trace.data[trace.data.length-1].x), yFn(trace.data[0].y)); |
| this._ctx.stroke(); |
| this._drawTrace(trace.data, traceIndex); |
| }); |
| }, |
| |
| // Draws the circles for a single trace. |
| _drawTrace: function(data, traceIndex) { |
| data.forEach((d, dataIndex) => { |
| this._ctx.beginPath(); |
| this._ctx.strokeStyle = STROKE_COLORS[d.s]; |
| if (this._hoverIndex === traceIndex || this._highlightIndex === dataIndex) { |
| this._ctx.fillStyle = HIGHLIGHT_COLORS[d.s]; |
| } else { |
| this._ctx.fillStyle = FILL_COLORS[d.s]; |
| } |
| this._ctx.arc(xFn(d.x), yFn(d.y), DOT_RADIUS, 0, Math.PI*2); |
| this._ctx.fill(); |
| this._ctx.stroke(); |
| }); |
| }, |
| |
| // Updates just the circles for a single trace. |
| _overDrawTrace: function(traceIndex) { |
| let trace = this._value.traces[traceIndex]; |
| if (!trace) { |
| return |
| } |
| this._drawTrace(trace.data, traceIndex); |
| }, |
| |
| setCommits: function(commits) { |
| this._commits = commits; |
| }, |
| |
| highlight: function(index, b) { |
| this._highlightIndex = b ? index : -1; |
| this._draw(); |
| }, |
| |
| _onMouseLeave: function() { |
| let oldIndex = this._hoverIndex; |
| this._hoverIndex = -1; |
| this._overDrawTrace(oldIndex); |
| this._lastMouseMove = null; |
| }, |
| |
| _onMouseMove: function(e) { |
| this._lastMouseMove = { |
| clientX: e.clientX, |
| clientY: e.clientY, |
| }; |
| pendingMouseMoves[this._id] = this; |
| }, |
| |
| // Gets the coordinates of the mouse event in dot coordinates. |
| _toDotSpace(e) { |
| let rect = this.$.dots.getBoundingClientRect(); |
| return { |
| x: Math.floor((e.clientX - rect.left - DOT_OFFSET_X + STROKE_WIDTH + DOT_RADIUS)/DOT_SCALE_X), |
| y: Math.floor((e.clientY - rect.top - DOT_OFFSET_Y + STROKE_WIDTH + DOT_RADIUS)/DOT_SCALE_Y) |
| }; |
| }, |
| |
| |
| // We look at the mousemove event, if one occurred, to determine which trace to highlight. |
| _updatePendingMouseMove() { |
| if (!this._lastMouseMove) { |
| return |
| } |
| let rect = this._toDotSpace(this._lastMouseMove); |
| this._lastMouseMove = null; |
| // If the focus has moved to a different trace then draw the two changing traces. |
| if (this._hoverIndex !== rect.y) { |
| let oldIndex = this._hoverIndex; |
| this._hoverIndex = rect.y; |
| if (this._hoverIndex >= 0 && this._hoverIndex < this._value.traces.length) { |
| this.fire('hover', this._value.traces[this._hoverIndex].label); |
| } |
| // Just update the dots of the traces that have changed. |
| this._overDrawTrace(oldIndex); |
| this._overDrawTrace(this._hoverIndex); |
| } |
| |
| // Set the cursor to a pointer if you are hovering over a dot. |
| let found = false; |
| let trace = this._value.traces[rect.y]; |
| if (trace) { |
| for (let i=trace.data.length-1; i>=0; i--) { |
| if (trace.data[i].x === rect.x) { |
| found = true; |
| break; |
| } |
| } |
| } |
| this.style.cursor = (found) ? 'pointer' : 'auto'; |
| }, |
| |
| // When a dot is clicked on produce the show-commits event with all |
| // the commits that are included upto and including that dot. |
| _onTap: function(e) { |
| let rect = this._toDotSpace(e.detail.sourceEvent); |
| // Look backwards in the trace for the previous commit with data. |
| let firstCommit = 0; |
| let trace = this._value.traces[rect.y]; |
| let found = false; |
| for (let i=trace.data.length-1; i>=0; i--) { |
| if (trace.data[i].x === rect.x) { |
| found = true; |
| if (i > 0) { |
| firstCommit = trace.data[i-1].x+1; |
| break; |
| } |
| } |
| } |
| if (!found) { |
| return |
| } |
| |
| let commitinfo = this._commits.slice(firstCommit, rect.x+1); |
| commitinfo.reverse(); |
| this.fire('show-commits', commitinfo); |
| }, |
| }); |
| })(); |
| </script> |
| </dom-module> |