index.html
<!DOCTYPE html>
<html manifest="timecoder.appcache?v=2">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="shortcut icon" href="favicon.png" type="image/x-icon">
<link rel="apple-touch-icon" href="favicon.png">
<title>Timecoder</title>
<style>
html, body {
margin: 0;
padding: 0;
font: 14px sans-serif;
}
* {
box-sizing: border-box;
}
.record {
width: 100%;
font: 28px sans-serif;
position: fixed;
top: 0;
left: 0;
background: white;
border: 1px solid black;
padding: 1em;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.record:hover {
background: #ccc;
cursor: pointer;
}
.record:active {
color: white;
background: black;
}
table {
margin-top: 90px;
width: 100%;
border-collapse: collapse;
}
table th, table td {
padding: 1em;
width: 33.3%;
border: 1px solid #ccc;
text-align: center;
}
hr {
border: none;
border-top: 1px solid gray;
}
pre {
margin-top: 1em;
width: 100%;
border-top: 1px solid gray;
color: gray;
font-family: monospace;
}
</style>
<body>
<button class="record">Press and hold</button>
<div class="table"></div>
<hr>
<button class="clear">Clear times</button>
<button class="epoch">Start timing from now</button>
<p>CSV (in seconds after time started): </p>
<pre></pre>
</body>
<script src="d3.min.js" charset="utf-8"></script>
<script src="d3-jetpack.js" charset="utf-8"></script>
<script>
var epoch = new Date(0);
var times = [];
if(typeof(Storage) !== "undefined") {
if(!localStorage.getItem("times")) {
save("times", times);
} else {
times = read("times");
}
if(!localStorage.getItem("epoch")) {
localStorage.setItem("epoch", epoch);
} else {
epoch = new Date(localStorage.getItem("epoch"));
}
}
var columns = [
{ head: 'Start', cl: 'start', html: ƒ(0, timeFormat) },
{ head: 'End', cl: 'stop', html: ƒ(1, timeFormat) },
{ head: 'Duration', cl: 'duration', html: diffFormat },
];
var table = d3.select('.table')
.append('table');
table.append('thead').append('tr')
.selectAll('th')
.data(columns).enter()
.append('th')
.attr('class', ƒ('cl'))
.text(ƒ('head'));
var tbody = table.append('tbody');
d3.select(".record")
.on("mousedown", start)
.on("mouseup", end)
.on("touchstart", touchStart)
.on("touchend", touchEnd);
d3.select(".clear").on("click", clear);
d3.select(".epoch").on("click", setEpoch);
render();
function touchStart() {
d3.event.preventDefault();
start();
}
function start() {
times.push([newDate(), null]);
save("times", times);
render();
}
function touchEnd() {
d3.event.preventDefault();
end();
}
function end() {
times[times.length-1][1] = newDate();
save("times", times);
render();
}
function render() {
var update = tbody
.selectAll('tr')
.data(times);
update.enter()
.append('tr')
.selectAll('td')
.data(columns).enter()
.append('td');
update.selectAll('td')
.data(function(row, i) {
return columns.map(function(c) {
var cell = {};
d3.keys(c).forEach(function(k) {
cell[k] = typeof c[k] == 'function' ? c[k](row,i) : c[k];
});
return cell;
});
})
.html(ƒ('html'))
.attr('class', ƒ('cl'));
update.exit().remove();
var csv = times.map(function(item) {
return item.map(function(endpoint) { return +endpoint / 1000; }).join(",")
}).join("\n");
d3.select("pre").text("start,end\n" + csv);
}
function clear() {
if(confirm("Are you sure you want to permanently delete all your recorded times?")) {
times = [];
save("times", times);
render();
}
}
function setEpoch() {
if(confirm("New items will show times counting from the moment you hit 'OK'")) {
epoch = new Date();
localStorage.setItem("epoch", epoch);
}
}
function timeFormat(d) {
if(d === null) return '⋯';
if(epoch - new Date(0) !== 0) {
renderTime = new Date(+d + d.getTimezoneOffset()*60*1000);
} else {
renderTime = d;
}
return d3.time.format('%X')(renderTime);
}
function diffFormat(d) {
if(d[1] === null) return '⋯';
return d3.time.format('%M:%S.%L')(new Date(d[1] - d[0]));
}
function save(key, value) {
return localStorage.setItem(key, JSON.stringify(value));
}
function read(key) {
var json = JSON.parse(localStorage.getItem(key));
return json.map(function(item) {
return item.map(function(endpoint) {
return new Date(endpoint);
})
});
}
function newDate() {
return new Date(new Date() - epoch);
}
</script>
</html>
d3-jetpack.js
(function() {
function jetpack(d3) {
d3.selection.prototype.translate = function(xy) {
return this.attr('transform', function(d,i) {
return 'translate('+[typeof xy == 'function' ? xy(d,i) : xy]+')';
});
};
d3.transition.prototype.translate = function(xy) {
return this.attr('transform', function(d,i) {
return 'translate('+[typeof xy == 'function' ? xy(d,i) : xy]+')';
});
};
d3.selection.prototype.tspans = function(lines, lh) {
return this.selectAll('tspan')
.data(lines)
.enter()
.append('tspan')
.text(function(d) { return d; })
.attr('x', 0)
.attr('dy', function(d,i) { return i ? lh || 15 : 0; });
};
d3.selection.prototype.append =
d3.selection.enter.prototype.append = function(name) {
var n = d3_parse_attributes(name), s;
name = n.attr ? n.tag : name;
name = d3_selection_creator(name);
s = this.select(function() {
return this.appendChild(name.apply(this, arguments));
});
return n.attr ? s.attr(n.attr) : s;
};
d3.selection.prototype.insert =
d3.selection.enter.prototype.insert = function(name, before) {
var n = d3_parse_attributes(name), s;
name = n.attr ? n.tag : name;
name = d3_selection_creator(name);
before = d3_selection_selector(before);
s = this.select(function() {
return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null);
});
return n.attr ? s.attr(n.attr) : s;
};
var d3_parse_attributes_regex = /([\.#])/g;
function d3_parse_attributes(name) {
if (typeof name === "string") {
var attr = {},
parts = name.split(d3_parse_attributes_regex), p;
name = parts.shift();
while ((p = parts.shift())) {
if (p == '.') attr['class'] = attr['class'] ? attr['class'] + ' ' + parts.shift() : parts.shift();
else if (p == '#') attr.id = parts.shift();
}
return attr.id || attr['class'] ? { tag: name, attr: attr } : name;
}
return name;
}
function d3_selection_creator(name) {
return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? function() {
return this.ownerDocument.createElementNS(name.space, name.local);
} : function() {
return this.ownerDocument.createElementNS(this.namespaceURI, name);
};
}
function d3_selection_selector(selector) {
return typeof selector === "function" ? selector : function() {
return this.querySelector(selector);
};
}
d3.wordwrap = function(line, maxCharactersPerLine) {
var w = line.split(' '),
lines = [],
words = [],
maxChars = maxCharactersPerLine || 40,
l = 0;
w.forEach(function(d) {
if (l+d.length > maxChars) {
lines.push(words.join(' '));
words.length = 0;
l = 0;
}
l += d.length;
words.push(d);
});
if (words.length) {
lines.push(words.join(' '));
}
return lines;
};
d3.ascendingKey = function(key) {
return typeof key == 'function' ? function (a, b) {
return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : key(a) >= key(b) ? 0 : NaN;
} : function (a, b) {
return a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : a[key] >= b[key] ? 0 : NaN;
};
};
d3.descendingKey = function(key) {
return typeof key == 'function' ? function (a, b) {
return key(b) < key(a) ? -1 : key(b) > key(a) ? 1 : key(b) >= key(a) ? 0 : NaN;
} : function (a, b) {
return b[key] < a[key] ? -1 : b[key] > a[key] ? 1 : b[key] >= a[key] ? 0 : NaN;
};
};
d3.f = function(){
var functions = arguments;
var i = 0, l = functions.length;
while (i < l) {
if (typeof(functions[i]) === 'string' || typeof(functions[i]) === 'number'){
functions[i] = (function(str){ return function(d){ return d[str] }; })(functions[i])
}
i++;
}
return function(d) {
var i=0, l = functions.length;
while (i++ < l) d = functions[i-1].call(this, d);
return d;
};
};
// store d3.f as convenient unicode character function (alt-f on macs)
if (!window.hasOwnProperty('ƒ')) window.ƒ = d3.f;
// this tweak allows setting a listener for multiple events, jquery style
var d3_selection_on = d3.selection.prototype.on;
d3.selection.prototype.on = function(type, listener, capture) {
if (typeof type == 'string' && type.indexOf(' ') > -1) {
type = type.split(' ');
for (var i = 0; i<type.length; i++) {
d3_selection_on.apply(this, [type[i], listener, capture]);
}
} else {
d3_selection_on.apply(this, [type, listener, capture]);
}
return this;
};
// for heaven's sake, let's add prop as alias for property
d3.selection.prototype.prop = d3.selection.prototype.property;
}
if (typeof d3 === 'object' && d3.version) jetpack(d3);
else if (typeof define === 'function' && define.amd) {
define(['d3'], jetpack);
}
})();