diff --git a/Gemfile b/Gemfile index 5c6bbd31..99870094 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ gem 'will_paginate', '~> 3.0' gem 'tubesock' gem 'faye-websocket' gem 'nokogiri' +gem 'd3-rails' group :development do gem 'better_errors', platform: :ruby diff --git a/Gemfile.lock b/Gemfile.lock index 5feafdc7..579c45e9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,6 +98,8 @@ GEM concurrent-ruby (1.0.0-java) concurrent-ruby-ext (1.0.0) concurrent-ruby (~> 1.0.0) + d3-rails (3.5.11) + railties (>= 3.1) database_cleaner (1.5.1) debug_inspector (0.0.2) diff-lcs (1.2.5) @@ -350,6 +352,7 @@ DEPENDENCIES coffee-rails (~> 4.0.0) concurrent-ruby (~> 1.0.0) concurrent-ruby-ext (~> 1.0.0) + d3-rails database_cleaner docker-api (~> 1.25.0) factory_girl_rails (~> 4.0) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index aa61da76..a78c7a67 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,6 +14,7 @@ // //= require ace/ace //= require chosen.jquery.min +//= require d3 //= require jquery.turbolinks //= require jquery_ujs //= require jstree/jstree.min diff --git a/app/assets/javascripts/working_time_graphs.js b/app/assets/javascripts/working_time_graphs.js new file mode 100644 index 00000000..d2f7c6fb --- /dev/null +++ b/app/assets/javascripts/working_time_graphs.js @@ -0,0 +1,177 @@ +$(function() { + + if ($.isController('exercises') && $('.graph-functions').isPresent()) { + var working_times = $('#data').data('working-time'); + + function get_minutes (time_stamp){ + try{ + hours = time_stamp.split(":")[0]; + minutes = time_stamp.split(":")[1]; + seconds = time_stamp.split(":")[2]; + + minutes = parseFloat(hours * 60) + parseInt(minutes); + return minutes + } catch (err){ + return 0; + } + + } + + minutes_array = _.map(working_times,function(item){return get_minutes(item)}); + maximum_minutes = _.max(minutes_array); + var minutes_count = new Array(maximum_minutes + 1); + + for (var i = 0; i < maximum_minutes; i++){ + for (var j = 0; j < minutes_array[i]; j++){ + if (minutes_count[j] == undefined){ + minutes_count[j] = 1; + } else{ + minutes_count[j] += 1; + } + } + } + + minutes_count[(maximum_minutes + 1)] = 0; + //$('.graph-functions').html("
") + //console.log(minutes_count) // THIS SHOWS THAT THE FINAL VALUES ARE 1 AND NOT ACTUALLY 0 + + // good to test at: localhost:3333/exercises/69/statistics + + var width_ratio = .8; + var height_ratio = .7; // percent of height + + + // currently sets as percentage of window width, however, unfortunately + // is not yet responsive + function getWidth() { + if (self.innerHeight) { + return self.innerWidth; + } + + if (document.documentElement && document.documentElement.clientWidth) { + return document.documentElement.clientWidth; + } + + if (document.body) { + return document.body.clientWidth; + } + } + + var margin = {top: 100, right: 20, bottom: 70, left: 70},//30,50 + width = (getWidth() * width_ratio) - margin.left - margin.right, + height = (width * height_ratio) - margin.top - margin.bottom; + + //var formatDate = d3.time.format("%M"); + + var x = d3.scale.linear() + .range([0, width]); + + var y = d3.scale.linear() + .range([height,0]); // - (height/20 + + var xAxis = d3.svg.axis() + .scale(x) + .orient("bottom") + .ticks(20); + + var yAxis = d3.svg.axis() + .scale(y) + .orient("left") + .ticks(20) + .innerTickSize(-width) + .outerTickSize(0); + + var line = d3.svg.line() + .x(function(d,i) { return x(i); }) + .y(function(d) { return y(d/minutes_count[0]*100); }); + + var svg = d3.select("#chart_1").append("svg") //PLACEMENT GOES HERE <--------------- + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + x.domain(d3.extent(minutes_count, function(d,i) { return (i); })); + y.domain(d3.extent(minutes_count, function(d) { return (d/minutes_count[0]*100); })); + + svg.append("g") //x axis + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + + svg.append("text")// x axis label + .attr("class", "x axis") + .attr("text-anchor", "middle") + .attr("x", width/2) + .attr("y", height) + .attr("dy", ((height/20)+20) + 'px') + .text("Time Spent on Assignment (Minutes)") + .style('font-size',14); + + svg.append("g") // y axis + .attr("class", "y axis") + .call(yAxis); + + svg.append("text") // y axis label + .attr("transform", "rotate(-90)") + .attr("x", -height/2) + .attr("dy", "-3em") + .style("text-anchor", "middle") + .text("Students (%)") + .style('font-size',14); + + svg.append("text")// Title + .attr("class", "x axis") + .attr("text-anchor", "middle") + .attr("x", (width/2))//+300) + .attr("y", 0) + .attr("dy", '-1.5em') + .text("Time Spent by Students on Exercise") + .style('font-size',20) + .style('text-decoration','underline'); + + svg.append("path") + .datum(minutes_count) + .attr("class", "line") + .attr('id','myPath')// new + .attr("stroke", "black") + .attr("stroke-width", 5) + .attr("fill", "none")// end new + .attr("d", line)//--- + .on("mousemove", mMove)//new again + .append("title"); + + + + + // THIS SHOULD DISPLAY THE X AND Y VALUES BUT + // THE RESULTS ARE WRONG AT THE END FOR SOME REASON + + //function mMove() { + // var x_width = getWidth() * width_ratio; + // //var x_value = m[0]*(minutes_count.length/x_width); + // + // var y_height = x_width * height_ratio; + // //var y_value = (((y_height - m[1])/y_height)*100); + // + // //console.log('y is: ' + y_value); + // var m = d3.mouse(this); + // d3.select("#myPath").select("title") + // .text((y_height-m[1])/(y_height) * 100 + "% of Students" +"\n"+ + // (m[0]*(minutes_count.length/x_width)) +" Minutes");//text(m[1]); + //} + + + + //svg.append("rect") // border + // .attr("x", 0) + // .attr("y", 0) + // .attr("height", height) + // .attr("width", width) + // .style("stroke", "#229") + // .style("fill", "none") + // .style("stroke-width", 3); + + } + +}); diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss index fffbcc23..68f49827 100644 --- a/app/assets/stylesheets/exercises.css.scss +++ b/app/assets/stylesheets/exercises.css.scss @@ -11,3 +11,30 @@ input[type='file'] { #exercise_template_code { font-family: monospace; } + +// Graph Settings + +.axis path { + fill: none; + stroke: #100; + shape-rendering: crispEdges; +} +.axis line { + fill: none; + stroke: #999; + //shape-rendering: crispEdges; +} + +.y.axis path { + display: none; +} + +.line { + fill: none; + stroke: orange;//steelblue; + stroke-width: 4px; +} + +div#chart_1{ + background-color: #FAFAFA; +} \ No newline at end of file diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss index ebabe3f7..f656cd9a 100644 --- a/app/assets/stylesheets/statistics.css.scss +++ b/app/assets/stylesheets/statistics.css.scss @@ -49,3 +49,4 @@ div.negative-result { -moz-box-shadow: 0px 0px 11px 1px rgba(222,0,0,1); box-shadow: 0px 0px 11px 1px rgba(222,0,0,1); } + diff --git a/app/views/exercises/statistics.html.slim b/app/views/exercises/statistics.html.slim index 717535be..d4a6d97a 100644 --- a/app/views/exercises/statistics.html.slim +++ b/app/views/exercises/statistics.html.slim @@ -13,6 +13,16 @@ h1 = @exercise - Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label| strong = label + -if symbol==:external_users + -working_time_array = [] + - @exercise.send(symbol).distinct().each do |user| + -working_time = @exercise.average_working_time_for(user.id) or 0 + -working_time_array.push working_time + hr + .hidden#data data-working-time=ActiveSupport::JSON.encode(working_time_array) + div#chart_1 + .graph-functions + hr .table-responsive table.table.table-striped.sortable thead