├── README.md └── d3.punchcard.js /README.md: -------------------------------------------------------------------------------- 1 | # d3.punchcard 2 | Inspired by GitHub's punchcard, I created one to track information in my applications. 3 | 4 | ![d3.punchcard](http://f.cl.ly/items/3c1A3o3S1z1O3c1p132l/punchcard.png) 5 | 6 | #### Footnote 7 | 8 | While there is great tracking libraries and products, sometimes it's fun to do it yourself. If you wan't to track registrations for your app over the last week, you can do the following via Ruby on Rails and dump the data to this JavaScript file. 9 | 10 | ```ruby 11 | week = Time.now.beginning_of_week - 1.week 12 | days = [week.strftime('%Y-%m-%d')] 13 | 1.upto(6) do |i| 14 | days << (week + i.days).strftime('%Y-%m-%d') 15 | end 16 | 17 | data = days.map do |day| 18 | by_hour = Array.new(24) { 0 } 19 | User. 20 | where('date(created_at) = ?', day). 21 | select('hour(created_at) as hour, count(*) as count'). 22 | group('hour(created_at)'). 23 | order('hour(created_at)'). 24 | map { |user| by_hour[user.hour] = user.count } 25 | 26 | by_hour 27 | end 28 | ``` 29 | -------------------------------------------------------------------------------- /d3.punchcard.js: -------------------------------------------------------------------------------- 1 | var pane_left = 120 2 | , pane_right = 800 3 | , width = pane_left + pane_right 4 | , height = 520 5 | , margin = 10 6 | , i 7 | , j 8 | , tx 9 | , ty 10 | , max = 0 11 | , data = [ 12 | [1, 0, 0, 0, 1, 1, 4, 5, 5, 1, 1, 1, 1, 1, 1, 2, 5, 5, 4, 1, 1, 1, 1, 0], 13 | [1, 0, 0, 0, 1, 1, 4, 5, 5, 1, 1, 1, 1, 1, 1, 2, 5, 5, 4, 1, 1, 1, 1, 0], 14 | [1, 0, 0, 0, 1, 1, 4, 5, 5, 1, 1, 1, 1, 1, 1, 2, 5, 5, 4, 1, 1, 1, 1, 0], 15 | [1, 0, 0, 0, 1, 1, 4, 5, 5, 1, 1, 1, 1, 1, 1, 2, 5, 5, 4, 1, 1, 1, 1, 0], 16 | [1, 0, 0, 0, 1, 1, 4, 5, 5, 1, 1, 1, 1, 1, 1, 2, 5, 5, 4, 1, 1, 1, 1, 0], 17 | [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 0], 18 | [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 0, 0] 19 | ]; 20 | 21 | // X-Axis. 22 | var x = d3.scale.linear().domain([0, 23]). 23 | range([pane_left + margin, pane_right - 2 * margin]); 24 | 25 | // Y-Axis. 26 | var y = d3.scale.linear().domain([0, 6]). 27 | range([2 * margin, height - 10 * margin]); 28 | 29 | // The main SVG element. 30 | var punchcard = d3. 31 | select("#punchcard"). 32 | append("svg"). 33 | attr("width", width - 2 * margin). 34 | attr("height", height - 2 * margin). 35 | append("g"); 36 | 37 | // Hour line markers by day. 38 | for (i in y.ticks(7)) { 39 | punchcard. 40 | append("g"). 41 | selectAll("line"). 42 | data([0]). 43 | enter(). 44 | append("line"). 45 | attr("x1", margin). 46 | attr("x2", width - 3 * margin). 47 | attr("y1", height - 3 * margin - y(i)). 48 | attr("y2", height - 3 * margin - y(i)). 49 | style("stroke-width", 1). 50 | style("stroke", "#efefef"); 51 | 52 | punchcard. 53 | append("g"). 54 | selectAll(".rule"). 55 | data([0]). 56 | enter(). 57 | append("text"). 58 | attr("x", margin). 59 | attr("y", height - 3 * margin - y(i) - 5). 60 | attr("text-anchor", "left"). 61 | text(["Sunday", "Saturday", "Friday", "Thursday", "Wednesday", "Tuesday", "Monday"][i]); 62 | 63 | punchcard. 64 | append("g"). 65 | selectAll("line"). 66 | data(x.ticks(24)). 67 | enter(). 68 | append("line"). 69 | attr("x1", function(d) { return pane_left - 2 * margin + x(d); }). 70 | attr("x2", function(d) { return pane_left - 2 * margin + x(d); }). 71 | attr("y1", height - 4 * margin - y(i)). 72 | attr("y2", height - 3 * margin - y(i)). 73 | style("stroke-width", 1). 74 | style("stroke", "#ccc"); 75 | } 76 | 77 | // Hour text markers. 78 | punchcard. 79 | selectAll(".rule"). 80 | data(x.ticks(24)). 81 | enter(). 82 | append("text"). 83 | attr("class", "rule"). 84 | attr("x", function(d) { return pane_left - 2 * margin + x(d); }). 85 | attr("y", height - 3 * margin). 86 | attr("text-anchor", "middle"). 87 | text(function(d) { 88 | if (d === 0) { 89 | return "12a"; 90 | } else if (d > 0 && d < 12) { 91 | return d; 92 | } else if (d === 12) { 93 | return "12p"; 94 | } else if (d > 12 && d < 25) { 95 | return d - 12; 96 | } 97 | }); 98 | 99 | // Data has array where indicy 0 is Monday and 6 is Sunday, however we draw 100 | // from the bottom up. 101 | data = data.reverse(); 102 | 103 | // Find the max value to normalize the size of the circles. 104 | for (i = 0; i < data.length; i++) { 105 | max = Math.max(max, Math.max.apply(null, data[i])); 106 | } 107 | 108 | // Show the circles on the punchcard. 109 | for (i = 0; i < data.length; i++) { 110 | for (j = 0; j < data[i].length; j++) { 111 | punchcard. 112 | append("g"). 113 | selectAll("circle"). 114 | data([data[i][j]]). 115 | enter(). 116 | append("circle"). 117 | style("fill", "#888"). 118 | on("mouseover", mover). 119 | on("mouseout", mout). 120 | on("mousemove", function() { 121 | return tooltip. 122 | style("top", (d3.event.pageY - 10) + "px"). 123 | style("left", (d3.event.pageX + 10) + "px"); 124 | }). 125 | attr("r", function(d) { return d / max * 14; }). 126 | attr("transform", function() { 127 | tx = pane_left - 2 * margin + x(j); 128 | ty = height - 7 * margin - y(i); 129 | return "translate(" + tx + ", " + ty + ")"; 130 | }); 131 | } 132 | function mover(d) { 133 | tooltip = d3.select("body") 134 | .append("div") 135 | .style("position", "absolute") 136 | .style("z-index", "99999") 137 | .attr("class", "vis-tool-tip") 138 | .text(d); 139 | } 140 | 141 | function mout(d) { 142 | $(".vis-tool-tip").fadeOut(50).remove(); 143 | } 144 | } 145 | --------------------------------------------------------------------------------