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

+
+ +
+

Builds per day

+
+
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; }