E1859 Visualizations for Instructors: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
 
(24 intermediate revisions by 3 users not shown)
Line 41: Line 41:
==== Tools and Design Choice ====
==== Tools and Design Choice ====


We plan to use the lightweight [https://developers.google.com/chart/ Google Charts] library for displaying the chart data on the page, with standard HTML for all of the options and dropdowns for option selection. Google Charts was chosen because of its high compatibility, full option set, and comparable graphical quality to the rest of expertiza while keeping a small JS footprint, which should help prevent slow page responsiveness.
We have used the lightweight [https://developers.google.com/chart/ Google Charts] library for displaying the chart data on the page, with standard HTML for all of the options and dropdowns for option selection. Google Charts was chosen because of its high compatibility, full option set, and comparable graphical quality to the rest of expertiza while keeping a small JS footprint, which should help prevent slow page responsiveness.


==== Visualization ====
==== Visualization ====


The following graph shows the expected view of the 'view scores' page. The instructor selects a subset of rubric criteria for which he/she wants to know how a class performed. A bar graph of the average score of the class for that subset of criteria would be displayed.  
The following graph shows the view of the 'view scores' page after the modifications. The instructor selects a subset of rubric criteria for which he/she wants to know how a class performed for a particular round. A bar graph of the average score of the class for that subset of criteria is displayed.  
 
[[File:1859_Chart_Preview.png]]
 
The above graph shows average score of the class for 5 rubric criteria in Round 1 selected by the instructor. A live demo with randomly generated data can be found on [https://jsfiddle.net/Jereman/5uxqr92y/ JSFiddle]


[[File:Screenshot (223).png]]


The above graph shows an average score of the class for 10 rubric criteria in Round 1 for assignment "OSS project/Writing assignment 2" selected by the instructor. A live demo with randomly generated data can be found on [https://jsfiddle.net/Jereman/5uxqr92y/ JSFiddle]


== Files Involved ==
== Files Involved ==
We plan to work on the files that the previous team were involved with such as controllers of grade and assignment and the view of grade and review_mapping. Hopefully there is only slight controller modifications necessary, as the chart can use the existing information collected by controller methods to display the raw details on the existing page.
We have implemented a new partial file criteria_charts to the team_chart that display the bar graph with existing data collected by the grades controller methods to the view page.


Proposed files:
Modified files:
<div style="display: inline-block">
<div style="display: inline-block">
<pre>
<pre>
app/controllers/assignments_controller.rb
app/controllers/grades_controller.rb
 
app/views/grades/view.html.erb


app/views/grades/_teams.html.erb
app/views/grades/_teams.html.erb


app/views/grades/_team_title.html.erb
app/views/grades/_criteria_charts.html.erb


app/views/grades/_team_charts.html.erb
app/views/grades/_team_charts.html.erb
app/view/review_mapping/_review_report.html.erb
</pre>
</pre>
</div>
</div>


=== Code ===
=== Code ===
== app/views/grades/_criteria_charts.html.erb ==
== app/views/grades/_criteria_charts.html.erb ==
<html>
<html>
   <head>
   <head>
  <%= content_tag :div, class: "chartdata_information", data: {chartdata: @chartdata} do %>
  <%= content_tag :div, class: "chartdata_information", data: {chartdata: @chartdata} do %>
Line 83: Line 78:
     <%= content_tag :div, class: "minmax_information", data: {minmax: @minmax} do %>
     <%= content_tag :div, class: "minmax_information", data: {minmax: @minmax} do %>
     <% end %>
     <% end %>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
     <script type="text/javascript">
     <script type="text/javascript">
       google.charts.load('current', {packages: ['corechart', 'bar']});
       google.charts.load('current', {packages: ['corechart', 'bar']});
@@ -55,29 +22,35 @@
       //Display Options
       //Display Options
       var showLabels = true;
       var showLabels = true;
Line 99: Line 93:
         chartText = $('.text_information').data('text');
         chartText = $('.text_information').data('text');
         chartRange = $('.minmax_information').data('minmax');
         chartRange = $('.minmax_information').data('minmax');
for (var i = 0; i < chartData.length; i++){ //Set all the criteriaSelected to true
for (var i = 0; i < chartData.length; i++){ //Set all the criteriaSelected to true
           var criteria = [];
           var criteria = [];
           for (var j = 0; j < chartData[i].length; j++){
           for (var j = 0; j < chartData[i].length; j++){
Line 110: Line 104:
         var rounds = 3;
         var rounds = 3;
         for(var i = 0; i < rounds; i++) {
         for(var i = 0; i < rounds; i++) {
var criteriaNum = Math.floor(Math.random() * 5 + 5);  //Random number of criteria
  var criteriaNum = Math.floor(Math.random() * 5 + 5);  //Random number of criteria
           var round = [];
           var round = [];
           var criteria = [];
           var criteria = [];
           for(var j = 0; j < criteriaNum; j++) {
           for(var j = 0; j < criteriaNum; j++) {
round.push(Math.floor(Math.random() * 101));  //Random score for each criterion
  round.push(Math.floor(Math.random() * 101));  //Random score for each criterion
             criteria.push(true);  //Everything starts out true
             criteria.push(true);  //Everything starts out true
           }
           }
           chartData.push(round);
           chartData.push(round);
           criteriaSelected.push(criteria);
           criteriaSelected.push(criteria);
@@ -89,7 +62,7 @@
         chartOptions = { //Render options for the chart
         chartOptions = { //Render options for the chart
           title: 'Class Average on Criteria',
           title: 'Class Average on Criteria',
Line 127: Line 121:
             italic: false,
             italic: false,
             bold: true
             bold: true
@@ -130,12 +103,12 @@
        chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
        chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
         var checkBox = document.getElementById("labelCheck");
         var checkBox = document.getElementById("labelCheck");
         checkBox.checked = true;
         checkBox.checked = true;
Line 134: Line 127:
         updateChart(currentRound);
         updateChart(currentRound);
         loadRounds(); }
         loadRounds(); }
function updateChart(roundNum) { //Updates the chart with a new round number and renders
function updateChart(roundNum) { //Updates the chart with a new round number and renders
         currentRound = roundNum;
         currentRound = roundNum;
         renderChart();
         renderChart();
         loadCriteria();
         loadCriteria();
@@ -150,18 +123,13 @@
      function renderChart() { //Renders the chart if changes have been made
      function renderChart() { //Renders the chart if changes have been made
           var data = loadData();
           var data = loadData();
  chartOptions.vAxis.viewWindow.max = 5;
  chartOptions.vAxis.viewWindow.max = 5;
Line 148: Line 140:
  if (chartRange[currentRound][0])
  if (chartRange[currentRound][0])
                   chartOptions.vAxis.viewWindow.min = chartRange[currentRound][0];
                   chartOptions.vAxis.viewWindow.min = chartRange[currentRound][0];
}
}
           chart.draw(data, chartOptions);
           chart.draw(data, chartOptions);
       }
       }
@@ -177,9 +145,9 @@
           chartOptions.hAxis.ticks = [];
           chartOptions.hAxis.ticks = [];
           var rowCount = 1;
           var rowCount = 1;
Line 160: Line 152:
               }
               }
           }
           }
@@ -192,7 +160,7 @@
           var data = new google.visualization.DataTable();
           var data = new google.visualization.DataTable();
           data.addColumn('number', 'Criterion');
           data.addColumn('number', 'Criterion');
           var i;
           var i;
for(i = 0; i < roundNum; i++) { //Add all columns for the data
for(i = 0; i < roundNum; i++) { //Add all columns for the data
               data.addColumn('number', 'Round ' + (i+1).toString());
               data.addColumn('number', 'Round ' + (i+1).toString());
               data.addColumn({type: 'string', role: 'style'}); //column for specifying the bar color
               data.addColumn({type: 'string', role: 'style'}); //column for specifying the bar color
               data.addColumn({type: 'string', role: 'annotation'});
               data.addColumn({type: 'string', role: 'annotation'});
@@ -208,16 +176,16 @@
               var newRow = [];
               var newRow = [];
               var elementsAdded = false;
               var elementsAdded = false;
Line 181: Line 173:
   newRow.push(barColors[j % barColors.length]); //Add column color
   newRow.push(barColors[j % barColors.length]); //Add column color
                   if (chartData[j] && chartData[j][i] && showLabels)
                   if (chartData[j] && chartData[j][i] && showLabels)
newRow.push(chartData[j][i].toFixed(1).toString()); //Add column annotations
  newRow.push(chartData[j][i].toFixed(1).toString()); //Add column annotations
                   else
                   else
                       newRow.push("");
                       newRow.push("");
               }
               }
@@ -249,15 +217,15 @@
       function loadCriteria() { //Creates the criteria check boxes
       function loadCriteria() { //Creates the criteria check boxes
           var form = document.getElementById("chartCriteria");
           var form = document.getElementById("chartCriteria");
while (form.firstChild) //Clear out the old check boxes
while (form.firstChild) //Clear out the old check boxes
               form.removeChild(form.firstChild);
               form.removeChild(form.firstChild);
  if (currentRound == -1) //Don't show criteria for 'all rounds'
  if (currentRound == -1) //Don't show criteria for 'all rounds'
Line 200: Line 191:
               }
               }
               var label = document.createElement('label')
               var label = document.createElement('label')
@@ -285,7 +253,7 @@
     <form id="chartOptions" name="chartOptions">
     <form id="chartOptions" name="chartOptions">
       <select id="chartRounds" name="rounds" onChange="updateChart(document.chartOptions.chartRounds.options[document.chartOptions.chartRounds.options.selectedIndex].value)" style = "display: none">
       <select id="chartRounds" name="rounds"  
onChange="updateChart(document.chartOptions.chartRounds.options[document.chartOptions.chartRounds.options.selectedIndex].value)" style = "display: none">
       </select>
       </select>
  <label><input type="checkbox" id = "labelCheck" checked="checked" style="display: none" onclick="showLabels = !showLabels; renderChart();">Show Labels</label>
  <label><input type="checkbox" id = "labelCheck" checked="checked" style="display: none" onclick="showLabels = !showLabels; renderChart();">Show  
Labels</label>
     </form>
     </form>
     <div id="chartCriteria" name="chartCriteria">
     <div id="chartCriteria" name="chartCriteria">
     </div>
     </div>
== app/controllers/grades_controller.rb ==
def action_allowed?
    case params[:action]
    when 'view_my_scores'
      ['Instructor',
        'Teaching Assistant',
        'Administrator',
        'Super-Administrator',
      'Student'].include? current_role_name and
        are_needed_authorizations_present?(params[:id], "reader", "reviewer") and
        check_self_review_status
when 'view_team'
      if ['Student'].include? current_role_name # students can only see the head map for their own team
        participant = AssignmentParticipant.find(params[:id])
        session[:user].id == participant.user_id
      else
true
      end
    else
      ['Instructor',
      'Teaching Assistant',
      'Administrator',
      'Super-Administrator'].include? current_role_name
    end
  end
  # collects the question text for display on the chart
  # Added as part of E1859
  def assign_chart_text
    @text = []
    (1..@assignment.num_review_rounds).to_a.each do |round|
      question = @questions[('review' + round.to_s).to_sym]
      @text[round - 1] = []
      next if question.nil?
      (0..(question.length - 1)).to_a.each do |q|
        @text[round - 1][q] = question[q].txt
      end
    end
  end
  # find the maximum and minimum scores for each questionnaire round
  # Added as part of E1859
  def assign_minmax(questionnaires)
    @minmax = []
    questionnaires.each do |questionnaire|
      next if questionnaire.symbol != :review
      round = AssignmentQuestionnaire.where(assignment_id: @assignment.id, questionnaire_id: questionnaire.id).first.used_in_round
      next if round.nil?
  @minmax[round - 1] = []
      @minmax[round - 1][0] = if !questionnaire.min_question_score.nil? and questionnaire.min_question_score < 0
                                questionnaire.min_question_score
                              else
                                0
                              end
      @minmax[round - 1][1] = if !questionnaire.max_question_score.nil?
                                questionnaire.max_question_score
                              else
                                5
                              end
    end
  end
# this method collects and averages all the review scores across teams
  # Added as part of E1859
  def assign_chart_data
    @rounds = @assignment.num_review_rounds
    @chartdata = []
    (1..@rounds).to_a.each do |round|
      @teams = AssignmentTeam.where(parent_id: @assignment.id)
      @teamids = []
      @result = []
      @responseids = []
      @scoreviews = []
      (0..(@teams.length - 1)).to_a.each do |t|
        @teamids[t] = @teams[t].id
        @result[t] = ResponseMap.find_by_sql ["SELECT id FROM response_maps
          WHERE type = 'ReviewResponseMap' AND reviewee_id = ?", @teamids[t]]
        @responseids[t] = []
        @scoreviews[t] = []
        (0..(@result[t].length - 1)).to_a.each do |r|
          @responseids[t][r] = Response.find_by_sql ["SELECT id FROM responses
            WHERE round = ? AND map_id = ?", round, @result[t][r]]
          @scoreviews[t][r] = Answer.where(response_id: @responseids[t][r][0]) unless @responseids[t][r].empty?
        end
      end
      @chartdata[round - 1] = []
      # because the nth first elements could be nil
      # iterate until a non-nil value is found or move to next round
      t = 0
      r = 0
t += 1 while @scoreviews[t].nil?
      while t < @scoreviews.length and @scoreviews[t][r].nil?
        if r < @scoreviews[t].length - 1
          r += 1
        else
def assign_chart_data
        end
      end
      next if t >= @scoreviews.length
(0..(@scoreviews[t][r].length - 1)).to_a.each do |q|
        sum = 0
        counter = 0
def retrieve_questions(questionnaires)
    questionnaires.each do |questionnaire|
      round = AssignmentQuestionnaire.where(assignment_id: @assignment.id, questionnaire_id: questionnaire.id).first.used_in_round
      questionnaire_symbol = if !round.nil?
                        (questionnaire.symbol.to_s + round.to_s).to_sym
                            else
                              questionnaire.symbol
                            end
      @questions[questionnaire_symbol] = questionnaire.questions
    end
  end
def update
    if format("%.2f", total_score) != params[:participant][:grade]
      participant.update_attribute(:grade, params[:participant][:grade])
      message = if participant.grade.nil?
          "The computed score will be used for " + participant.user.name + "."
                else
                  "A score of " + params[:participant][:grade] + "% has been saved for " + participant.user.name + "."
                end
    end
    flash[:note] = message
    redirect_to action: 'edit', id: params[:id]
== app/views/grades/_team_charts.html.erb ==
<pre>
  <br> <br>
  <%= render :partial => 'criteria_charts' %>
  <br>
  <a href="#" name='team-chartLink' onClick="toggleElement('team-chart', 'stats');return false;">Hide stats</a>
  <br>
  <TR style ="background-color: white;" class="team" id="team-chart">
    <th>
    <div class="circle" id="average-score">
    </div>
      Class Average
    </th>
    <TH COLSPAN="8">
    <img src="<%= @average_chart  %>" ><br>
    Class Distribution
    <br>
    </th>
    <TH WIDTH="9">&nbsp;</th>
  </TR>
  <script type="text/javascript">
    var myCircle = Circles.create({
      id:          'average-score',
      radius:      50,
      value:        <%= @avg_of_avg.to_i %>,
      maxValue:    100,
      width:        15,
      text:        '<%=@avg_of_avg.to_i%>',
      colors:      ['#FFEB99', '#FFCC00'],
      duration:      700,
      textClass:      'circles-final'
    });
  </script>
  <style>
  .circles-final{
      font-size: 16px !important;
    }
  </style>
</pre>


== Test Plan ==
== Test Plan ==
The specification of the project does not require us to use automated tests. However, we plan to test the modified ruby code or the new features that will be added to the grades_controller. We will be approaching [http://rspec.info/ RSpec] testing framework. For the javascript, the tests will be conducted manually.
The specification of the project does not require us to use automated tests. However, we have tested the existing tests after the addition of new features to the grades_controller. We have approached [http://rspec.info/ RSpec] testing framework. The UI feature tests are conducted manually.


===JavaScript Chart===
===JavaScript Chart===
Line 230: Line 392:


===RSpec Tests===
===RSpec Tests===
RSpec tests were added to validate functionality of the helper methods that were added to the grades controller.
We have modified one of the existing tests for grades controller because the addition of new functionality broke that particular test. The added test handles the chart functionality correctly.
 
== spec/controllers/grades_controller_spec.rb ==
    describe '#view' do
    before(:each) do
      allow(Answer).to receive(:compute_scores).with([review_response], [question]).and_return(max: 95, min: 88, avg: 90)
      allow(Participant).to receive(:where).with(parent_id: 1).and_return([participant])
      allow(AssignmentParticipant).to receive(:find).with(1).and_return(participant)
      allow(assignment).to receive(:late_policy_id).and_return(false)
      allow(assignment).to receive(:calculate_penalty).and_return(false)
    end
    context 'when current assignment does not vary rubric by round' do
      it 'calculates scores and renders grades#view page' do
        allow(AssignmentQuestionnaire).to receive(:where).with(assignment_id: 1, used_in_round: 2).and_return([])
        # added AssignmentQuestionnaire 'allow' below to handle chart functionality added in E1859
        allow(AssignmentQuestionnaire).to receive(:where).with(assignment_id: 1, questionnaire_id: 1).and_return([assignment_questionnaire])
        allow(ReviewResponseMap).to receive(:get_assessments_for).with(team).and_return([review_response])
        params = {id: 1}
        get :view, params
        expect(controller.instance_variable_get(:@questions)[:review].size).to eq(1)
        expect(response).to render_template(:view)
      end
    end
  end

Latest revision as of 13:16, 11 December 2018

Introduction

Expertiza is an online assignment grading platform. Instructors can create assignments and implement peer reviews for submitted assignments. This project concerns the creation of a system for visualizing student performance on those assignments, primarily as graded in peer reviews. Graphs will be made to show various rubric criteria and the class' performance on the criteria. If the criteria are the same for multiple stages of review, an instructor should be able to compare performance over time or between reviews.



Project Purpose

Our task is to provide an interactive visualization or a table for instructors that shows how their class performed on selected rubric criteria. Such feature would be immensely helpful for instructors as it would assist them to identify what they need to focus more attention on. For example, creating a graph showing the average scores for all or a certain subset of main rubric criteria (questionnaire). If the average score of the class on selected criteria (question) is low means the instructor can emphasize more on the learning materials related to it.

Proposed Changes

The visualizations will be implemented as either a single or stacked bar chart with a bar for each of the selected criteria to be observed. If a single bar, then the height of the bar will be the total class average, but a stacked bar chart may be better to show the percentage of the class that received each score. The changes made to the expertiza project will primarily include HTML/ERB changes to the view files to accommodate the added charts on the page and the necessary javascript to allow responsive design. Brief controller modifications will be made to facilitate database filtering to get the displayed data.


1. On clicking Manage and then on assignments, following page appears.

2. Click on 'view score' icon of an assignment. The summary report page of the selected assignment comes up.

3. Following are mockup screens which we wish to create:

a) Instructor would select the round and rubric criteria of the assignment for which he/she wants to view the class performance.

b) The bar graph of the class performance for those criteria would be displayed.

Project Design

Design Flow

The flowchart representing graphical flow of an instructor visiting view scores under assignments is given below:

Tools and Design Choice

We have used the lightweight Google Charts library for displaying the chart data on the page, with standard HTML for all of the options and dropdowns for option selection. Google Charts was chosen because of its high compatibility, full option set, and comparable graphical quality to the rest of expertiza while keeping a small JS footprint, which should help prevent slow page responsiveness.

Visualization

The following graph shows the view of the 'view scores' page after the modifications. The instructor selects a subset of rubric criteria for which he/she wants to know how a class performed for a particular round. A bar graph of the average score of the class for that subset of criteria is displayed.

The above graph shows an average score of the class for 10 rubric criteria in Round 1 for assignment "OSS project/Writing assignment 2" selected by the instructor. A live demo with randomly generated data can be found on JSFiddle

Files Involved

We have implemented a new partial file criteria_charts to the team_chart that display the bar graph with existing data collected by the grades controller methods to the view page.

Modified files:

app/controllers/grades_controller.rb

app/views/grades/_teams.html.erb

app/views/grades/_criteria_charts.html.erb

app/views/grades/_team_charts.html.erb

Code

app/views/grades/_criteria_charts.html.erb

<html>
 <head>
<%= content_tag :div, class: "chartdata_information", data: {chartdata: @chartdata} do %>
   <% end %>
<%= content_tag :div, class: "text_information", data: {text: @text} do %>
   <% end %>
   <%= content_tag :div, class: "minmax_information", data: {minmax: @minmax} do %>
   <% end %>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
   <script type="text/javascript">
     google.charts.load('current', {packages: ['corechart', 'bar']});
      //Display Options
     var showLabels = true;
     var barColors = [ // Other colors generated from the expertiza base red
         '#A90201',    // using paletton.com
         '#018701',
         '#016565',
         '#A94D01'
     ];
     function getData(){ //Loads all chart data from the page
       chartData = $('.chartdata_information').data('chartdata');
       chartText = $('.text_information').data('text');
       chartRange = $('.minmax_information').data('minmax');
for (var i = 0; i < chartData.length; i++){ //Set all the criteriaSelected to true
         var criteria = [];
         for (var j = 0; j < chartData[i].length; j++){
           criteria.push(true);
         }
         criteriaSelected.push(criteria);
       }
    }
      function generateData() {	//Generates random data for testing
       var rounds = 3;
       for(var i = 0; i < rounds; i++) {
 var criteriaNum = Math.floor(Math.random() * 5 + 5);  //Random number of criteria
         var round = [];
         var criteria = [];
         for(var j = 0; j < criteriaNum; j++) {
 round.push(Math.floor(Math.random() * 101));  //Random score for each criterion
           criteria.push(true);  //Everything starts out true
         }
         chartData.push(round);
         criteriaSelected.push(criteria);

       chartOptions = {		//Render options for the chart
         title: 'Class Average on Criteria',
         titleTextStyle: {
 fontName: 'arial',
           fontSize: 18,
           italic: false,
           bold: true
        chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
       var checkBox = document.getElementById("labelCheck");
       checkBox.checked = true;
       checkBox.style.display = "inline";
       updateChart(currentRound);
       loadRounds(); }
function updateChart(roundNum) {	//Updates the chart with a new round number and renders
       currentRound = roundNum;
       renderChart();
       loadCriteria();
     function renderChart() {	//Renders the chart if changes have been made
         var data = loadData();
chartOptions.vAxis.viewWindow.max = 5;
         chartOptions.vAxis.viewWindow.min = 0;
         if (chartRange[currentRound]) { //Set axis ranges if they exist
             if (chartRange[currentRound][1])
                 chartOptions.vAxis.viewWindow.max = chartRange[currentRound][1];
if (chartRange[currentRound][0])
                 chartOptions.vAxis.viewWindow.min = chartRange[currentRound][0];
}
         chart.draw(data, chartOptions);
     }

          chartOptions.hAxis.ticks = [];
         var rowCount = 1;
for(var i = 0; i < chartData[currentRound].length; i++) { //Add a chart row for each criterion if not null
             if (criteriaSelected[currentRound][i] && chartData[currentRound][i]) {
 data.addRow([rowCount, chartData[currentRound][i], barColors[0], (showLabels) ? chartData[currentRound][i].toFixed(1).toString() : ""]);
                 chartOptions.hAxis.ticks.push({v: rowCount++, f: (i+1).toString()});
             }
         }

         var data = new google.visualization.DataTable();
         data.addColumn('number', 'Criterion');
         var i;
for(i = 0; i < roundNum; i++) { //Add all columns for the data
             data.addColumn('number', 'Round ' + (i+1).toString());
             data.addColumn({type: 'string', role: 'style'});	//column for specifying the bar color
             data.addColumn({type: 'string', role: 'annotation'});

             var newRow = [];
             var elementsAdded = false;
             newRow.push(rowCount);
for(var j = 0; j < roundNum; j++) { //If the round has the criterion, add it
                 if (chartData[j][i]) {
                     newRow.push(chartData[j][i]);
                     elementsAdded = true;
                 } else {
                     newRow.push(null);
                 }
 newRow.push(barColors[j % barColors.length]); //Add column color
                 if (chartData[j] && chartData[j][i] && showLabels)
 newRow.push(chartData[j][i].toFixed(1).toString()); //Add column annotations
                 else
                     newRow.push("");
             }
      function loadCriteria() {	//Creates the criteria check boxes
         var form = document.getElementById("chartCriteria");
while (form.firstChild) //Clear out the old check boxes
             form.removeChild(form.firstChild);
if (currentRound == -1) //Don't show criteria for 'all rounds'
             return;
         chartData[currentRound].forEach(function(dat, i) {
             var checkbox = document.createElement('input');
             checkbox.type = "checkbox";
             checkbox.id = "checkboxoption" + i;
checkbox.onclick = function() { //Register callback to toggle the criterion
                 checkboxUpdate(i);
             }
             var label = document.createElement('label')

   <form id="chartOptions" name="chartOptions">
     <select id="chartRounds" name="rounds" 
onChange="updateChart(document.chartOptions.chartRounds.options[document.chartOptions.chartRounds.options.selectedIndex].value)" style = "display: none">
     </select>
<label><input type="checkbox" id = "labelCheck" checked="checked" style="display: none" onclick="showLabels = !showLabels; renderChart();">Show 
Labels</label>
   </form>

app/controllers/grades_controller.rb

def action_allowed?
    case params[:action]
    when 'view_my_scores'
      ['Instructor',
       'Teaching Assistant',
       'Administrator',
       'Super-Administrator',
      'Student'].include? current_role_name and
       are_needed_authorizations_present?(params[:id], "reader", "reviewer") and
       check_self_review_status
when 'view_team'
     if ['Student'].include? current_role_name # students can only see the head map for their own team
       participant = AssignmentParticipant.find(params[:id])
       session[:user].id == participant.user_id
     else
true
     end
   else
     ['Instructor',
      'Teaching Assistant',
      'Administrator',
      'Super-Administrator'].include? current_role_name
   end
 end
  # collects the question text for display on the chart
  # Added as part of E1859
  def assign_chart_text
    @text = []
    (1..@assignment.num_review_rounds).to_a.each do |round|
      question = @questions[('review' + round.to_s).to_sym]
      @text[round - 1] = []
      next if question.nil?
      (0..(question.length - 1)).to_a.each do |q|
        @text[round - 1][q] = question[q].txt
      end
    end
  end

  # find the maximum and minimum scores for each questionnaire round
  # Added as part of E1859
  def assign_minmax(questionnaires)
    @minmax = []
    questionnaires.each do |questionnaire|
     next if questionnaire.symbol != :review
     round = AssignmentQuestionnaire.where(assignment_id: @assignment.id, questionnaire_id: questionnaire.id).first.used_in_round
     next if round.nil?
 @minmax[round - 1] = []
     @minmax[round - 1][0] = if !questionnaire.min_question_score.nil? and questionnaire.min_question_score < 0
                               questionnaire.min_question_score
                             else
                               0
                             end
     @minmax[round - 1][1] = if !questionnaire.max_question_score.nil?
                               questionnaire.max_question_score
                             else
                               5
                             end
   end
 end
# this method collects and averages all the review scores across teams
  # Added as part of E1859
  def assign_chart_data
    @rounds = @assignment.num_review_rounds
    @chartdata = []
    (1..@rounds).to_a.each do |round|
      @teams = AssignmentTeam.where(parent_id: @assignment.id)
      @teamids = []
      @result = []
      @responseids = []
      @scoreviews = []
      (0..(@teams.length - 1)).to_a.each do |t|
        @teamids[t] = @teams[t].id
        @result[t] = ResponseMap.find_by_sql ["SELECT id FROM response_maps
          WHERE type = 'ReviewResponseMap' AND reviewee_id = ?", @teamids[t]]
        @responseids[t] = []
        @scoreviews[t] = []
        (0..(@result[t].length - 1)).to_a.each do |r|
          @responseids[t][r] = Response.find_by_sql ["SELECT id FROM responses
            WHERE round = ? AND map_id = ?", round, @result[t][r]]
         @scoreviews[t][r] = Answer.where(response_id: @responseids[t][r][0]) unless @responseids[t][r].empty?
       end
     end
     @chartdata[round - 1] = []
     # because the nth first elements could be nil
     # iterate until a non-nil value is found or move to next round
     t = 0
     r = 0
t += 1 while @scoreviews[t].nil?
     while t < @scoreviews.length and @scoreviews[t][r].nil?
       if r < @scoreviews[t].length - 1
         r += 1
       else
def assign_chart_data
       end
     end
     next if t >= @scoreviews.length
(0..(@scoreviews[t][r].length - 1)).to_a.each do |q|
       sum = 0
       counter = 0
def retrieve_questions(questionnaires)
   questionnaires.each do |questionnaire|
     round = AssignmentQuestionnaire.where(assignment_id: @assignment.id, questionnaire_id: questionnaire.id).first.used_in_round
     questionnaire_symbol = if !round.nil?
                        (questionnaire.symbol.to_s + round.to_s).to_sym
                            else
                              questionnaire.symbol
                            end
     @questions[questionnaire_symbol] = questionnaire.questions
   end
 end
def update
   if format("%.2f", total_score) != params[:participant][:grade]
     participant.update_attribute(:grade, params[:participant][:grade])
     message = if participant.grade.nil?
          "The computed score will be used for " + participant.user.name + "."
               else
                 "A score of " + params[:participant][:grade] + "% has been saved for " + participant.user.name + "."
               end
   end
   flash[:note] = message
   redirect_to action: 'edit', id: params[:id]

app/views/grades/_team_charts.html.erb

  <br> <br>
  <%= render :partial => 'criteria_charts' %>
  <br>

  <a href="#" name='team-chartLink' onClick="toggleElement('team-chart', 'stats');return false;">Hide stats</a>
  <br>
  <TR style ="background-color: white;" class="team" id="team-chart">
    <th>

    <div class="circle" id="average-score">

    </div>
      Class Average
    </th>
    <TH COLSPAN="8">
    	<img src="<%= @average_chart  %>" ><br>
    	Class Distribution
    	<br>
    </th>
    <TH WIDTH="9"> </th>
  </TR>

  <script type="text/javascript">
    var myCircle = Circles.create({
      id:           'average-score',
      radius:       50,
      value:        <%= @avg_of_avg.to_i %>,
      maxValue:     100,
      width:        15,
      text:         '<%=@avg_of_avg.to_i%>',
      colors:       ['#FFEB99', '#FFCC00'],
      duration:       700,
      textClass:      'circles-final'
    });
  </script>

  <style>
   .circles-final{
      font-size: 16px !important;
    }
  </style>

Test Plan

The specification of the project does not require us to use automated tests. However, we have tested the existing tests after the addition of new features to the grades_controller. We have approached RSpec testing framework. The UI feature tests are conducted manually.

JavaScript Chart

To validate all functionality of the chart when adding new features or fixing old ones, the following criteria were tested manually for expected functionality:

  1. Chart is displaying correctly
    1. Bars are showing up where expected
    2. Bar annotations are showing the expected value
    3. Criteria labels are for the correct bar and displaying correct values
    4. Hover text is displaying the correct values
    5. Null values are not present on the chart
    6. Correct colors are used for the multi-round view
  2. Show Labels checkbox works as expected
  3. Round Criteria is displaying correctly
    1. Round dropdown menu shows all rounds for the assignment
    2. Selecting a round changes the criteria checkboxes
    3. All checkboxes are displayed with appropriate text
    4. Checkboxes correctly remove or add criterion bars to the chart

RSpec Tests

We have modified one of the existing tests for grades controller because the addition of new functionality broke that particular test. The added test handles the chart functionality correctly.

spec/controllers/grades_controller_spec.rb

   describe '#view' do
   before(:each) do
     allow(Answer).to receive(:compute_scores).with([review_response], [question]).and_return(max: 95, min: 88, avg: 90)
     allow(Participant).to receive(:where).with(parent_id: 1).and_return([participant])
     allow(AssignmentParticipant).to receive(:find).with(1).and_return(participant)
     allow(assignment).to receive(:late_policy_id).and_return(false)
     allow(assignment).to receive(:calculate_penalty).and_return(false)
   end
   context 'when current assignment does not vary rubric by round' do
     it 'calculates scores and renders grades#view page' do
       allow(AssignmentQuestionnaire).to receive(:where).with(assignment_id: 1, used_in_round: 2).and_return([])
       # added AssignmentQuestionnaire 'allow' below to handle chart functionality added in E1859
       allow(AssignmentQuestionnaire).to receive(:where).with(assignment_id: 1, questionnaire_id: 1).and_return([assignment_questionnaire])
       allow(ReviewResponseMap).to receive(:get_assessments_for).with(team).and_return([review_response])
       params = {id: 1}
       get :view, params
       expect(controller.instance_variable_get(:@questions)[:review].size).to eq(1)
       expect(response).to render_template(:view)
     end
   end
 end