diff --git a/index.html b/index.html
index 8a30cec..6ec1c5b 100644
--- a/index.html
+++ b/index.html
@@ -12,8 +12,15 @@
+
Build durations over time
+
+
+
diff --git a/script.js b/script.js
index 85a0df1..d61db7c 100644
--- a/script.js
+++ b/script.js
@@ -1,4 +1,76 @@
-function renderBuildTimes(selector, data, baseUrl) {
+function renderBuildCounts(container, data) {
+ var valueLabelWidth = 40; // space reserved for value labels (right)
+ var barHeight = 20; // height of one bar
+ var barLabelWidth = 130; // space reserved for bar labels
+ var barLabelPadding = 5; // padding between bar and bar labels (left)
+ var gridLabelHeight = 18; // space reserved for gridline labels
+ var gridChartOffset = 3; // space between start of grid and first bar
+ var maxBarWidth = 420; // width of the bar with the max value
+
+ // accessor functions
+ var barValue = function(d) { return d.value; };
+
+ // scales
+ var yScale = d3.scale.ordinal().domain(d3.range(0, data.length)).rangeBands([0, data.length * barHeight]);
+ var y = function(d, i) { return yScale(i); };
+ var yText = function(d, i) { return y(d, i) + yScale.rangeBand() / 2; };
+ var x = d3.scale.linear().domain([0, d3.max(data, barValue)]).range([0, maxBarWidth]);
+ // svg container element
+ var chart = d3.select().html('').append("svg")
+ var chart = d3.select(container).html('').append("svg")
+ .attr('width', maxBarWidth + barLabelWidth + valueLabelWidth)
+ .attr('height', gridLabelHeight + gridChartOffset + data.length * barHeight);
+ // grid line labels
+ var gridContainer = chart.append('g')
+ .attr('transform', 'translate(' + barLabelWidth + ',' + gridLabelHeight + ')');
+ gridContainer.selectAll("text").data(x.ticks(10)).enter().append("text")
+ .attr("x", x)
+ .attr("dy", -3)
+ .attr("text-anchor", "middle")
+ .text(String);
+ // vertical grid lines
+ gridContainer.selectAll("line").data(x.ticks(10)).enter().append("line")
+ .attr("x1", x)
+ .attr("x2", x)
+ .attr("y1", 0)
+ .attr("y2", yScale.rangeExtent()[1] + gridChartOffset)
+ .style("stroke", "#ccc");
+ // bar labels
+ var labelsContainer = chart.append('g')
+ .attr('transform', 'translate(' + (barLabelWidth - barLabelPadding) + ',' + (gridLabelHeight + gridChartOffset) + ')');
+ labelsContainer.selectAll('text').data(data).enter().append('text')
+ .attr('y', yText)
+ .attr("dy", ".35em") // vertical-align: middle
+ .attr('text-anchor', 'end')
+ .text(function(d) { return d.key; });
+
+ // bars
+ var barsContainer = chart.append('g')
+ .attr('transform', 'translate(' + barLabelWidth + ',' + (gridLabelHeight + gridChartOffset) + ')');
+ barsContainer.selectAll("rect").data(data).enter().append("rect")
+ .attr('y', y)
+ .attr('height', yScale.rangeBand())
+ .attr('width', function(d) { return x(barValue(d)); })
+ .attr('stroke', 'white')
+ .attr('fill', 'steelblue');
+ // bar value labels
+ barsContainer.selectAll("text").data(data).enter().append("text")
+ .attr("x", function(d) { return x(barValue(d)); })
+ .attr("y", yText)
+ .attr("dx", 3) // padding-left
+ .attr("dy", ".35em") // vertical-align: middle
+ .attr("text-anchor", "start") // text-align: right
+ .attr("fill", "black")
+ .attr("stroke", "none")
+ .text(function(d) { return d3.round(barValue(d), 2); });
+ // start line
+ barsContainer.append("line")
+ .attr("y1", -gridChartOffset)
+ .attr("y2", yScale.rangeExtent()[1] + gridChartOffset)
+ .style("stroke", "#000");
+}
+
+function renderBuildTimes(container, data, baseUrl) {
var valueLabelWidth = 40; // space reserved for value labels (right)
var barHeight = 20; // height of one bar
var barLabelWidth = 50; // space reserved for bar labels
@@ -17,7 +89,7 @@ function renderBuildTimes(selector, data, baseUrl) {
var x = d3.scale.linear().domain([0, d3.max(data, barValue)]).range([0, maxBarWidth]);
// svg container element
var chart = d3.select().html('').append("svg")
- var chart = d3.select(selector).html('').append("svg")
+ var chart = d3.select(container).html('').append("svg")
.attr('width', maxBarWidth + barLabelWidth + valueLabelWidth)
.attr('height', gridLabelHeight + gridChartOffset + data.length * barHeight);
// grid line labels
@@ -76,6 +148,11 @@ function renderBuildTimes(selector, data, baseUrl) {
.style("stroke", "#000");
}
+function getBuildDate(build) {
+ var dt = new Date(Date.parse(build.started_at));
+ return dt.toDateString();
+}
+
function updateChart() {
var repoName = document.getElementById('repo-name').value;
var baseUrl = 'https://travis-ci.org/' + repoName + '/builds/';
@@ -87,6 +164,18 @@ function updateChart() {
var oldestBuild = Infinity;
var i=0, n=15;
+ var buildCounts = {};
+
+ function updateCount(build) {
+ var buildDate = getBuildDate(build);
+
+ if (!buildCounts[buildDate]) {
+ buildCounts[buildDate] = 1;
+ } else {
+ buildCounts[buildDate] += 1;
+ }
+ }
+
function filterBuilds(rawBuilds) {
var curOldestBuild = oldestBuild;
@@ -100,12 +189,12 @@ function updateChart() {
return;
}
- build.started_at = Date.parse(build.started_at);
-
+ updateCount(build);
builds.push(build);
});
renderBuildTimes('#build-times', builds, baseUrl);
+ renderBuildCounts('#build-counts', d3.entries(buildCounts), baseUrl);
if (++i < n && curOldestBuild < oldestBuild) {
oldestBuild = curOldestBuild;
diff --git a/style.css b/style.css
index 6d9f8a4..65c940e 100644
--- a/style.css
+++ b/style.css
@@ -3,6 +3,11 @@ form {
margin-bottom: 10px;
}
+.column {
+ width: 49%;
+ float: left;
+}
+
.build-nr {
fill: blue;
}