diff --git a/README.md b/README.md index 7ead218..9b77191 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,30 @@ # Introduction -This project is an experiment in visualizing code development using the amazing [d3][d3home] JavaScript library and the github REST [api][githubapi]. +This project is an experiment in visualizing code development using the amazing [D3][d3home] JavaScript library and the github REST [API][githubapi]. It was conceived at the 2012 Java Posse [Roundup][roundup], an amazing gathering of talented developers from around the world. The developers at the Roundup used [github][github] to collaborate on many different interesting projects - too many for any one person to fully participate in. -[I][githubmtye] was consequently inspired to see if I could use the d3 JavaScript library to create a [visualization][visarticle] of all the work the Roundup developers had accompished (or at least all the work they'd committed to [github][githubjpr]). This is the result. +[I][githubmtye] was consequently inspired to see if I could use the D3 JavaScript library to create a [visualization][visarticle] of all the work the Roundup developers had accomplished (or at least all the work they'd committed to [github][githubjpr]). This project is the result. -[d3home]: http://mbostock.github.com/d3/ "d3 at github" +More information is available at the d3github [project page][d3githubpage]. + +[d3home]: http://d3js.org/ "D3 (Data-Driven Documents) website" [githubapi]: http://developer.github.com/v3/ [roundup]: http://www.mindviewinc.com/Conferences/JavaPosseRoundup/ [github]: https://github.com/ "Duh!" [githubmtye]: https://github.com/mtye [visarticle]: http://en.wikipedia.org/wiki/Information_visualization [githubjpr]: https://github.com/JavaPosseRoundup "Java Posse Roundup at github" +[d3githubpage]: http://javaposseroundup.github.com/d3github/ ## Installation and Use -To use this project, simple clone this github repository and open the ```commits.html``` file in your web browser. The d3 library is available in the root directory, so there's no need to install it (or anything else). +To use this project, simply clone this github repository and open the ```commits.html``` file in your web browser. (The file references CDN-hosted versions of the D3 and jQuery libraries, so you'll need an Internet connection.) ## Examples -To see the d3github visualizer in use without cloning/downloading the project, look at this [example][example] on [bl.ocks.org][blocksorg]. - -[example]: http://bl.ocks.org/2356163 -[blocksorg]: http://bl.ocks.org +A live example of the d3github visualizer can be found on the d3github [project page][d3githubpage]. ## Compatibility -The d3 library uses the CSS Selectors API Level 1 and SVG, which may not be supported in older browsers. This code in this project has been verified to work with Google Chrome (verison 17.0), Firefox (11.0), and Safari (5.1), but not with Internet Explorer. The minimum versions that _should_ work are Chrome 4.0, Firefox 3.5, Safari 3.2, and Internet Explorer 9.0. \ No newline at end of file +The D3 library uses the CSS Selectors API Level 1 and SVG, which may not be supported in older browsers. This code in this project has been verified to work with Google Chrome (verison 17.0), Firefox (11.0), and Safari (5.1), but not with Internet Explorer. The minimum versions that _should_ work are Chrome 4.0, Firefox 3.5, Safari 3.2, and Internet Explorer 9.0. \ No newline at end of file diff --git a/commits.css b/commits.css new file mode 100644 index 0000000..39d83d9 --- /dev/null +++ b/commits.css @@ -0,0 +1,33 @@ +body { + font: 14px sans-serif; + margin: 0; +} +.title { + font-size: 24px; + margin: 25px 0px 5px 200px; +} +.commit { + stroke: black; + stroke-width: 2px; +} +.timeline { + stroke: #444; + stroke-width: 1px; +} +.axis { + shape-rendering: crispEdges; +} +.x.axis .minor { + stroke-opacity: .5; +} +.y.axis path { + stroke: #fff; +} +.axis line, .axis path { + fill: none; + stroke: #000; +} +.help { + font-size: 16px; + margin: 0px 0px 5px 200px; +} diff --git a/commits.html b/commits.html index c2c6206..2367ebc 100644 --- a/commits.html +++ b/commits.html @@ -1,47 +1,13 @@ - - - + + +
Javaposse Roundup GitHub Repositories
-
+
Mouseover a tick mark on a timeline to see commit info
Scroll to zoom in or out
Drag to pan left or right
diff --git a/commits.js b/commits.js index c9c7b4d..1d530f1 100644 --- a/commits.js +++ b/commits.js @@ -1,10 +1,19 @@ -var orgName = "JavaPosseRoundup"; -var roundupStart = new Date(2012, 2, 25); - -var margin = {top: 20, right: 0, bottom: 50, left: 180}, - w = 1200 - margin.left - margin.right, - h = 600 - margin.top - margin.bottom, - tickHeight = 10; +// Find the element where the commit timeline should be drawn +var commits = d3.select("#commits"); + +var orgName = commits.attr("orgName") || "JavaPosseRoundup"; +var startDate = commits.attr("startDate") || ""; +var leftRightPadding = commits.attr("left-right-padding") || 20; +var topBottomPadding = commits.attr("top-bottom-padding") || 150; +var width = commits.attr("width") || $(document).width() - leftRightPadding; +var height = commits.attr("height") || $(window).height() - topBottomPadding; +var leftMargin = commits.attr("left-margin") || 180; +var rightMargin = commits.attr("right-margin") || 0; +var topMargin = commits.attr("top-margin") || 20; +var bottomMargin = commits.attr("bottom-margin") || 60; + +var w = width - leftMargin - rightMargin, + h = height - topMargin - bottomMargin; // Scales. Note the inverted domain for the y-scale: bigger is up! var x = d3.time.scale().rangeRound([0, w]), @@ -16,29 +25,29 @@ var xAxis = d3.svg.axis().scale(x).tickSubdivide(true); var yAxis = d3.svg.axis().scale(y).tickSize(0).tickPadding(5).orient("left"); // Add an SVG element with the desired dimensions and margin. -var svg = d3.select("#chart").append("svg") - .attr("width", w + margin.left + margin.right) - .attr("height", h + margin.top + margin.bottom) +var svg = commits.append("svg") + .attr("width", width) + .attr("height", height) .append("g") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + .attr("transform", "translate(" + leftMargin + "," + topMargin + ")"); // Add the clip path. svg.append("clipPath") .attr("id", "clip") .append("rect") .attr("width", w) - .attr("height", h + 20); + .attr("height", h); // Need something in the background absorb the mouse events! svg.append("rect") .attr("width", w) .attr("height", h) - .style("fill", "#fff"); + .style("fill-opacity", ".0"); $.getJSON("https://api.github.com/orgs/" + orgName + "/repos?callback=?", function(response) { var allCommits = []; var timelines = []; - $.each(response.data, function(i, repo) { + response.data.forEach(function(repo, i, array) { function gitSource(repo) { return repo.url + "/commits?callback=?"; } @@ -61,21 +70,27 @@ $.getJSON("https://api.github.com/orgs/" + orgName + "/repos?callback=?", functi }; allCommits.push.apply(allCommits, commits); timelines.push(timeline); - if (timelines.length == response.data.length) drawChart(allCommits, timelines); + if (timelines.length == array.length) drawChart(allCommits, timelines); }); }); }); function drawChart(allCommits, timelines) { - x.domain([roundupStart, d3.time.day.ceil(new Date())]); + var tickHeight = height / (timelines.length * 4 + 1); + + var earliestCommitDate = d3.min(timelines, function(d) { return d.earliest; }); + + var start = d3.time.format("%Y-%m-%d").parse(startDate) || earliestCommitDate; + + x.domain([d3.time.day.floor(start), d3.time.day.ceil(new Date())]); y.domain(timelines.map(function(t) { return t.repo; })); // Add the x-axis. svg.append("g") .attr("class", "x axis") - .attr("transform", "translate(0," + (h + 20) + ")") + .attr("transform", "translate(0," + (h + topMargin + tickHeight) + ")") .call(xAxis); // Add the y-axis. @@ -128,7 +143,6 @@ function drawChart(allCommits, timelines) { committer.enter().append("text") .attr("class", "committer") - .attr("clip-path", "url(#clip)") .attr("x", x(d.commitDate)) .attr("y", y(d.repo)) .attr("dx", -2) @@ -144,7 +158,6 @@ function drawChart(allCommits, timelines) { message.enter().append("text") .attr("class", "message") - .attr("clip-path", "url(#clip)") .attr("x", x(d.commitDate)) .attr("y", y(d.repo)) .attr("dx", 2) diff --git a/commitsaction.js b/commitsaction.js new file mode 100644 index 0000000..5ee38c8 --- /dev/null +++ b/commitsaction.js @@ -0,0 +1,30 @@ +$.getJSON("https://api.github.com/orgs/" + orgName + "/repos?callback=?", function(response) { + var allCommits = []; + var timelines = []; + response.data.forEach(function(repo, i, array) { + function gitSource(repo) { + return repo.url + "/commits?callback=?"; + } + $.getJSON(gitSource(repo), function(response) { + var commits = response.data.map(function(r) { + return { + repo: repo.name, + message: r.commit.message, + authorDate: d3.time.format.iso.parse(r.commit.author.date), + committerEmail: r.commit.committer.email, + committerName: r.commit.committer.name, + commitDate: d3.time.format.iso.parse(r.commit.committer.date), + sha: r.sha + } + }); + var timeline = { + repo: repo.name, + earliest: commits[commits.length - 1].commitDate, + latest: commits[0].commitDate + }; + allCommits.push.apply(allCommits, commits); + timelines.push(timeline); + if (timelines.length == array.length) drawChart(allCommits, timelines); + }); + }); +}); diff --git a/commitsbase.js b/commitsbase.js new file mode 100644 index 0000000..47db2cb --- /dev/null +++ b/commitsbase.js @@ -0,0 +1,139 @@ +// Find the element where the commit timeline should be drawn +var commits = d3.select("#commits"); + +var orgName = commits.attr("orgName") || "JavaPosseRoundup"; +var startDate = commits.attr("startDate") || ""; +var leftRightPadding = commits.attr("left-right-padding") || 20; +var topBottomPadding = commits.attr("top-bottom-padding") || 150; +var width = commits.attr("width") || $(document).width() - leftRightPadding; +var height = commits.attr("height") || $(window).height() - topBottomPadding; +var leftMargin = commits.attr("left-margin") || 180; +var rightMargin = commits.attr("right-margin") || 0; +var topMargin = commits.attr("top-margin") || 20; +var bottomMargin = commits.attr("bottom-margin") || 60; + +var w = width - leftMargin - rightMargin, + h = height - topMargin - bottomMargin; + +// Scales. Note the inverted domain for the y-scale: bigger is up! +var x = d3.time.scale().rangeRound([0, w]), + y = d3.scale.ordinal().rangePoints([0, h], .5), + colors = d3.scale.category20(); + +// Axes. +var xAxis = d3.svg.axis().scale(x).tickSubdivide(true); +var yAxis = d3.svg.axis().scale(y).tickSize(0).tickPadding(5).orient("left"); + +// Add an SVG element with the desired dimensions and margin. +var svg = commits.append("svg") + .attr("width", width) + .attr("height", height) + .append("g") + .attr("transform", "translate(" + leftMargin + "," + topMargin + ")"); + +// Add the clip path. +svg.append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", w) + .attr("height", h); + +// Need something in the background absorb the mouse events! +svg.append("rect") + .attr("width", w) + .attr("height", h) + .style("fill-opacity", ".0"); + +function drawChart(allCommits, timelines) { + + var tickHeight = height / (timelines.length * 4 + 1); + + var earliestCommitDate = d3.min(timelines, function(d) { return d.earliest; }); + + var start = d3.time.format("%Y-%m-%d").parse(startDate) || earliestCommitDate; + + x.domain([d3.time.day.floor(start), d3.time.day.ceil(new Date())]); + + y.domain(timelines.map(function(t) { return t.repo; })); + + // Add the x-axis. + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + (h + topMargin + tickHeight) + ")") + .call(xAxis); + + // Add the y-axis. + svg.append("g") + .attr("class", "y axis") + .call(yAxis); + + svg.selectAll(".commit") + .data(allCommits) + .enter().append("line") + .attr("class", "commit") + .attr("clip-path", "url(#clip)") + .attr("x1", function(d) { return x(d.commitDate); }) + .attr("y1", function(d) { return Math.floor(y(d.repo)) - tickHeight; }) + .attr("x2", function(d) { return x(d.commitDate); }) + .attr("y2", function(d) { return Math.floor(y(d.repo)) + tickHeight; }) + .style("stroke", function(d) { return colors(d.committerEmail); }) + .on("mouseover", showCommitInfo); + + svg.selectAll(".timeline") + .data(timelines) + .enter().append("line") + .attr("class", "timeline") + .attr("clip-path", "url(#clip)") + .attr("x1", function(d) { return x(d.earliest); }) + .attr("y1", function(d) { return Math.floor(y(d.repo)); }) + .attr("x2", function(d) { return x(d.latest); }) + .attr("y2", function(d) { return Math.floor(y(d.repo)); }); + + svg.call(d3.behavior.zoom().x(x).on("zoom", zoom)); + + function zoom() { + svg.select(".x.axis").call(xAxis); + svg.selectAll(".commit") + .data(allCommits) + .attr("x1", function(d) { return x(d.commitDate); }) + .attr("x2", function(d) { return x(d.commitDate); }); + svg.selectAll(".timeline") + .data(timelines) + .attr("x1", function(d) { return x(d.earliest); }) + .attr("x2", function(d) { return x(d.latest); }); + svg.selectAll(".committer, .message") + .attr("x", function(d) { return x(d.commitDate); }); + } + + function showCommitInfo(d, i) { + + var committer = svg.selectAll(".committer") + .data([d], function(d) { return d.sha; }); + + committer.enter().append("text") + .attr("class", "committer") + .attr("x", x(d.commitDate)) + .attr("y", y(d.repo)) + .attr("dx", -2) + .attr("dy", 2 * tickHeight + 5) + .attr("text-anchor", "end") + .style("fill", colors(d.committerEmail)) + .text(d.committerName); + + committer.exit().remove(); + + var message = svg.selectAll(".message") + .data([d], function(d) { return d.sha; }); + + message.enter().append("text") + .attr("class", "message") + .attr("x", x(d.commitDate)) + .attr("y", y(d.repo)) + .attr("dx", 2) + .attr("dy", 2 * tickHeight + 5) + .text(d.message); + + message.exit().remove(); + } + +}