blob: 0f43aa435cd1c31170e17a73bd70ccf623c19139 [file] [log] [blame]
<!-- 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>