diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb new file mode 100644 index 00000000..7e048b55 --- /dev/null +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -0,0 +1,103 @@ +$(function() { + if ($.isController('exercise_collections')) { + var data = $('#data').data('working-times'); + var averageWorkingTimeValue = parseFloat($('#data').data('average-working-time')); + + var margin = { top: 30, right: 40, bottom: 30, left: 50 }, + width = 720 - margin.left - margin.right, + height = 500 - margin.top - margin.bottom; + + var x = d3.scaleBand().range([0, width]); + var y = d3.scaleLinear().range([height, 0]); + + var xAxis = d3.axisBottom(x); + var yAxisLeft = d3.axisLeft(y); + + var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip"); + + var averageWorkingTime = d3.line() + .x(function (d) { return x(d.index) + x.bandwidth()/2; }) + .y(function () { return y(averageWorkingTimeValue); }); + + var minWorkingTime = d3.line() + .x(function (d) { return x(d.index) + x.bandwidth()/2; }) + .y(function () { return y(0.1*averageWorkingTimeValue); }); + + var maxWorkingTime = d3.line() + .x(function (d) { return x(d.index) + x.bandwidth()/2; }) + .y(function () { return y(2*averageWorkingTimeValue); }); + + var svg = d3.select('#graph') + .append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", + "translate(" + margin.left + "," + margin.top + ")"); + + // Get the data + data = Object.keys(data).map(function (key, index) { + return { + index: index, + exercise_id: parseInt(key), + working_time: parseFloat(data[key]) + }; + }); + + // Scale the range of the data + x.domain(data.map(function (d) { return d.index; })); + y.domain([0, d3.max(data, function (d) { return d.working_time; })]); + + // Add the X Axis + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + + // Add the Y Axis + svg.append("g") + .attr("class", "y axis") + .style("fill", "steelblue") + .call(yAxisLeft); + + // Draw the bars + svg.selectAll("bar") + .data(data) + .enter() + .append("rect") + .style("fill", "#008cba") + .style("cursor", "pointer") + .on("mousemove", function (d){ + tooltip + .style("left", d3.event.pageX - 50 + "px") + .style("top", d3.event.pageY + 50 + "px") + .style("display", "inline-block") + .html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "
" + + "<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s"); + }) + .on("mouseout", function (){ tooltip.style("display", "none");}) + .on("click", function (d) { + window.location.href = "/exercises/" + d.exercise_id + "/statistics"; + }) + .attr("x", function (d) { return x(d.index); }) + .attr("width", x.bandwidth()) + .attr("y", function (d) { return y(d.working_time); }) + .attr("height", function (d) { return height - y(d.working_time); }); + + // Add the average working time path + svg.append("path") + .datum(data) + .attr("class", "line average-working-time") + .attr("d", averageWorkingTime); + + // Add the anomaly paths (min/max average exercise working time) + svg.append("path") + .datum(data) + .attr("class", "line minimum-working-time") + .attr("d", minWorkingTime); + svg.append("path") + .datum(data) + .attr("class", "line maximum-working-time") + .attr("d", maxWorkingTime); + } +}); diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss new file mode 100644 index 00000000..6a2d630e --- /dev/null +++ b/app/assets/stylesheets/exercise_collections.scss @@ -0,0 +1,22 @@ +path.line.minimum-working-time { + stroke: #8efa00; +} + +path.line.average-working-time { + stroke: #ffca00; +} + +path.line.maximum-working-time { + stroke: #ff2600; +} + +.exercise-id-tooltip { + position: absolute; + display: none; + min-width: 80px; + height: auto; + background: none repeat scroll 0 0 #ffffff; + border: 1px solid #008cba; + padding: 14px; + text-align: center; +} diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb index 249e7269..661bed81 100644 --- a/app/models/exercise_collection.rb +++ b/app/models/exercise_collection.rb @@ -4,12 +4,16 @@ class ExerciseCollection < ActiveRecord::Base has_and_belongs_to_many :exercises belongs_to :user, polymorphic: true - def average_working_time + def exercise_working_times working_times = {} exercises.each do |exercise| working_times[exercise.id] = time_to_f exercise.average_working_time end - working_times.values.reduce(:+) / working_times.size + working_times + end + + def average_working_time + exercise_working_times.values.reduce(:+) / exercises.size end def to_s diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim index 686c79c1..4a8a04ac 100644 --- a/app/views/exercise_collections/statistics.html.slim +++ b/app/views/exercise_collections/statistics.html.slim @@ -4,3 +4,6 @@ h1 = @exercise_collection = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) = row(label: 'exercise_collections.exercises', value: @exercise_collection.exercises.count) = row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's') + +#graph + #data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.exercise_working_times) data-average-working-time=@exercise_collection.average_working_time)